1. 程式人生 > >TCP/IP和Socket開發經驗分享

TCP/IP和Socket開發經驗分享

當前與網路相關的業務主要是基於tcp/ip或http,熟悉j2ee的同學一定會對http場景下的開發比較瞭解。但是,精通tcp/ip以及如何構建一個直接基於tcp/ip層通訊的知識卻不太多見。恰巧,最近一年來我參與了一些基於tcp/ip應用的開發工作。總算有所收穫,今天在部落格中做些分享,希望對有興趣的同學有所幫助。

比較常見的4層網路模型(圖)如下:

基於應用層的開發難度是相對比較低的,因為絕大部分與連線和資料傳輸、校驗相關的事情已經交給(系統)來完成,使得開發人員只需要專注於業務即可。這種分層的技術結構是非常高階和有效的。基於應用層的開發雖然方便,但是當我們需要在功能上實現某些特殊需求的時候,就難免有些掣肘。例如,我們需要從一些感測器上採集資料或希望他們能夠主動將資料上送,並在經過了中心繫統處理後推送到其它響應裝置。這樣的需求使用http來開發,反而增大了難度。

作業系統實際已經為我們提供了一種基於傳輸層的通訊方式:套接字(socket)。使用套接字可以讓我們自由定義通訊協議並選擇合適的連線方式。

利用socket實現網路通訊分為服務端和客戶端,服務端繫結埠並主動監聽連線,客戶端需要向服務端發起連線。建立一次tcp連線需要進過“三次”握手:

第一次握手:客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認;

第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;

第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。

三次握手被抽象成socket連線,這個過程服務端和客戶端會分別生成一個socket並通過在這個套接字上的連線收發資料。那麼問題產生了,假如我們知道服務端對8081埠進行監聽,客戶端會隨機開啟一個高位埠進行連線。連線建立後,服務端是在哪個埠上監聽資料的呢?答案是8081埠,服務端會根據埠上資料的源地址和埠判斷從而將資料分發到正確的應用上去。

理解這一點其實很重要,如果此時通訊的雙方沒有任何資料交換,socket也無法判斷連線是否被斷開。任意一方必須首先通知socket斷開連線,整個通訊過程才算結束。如果中間網路中斷,連線會一直處於等待狀態。

利用socket程式設計的另一個難點是,由於通訊的雙方完全對等任何一方都可以主動傳送資料,如何實現在http應用中常見的請求/應答會比較麻煩。為此我專門查閱了http1.0和http1.1的相關資料,基本的解決方案總結如下:

  1. 客戶端等待:客戶端傳送請求後,都需要進行堵塞並直到接收到應答或超時為止。這個是http1.0的協議規範,整個資料的互動方式是序列的。
  2. 服務端等待:序列的執行方式實際上浪費了大量的系統運算時間,使得網路通訊很容易成為整個系統的瓶頸。於是http1.1協議做了更改,客戶端只要準備好請求就可以直接傳送,服務端可能會一次性接收到多條請求,但是隻能按照請求的順序依次應答。

伺服器的運算能力通常都比客戶端強,第二種解決方案能更加有效的利用網路。但是,如果有一條請求需要請求佔用服務端大量的運算時間,後續應答都會被堵塞,因此在某些情況下也會引發比較嚴重的問題。

為了解決這個問題,我借鑑了spring kafka在實現訊息互動的時候提供的一種解決思路:為每一條請求指定一個ID,經過服務端處理後的應答都需要帶上這個ID。這樣在回覆給客戶端的時候,客戶端就可以根據這條ID值來呼叫不同的回撥處理業務。

與tcp/ip開發的總結,大致如此。後面,我還會分享一些基於技術的實際專案,如果你對這些問題有興趣,也歡迎給我留言討