Java必知必會之socket

weixin_33672400發表於2016-05-15

Internet上,資料按有限大小的包傳輸,這些包成為資料包(datagram),每個資料包包含一個首部(header)和一個有效載荷(payload)。首部包含包傳送到的地址和埠、包來自的地址和埠、檢測資料是否被破壞的校驗和,以及用於保證可靠傳輸的各種其他管理資訊。

有效載荷包含資料本身。

不過,由於資料包長度有限,通常必須將資料分解為多個包,再在目的地重新組合。也有可能一個包或多個包在傳輸中丟失或遭到破壞,需要重傳。或者包亂序到達,需要重新排序。所有這些(將資料分解為包、生成首部、解析入站包的首部、跟蹤哪些包已經收到而哪些沒有收到等)是很繁重的工作,需要大量複雜的程式碼。

Socket幫你掩蓋了這些底層細節,如錯誤檢測、包大小、包分解、包重傳、網路地址等。Socket允許程式設計師將網路連線看作是另外一個可以讀寫位元組的流。

Socket是兩臺主機之間的一個連線,它可以完成7個基本操作:
1)連線遠端主機
2)傳送資料
3)接收資料
4)關閉連線
5)繫結埠
6)監聽入站資料
7)在繫結埠上接受來自遠端機器的連線

一旦建立了socket連線,就可以使用輸入輸出流,這個連線是全雙工的(full-duplex),兩臺主機都可以同時傳送和接收資料。

SMTP是伺服器之間或郵件客戶端與伺服器之間傳輸電子郵件所用的協議。

半關閉Socket:close方法同時關閉Socket的輸入流和輸出流,如果只希望關閉連線的一半(輸入/輸出),呼叫shutdownInput或shutdownOutput方法即可。這兩個方法並不關閉Scoket,實際上,它會調整與Socket連線的流,使它認為已經到了流的末尾。關閉輸入之後,再讀取輸入流會返回-1,關閉輸出流之後再寫入Socket則會丟擲一個IOException異常。
即使半關閉了連線,或者將連線的兩半都關閉了,使用結束後還是需要關閉該Socket。shutdown只是影響了socket流,並不釋放與socket關聯的資源,如佔用的埠等。

java.net.Socket是Java完成客戶端TCP操作的基礎類,它使用原生程式碼與主機作業系統的本地TCP棧進行通訊。

public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress host, int port) throws IOException

這兩個建構函式,在返回之前會與遠端主機建立一個活動的網路連線。port在1~65535之間。

public Socket()
public Socket(Proxy proxy)
protected Socket(SocketImpl impl)
這三個函式可以建立未連線的Socket。

public Socket(String host, int port, InetAddress interface, int localPort)

  throws IOException, UnknownHostException

public Socket(InetAddress host, int port, InetAddress interface, int localPort)

  throws IOException

這兩個建構函式可以用來指定從哪個介面和埠連線。

SocketAddress

SocketAddress表示一個連線端點,理論上可以用於TCP和非TCP socket。實際上只支援TCP/IP Socket。

SocketAddress主要是為了暫時的socket連線資訊(如IP地址和埠)提供一個方便的儲存,即使最初的socket已斷開並被垃圾回收,這些資訊也可以重用來建立新的Socket。

boolean connected = socket.isConnected() && ! socket.isClosed();

isConnected表示是否連線過一個遠端主機,即使socket已經關閉,因而要判斷socket是否開啟著的,還要判斷是否已經關閉了。

Socket選項

1)TCP_NODELAY

設定為true,可確保包會盡可能快地傳送,而不論包的大小。
正常情況下,小資料包在傳送前會組合為更大的包,在傳送另一個包之前,本地主機要等待遠端系統對前一個包的確認,這稱為Nagle演算法。

Nagle演算法的問題是如果遠端系統沒有足夠快地將確認傳送回本地系統,那麼依賴於小資料量資訊穩定傳輸的應用程式會變慢。對於網路或遊戲等計算機應用程式(伺服器需要實時跟蹤客戶端滑鼠的移動),這個問題尤為嚴重,在一個相當慢的網路中,即使簡單地打字也會由於持續的緩衝而變得太慢。設定為true,可以關閉這種緩衝模式,這樣素有包一旦就緒就會傳送。

2)SO_LINGER

指定了Socket關閉時如何處理尚未傳送的資料包,預設情況下,close方法將立即返回,但系統仍然會嘗試傳送剩餘的資料,如果延遲時間設定為0,那麼當Socket關閉時,所有未傳送的資料包都將被丟棄,如果SO_LINGER開啟而且延遲時間設定為任意正數,close方法會阻塞指定的時間,等待傳送資料和接收確認。指定時間一過,Socket關閉,所有剩餘的資料都不會傳送,也不會接收確認。
返回-1表示該選項被禁用,會根據需要用更多的時間傳送剩餘的資料。

3)SO_TIMEOUT

正常情況下,嘗試從Socket讀取資料時,read()呼叫會阻塞儘可能長的時間來得到足夠的位元組。設定timeout確保這個呼叫會阻塞的時間不會超過指定的閾值,如果超出則拋異常,但是Socket仍然是連線的,可以再次嘗試肚餓去這個Socket,下一次呼叫可能會成功。
0表示無限超時。

