前面已經(jīng)看到,Socket
類的 getInputStream()
和 getOutStream()
方法分別獲取套接字的輸入流和輸出流。輸入流用來讀取遠(yuǎn)端發(fā)送過來的數(shù)據(jù),輸出流則用來向遠(yuǎn)端發(fā)送數(shù)據(jù)。
輸入流
使用套接字的輸入流讀取數(shù)據(jù)時,當(dāng)前線程會進(jìn)入阻塞狀態(tài),直到套接字收到一些數(shù)據(jù)為止(亦即套接字的接收緩沖區(qū)有可用數(shù)據(jù))。該輸入流的 available()
方法只是返回接收緩沖區(qū)的可用字節(jié)數(shù)量,不可能知道遠(yuǎn)端還要發(fā)送多少字節(jié)。使用輸入流的時候,最好先將它包裝為一個 BufferedInputStream
,因?yàn)樽x取接收緩沖區(qū)將導(dǎo)致 JVM 和底層系統(tǒng)之間的切換,應(yīng)當(dāng)盡量減少切換次數(shù)以提高性能。BufferedInputStream
的緩沖區(qū)大小最好設(shè)為套接字接收緩沖區(qū)的大小。
如果直接調(diào)用輸入流的 close()
方法來關(guān)閉它,則將導(dǎo)致套接字被關(guān)閉。對此,Socket
類提供了一個 shutdownInput()
方法來禁用輸入流。調(diào)用該方法后,每次讀操作都將返回 EOF
,無法再讀取遠(yuǎn)端發(fā)送的數(shù)據(jù)。對這個 EOF
的檢測,不同的輸入流包裝體現(xiàn)出不同的結(jié)果,可能讀到 -1 個字節(jié),可能讀到的字符串為 null
,還可能收到一個 EOFException
等等。禁用輸入流后,遠(yuǎn)端輸出流的行為是平臺相關(guān)的:
- 在 BSD 平臺上,遠(yuǎn)端的發(fā)送的數(shù)據(jù)能正常接收,然后直接丟棄。遠(yuǎn)端無法知道本端的輸入流已禁用。這和 JDK 文檔描述的行為一致。
- 在 WINSOCK 平臺上,遠(yuǎn)端發(fā)送數(shù)據(jù)將會導(dǎo)致“連接被重置”的錯誤。
- 在 Linux 平臺上,遠(yuǎn)端發(fā)送的數(shù)據(jù)能繼續(xù)接收,直到套接字輸入緩沖區(qū)填滿,之后遠(yuǎn)端再也無法發(fā)送數(shù)據(jù)(若使用阻塞模式則進(jìn)入死鎖)。
禁用輸入流這種技術(shù)并不常用。
輸出流
套接字的輸出操作實(shí)際上僅僅將數(shù)據(jù)寫到發(fā)送緩沖區(qū)內(nèi),當(dāng)發(fā)送緩沖區(qū)填滿且上次的發(fā)送成功后,由底層系統(tǒng)負(fù)責(zé)發(fā)送。如果發(fā)送緩沖區(qū)的剩余空間不夠,當(dāng)前線程就會阻塞。和輸入流類似,最好將輸出流包裝為 BufferedOutputStream
。
如果套接字的雙發(fā)都使用 ObjectInputStream
和 ObjectOutputStream
來讀寫 Java 對象,則必須先創(chuàng)建 ObjectOutputStream
,因?yàn)?ObjectInputStream
在構(gòu)造的時候會試圖讀取對象頭部,如果雙發(fā)都先創(chuàng)建 ObjectInputStream
,則會互相等待對方的輸出,造成死鎖:
// 創(chuàng)建的順序不能顛倒!
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
類似于輸入流,關(guān)閉輸出流也導(dǎo)致關(guān)閉套接字,所以 Socket
類同樣提供了一個 shutdownOutput()
來禁用輸出流。禁用輸出流后,已寫入發(fā)送緩沖區(qū)的數(shù)據(jù)會正常發(fā)送,之后的任何寫操作都會導(dǎo)致 IOException
,且遠(yuǎn)端的輸入流始終會讀到 EOF
。禁用輸出流非常有用,例如套接字的雙發(fā)都在發(fā)送完畢數(shù)據(jù)后禁用輸入流,然后雙方都會收到 EOF
,從而知道數(shù)據(jù)已經(jīng)全部交換完畢,可以安全關(guān)閉套接字。直接關(guān)閉套接字會同時關(guān)閉輸入流和輸出流,且斷開連接,達(dá)不到這種效果。
使用流的阻塞套接字的優(yōu)缺點(diǎn)
如果要使用流進(jìn)行輸入和輸出,就只能用阻塞模式的套接字。這里總結(jié)一下阻塞套接字的優(yōu)缺點(diǎn)。先看看優(yōu)點(diǎn):
- 編程模型簡單,非常適合初學(xué)者上手。
- 以裝飾器模式設(shè)計(jì)的 Java I/O 使得開發(fā)人員可以輕松地從 I/O 流讀寫任何類型的數(shù)據(jù)。
但在性能方面有致命的缺點(diǎn):
- 由于服務(wù)器套接字接受連接,以及套接字的讀寫都會阻塞,性能低下。
- 如果不對 I/O 流手動進(jìn)行緩沖,則可能造成一次只處理一個字節(jié),性能低下。
- 服務(wù)器套接字每次只能接受一個連接,導(dǎo)致 JVM 和底層系統(tǒng)之間頻繁的調(diào)用切換,性能低下。
下一篇文章開始探討使用基于 NIO 的套接字通道和緩沖區(qū)實(shí)現(xiàn)伸縮性更強(qiáng)的 TCP 套接字。