4)SO_RCVBUF和SO_SNDBUF

TCP使用緩衝區來提升網路效能,較大的緩衝區會提升快速連線(比如10M/s)的效能,而較慢的撥號連線利用較小的緩衝區會有更好地表現。

一般來講,傳輸連續的大資料塊時(在ftp和http中較為常見),可以從大緩衝區收益,而對於互動式會話的小資料量傳輸(比如telnet和很多遊戲),大緩衝區則沒有多大幫助。如今128K位元組已經是一個常見的預設設定。

可以達到的最大頻寬=緩衝區大小/延遲。例如,xp上,假設兩個主機之間的延遲為500ms,xp上的緩衝區大小為17520位元組,則頻寬=17520/0.5=273.75kb/s。這是Socket的最大速度,而不論網路速度有多快。對於一個撥號連線來說,這樣的速度已經很快了。

可以通過減少延遲來提升速度,不過,延遲與網路硬體有關,另外還取決於你的應用控制之外的其他一些因素。
如果把緩衝區從17520位元組提升到128K,則最大頻寬會增加到2Mb/s,如果加到256K時,最大頻寬會增大到4Mb/s。

當然網路本身對最大頻寬也是有限制的,如果將緩衝區設定的過高,程式會試圖以過高的速度傳送和接收資料,而網路來不及處理,這就會導致擁塞、丟包和效能下降。因此,要得到最大頻寬,需要讓緩衝區大小與連線的延遲匹配,是它稍小於網路的頻寬。

可以用ping去檢測主機的延遲。

SO_RCVBUF控制用於網路輸入的建議的接收緩衝區的大小,雖然可以獨立地設定接收和傳送緩衝區的大小,但是實際上緩衝區通常會設定為二者中較小的一個。

Linux系統通常指定一個最大緩衝區大小,一般是64KB或256KB,而且不允許任何socket有更大的緩衝區。

一般情況下,如果你傳送你的應用不能充分利用可用頻寬(例如,你有一個25Mb/s的Internet連線,但是資料傳輸速率僅為1.5Mb/s),那麼可以試著增加緩衝區的大小;相反,如果存在丟包和擁塞現象,則要減少緩衝區大小。

不過,大部分情況,除非網路在某個方向上負載過大,否則預設值就很合適。具體來說,現代作業系統使用TCP視窗縮放來動態調整緩衝區的大小,以適應網路。

一般經驗是,除非你檢測到某個問題,否則不要進行調整。一般調整作業系統的最大緩衝區比在Java裡頭調整單個socket的緩衝區大小效益要高。

5)SO_KEEPALIVE

如果開啟這個,客戶端偶爾會通過一個空閒連線傳送一個資料包(一般兩小時一次),以確保伺服器沒有崩潰。如果伺服器沒能響應這個包,客戶端會持續嘗試11分鐘多的時間,直到接收到響應為止。如果在12分鐘內未收到響應,則客戶端就關閉socket。如果不開啟這個,不活動的客戶端可能會永遠存在下去,而不會注意到伺服器是否已經崩潰。

6)OOBINLINE

TCP包括一個可以傳送單位元組帶外(Out Of Band,OOB)緊急資料的特性。這個資料會立即傳送,此外,當接收方收到緊急資料時會得到通知,在處理其他已收到的資料之前可以選擇先處理這個緊急資料(必要時flush當前緩衝區)。

Java裡對應的方法是sendUrgentData

預設情況下,Java會忽略從Socket接收的緊急資料,如果希望接收到正常資料中的緊急資料,需要setOOBInline為true。一旦開啟,到達的任何緊急資料將以正常方式放在Socket的輸入流中等待讀取。

7)SO_REUSEADDR

一個Socket關閉時,可能不會立即釋放本地埠,尤其是當Socket關閉時若仍有一個開啟的連線,就不會釋放本地埠,有時會等待一小段時間,確保接收到所有要傳送到這個埠的延遲資料包,Socket關閉時這些資料包可能仍在網路上傳輸,系統不會對接收到的延遲包做任何處理,只是希望確保這些資料不會意外地傳入繫結到同一埠的新程式。

如果使用隨機埠,則問題不大,但是如果Socket繫結到一個已知的埠,可能會有問題,因為這會阻止所有其他Socket同時使用這個埠,如果開啟這個引數(預設是關閉),則允許另外一個Socket繫結到這個埠,即使此時仍有可能存在前一個Socket未接收的資料。

setReuseAddress必須在繫結新Socket之前呼叫。只有之前連線的Socket和重用老地址的新Scoket的這個值都設定為true,才能生效。

8)IP_TOS

不同型別的Internet服務對效能有不同的需求,比如視訊要求相對較高的頻寬和較短的延遲,而email可以通過較低頻寬的連線傳遞等。
服務型別儲存在IP首部中的一個名為IP_TOS的8位欄位中。在Java中使用setTrafficClass來設定,java裡頭是0-255,但是TCP首部要求是8位,因而只能使用int的低位元組。

Socket異常
1)BindException,埠被佔用或沒有許可權使用該埠
2)ConnectException,連線遠端主機被拒絕(遠端主機忙或者沒有程式監聽該埠)
3)NoRouteToHostException,連線超時
4)ProtocolException,違反TCP/IP規定

相關文章