1. 程式人生 > >Socket 網路程式設計實踐經驗

Socket 網路程式設計實踐經驗

目錄

相關文章

NOTE:本文假設你已經對 Socket 的使用有一定的瞭解。

Socket 與 HTTP 的區別

首先通過對比法來了解兩者不同的特性:

  • HTTP:超文字傳輸協議,首先它是一個協議,並且是基於 TCP/IP 協議(傳輸層)之上的應用層協議,要想通過 HTTP 來進行通訊,首先需要雙方建立起 TCP/IP 連線,因為TCP/IP 主要解決的問題是資料如何在網路中傳輸而 HTTP 協議主要解決的問題是如何包裝需要傳輸的資料。所以 HTTP 協議能夠支援使用 Header 資訊來詳細規定瀏覽器與伺服器之間的通訊規則。HTTP 連線是基於 Request-Response 的非持久(短)連線

    ,其連線的生命週期通過 Request 界定,一個 Request 一個 Response,此次 HTTP 連線的生命週期結束。對於這點在 HTTP 1.1 中進行了改進,支援 Keep-alive,允許在一個 HTTP 連線的生命週期中,可以傳送多個 Request,接收多個 Response,但由 Request 來界定生命週期的本質沒有改變。

  • Socket:首先需要注意的是 Socket 不是一種協議,而是一種呼叫介面(API)或稱之為 TCP/IP 的基本操作單元。Socket 實際上是對 TCP/IP 協議的封裝,通過 Socket,就能夠應用 TCP/IP 協議來建立連線並傳輸資料。Socket 連線是持久化(長)連線,這種連線的生命週期能夠自主控制,也就是說客戶端和伺服器端一旦建立連線後,理想狀態下連線會持久存在,直到一方自動發出斷開指令。

生產實踐考慮

下面列出在生產專案中應用 Socket 所需要注意的幾點:

  1. 網路斷開重連問題
  2. 連線會話和身份認證問題
  3. 同步和非同步問題
  4. 資料快取問題
  5. 完全斷開連線問題

網路斷開重連問題

的確,Socket 連線在理想的網路環境下是持久的長連線,但實際網路環境是複雜的,網路抖動、路由宕機等各種網路問題都會導致 Socket 連線被動斷開。而且 Socket 沒有提供「自動重連」的機制,所以解決網路斷開重連問題,是 Socket 程式穩定性的重要保證。

思路:在 傳送接收 前檢查 Socket 連線是否依然生效,若不生效,則重新建立 Socket 連線。

那麼首先需要解決的是:如何判斷 Socket 連線狀態是否 ACTIVE?

NOTE:一般的來說,「判斷 Socket 連線狀態是否 ACTIVE」都是服務端的功能需求,因為服務端需要以此來作為是否回收連線資源的依據。而客戶端則無需特別在意,因為即便斷開了連線也只需捕獲異常、重新連線、重新發送即可。

Heartbeat 心跳機制

別名定義

  • 客戶端 Socket == cli-socket
  • 服務端 Socket == ser-socket

一般的 Socket 應用程式邏輯中,ser-socket 應該能夠感知到 cli-socket 的斷開,並且執行相應的斷開邏輯處理,釋放相應的 Socket 連線資源。但實際是,ser-socket 無法有效的區分 cli-socket 是處於長時間空閒還是處於 offline 的狀態,所以也無法確定 cli-socket 的連線是否已經斷開。為解決這個問題,程式設計師所提出的思路就是,遮蔽「長時間空閒」的場景,讓 cli-socket 看起來始終是忙碌的(不斷髮送「無用包」),直到其靜默即表示連線斷開。

這就是較為通用的用於保證連線質量的心跳機制,而 cli-socket 傳送的無用包也稱之為心跳包。所謂心跳包就是 cli-socket 定時傳送簡單的協議資訊給 ser-socket,以此讓 ser-socket 知道 cli-socket 依舊 online。相對的, ser-socket 就會認為 cli-socket 已經斷開。注意,發包方可以是 cli-socket 也可以是 ser-socket,但出於效率的考慮(減輕伺服器壓力),一般由 cli-socket 承擔。如果是流式 Socket(for TCP),則使用 send 發出;如果是資料報式 Socket(for UDP),則使用 sendto 發出。還有一點需要說明,心跳包實際是一個自定義協議包,由開發者制定,並在 cli-socket 和 ser-socket 中遵守。

NOTE:如果僅為了確定 ser-socket 是否 online,可以用 TCP 協議自帶的心跳包,應用 socket.socket.setsockopt 的 SO_KEEPALIVE 屬性,來設定發包時間間隔。SO_KEEPALIVE 是作業系統的底層機制,用於維護每一個 TCP 連線。但SO_KEEPALIVE 並不能用於替代心跳機制,因為其僅能確保 ser-socket 一方的連線狀態。

使用非阻塞模式下的 select 函式進行 Socket 連線檢查

以非同步(非阻塞)模式建立連線 s.setblocking(0),如果 select 函式返回的值為 1 (表示 Socket 可讀),但使用 recv 函式讀取到的資料長度為 0,並且 errno != EINTR and errno != EAGAIN,則說明該 Socket 已經斷開

NOTE:需要注意的是,在非阻塞模式下,即便 recv 函式的返回值小於等於 0,依舊不足以證明問題。此時還需要繼續判斷 errno ?= EINTR,如果 Yes,則說明此次 recv 是由於程式接收到 EINTR 中斷訊號後返回的,Socket 連線仍然正常。除此之外,如果 write 寫的太快,很有可能會把 Buffer 寫滿,這時的 errno == EAGAIN。根據實際需要,如果 errno == EAGAIN 的話,建議重試幾次。當然,Read 也有類似的情況。

會話過期問題

如果服務端程式應用了 Session 機制,那麼在實現客戶端程式時除了需要考慮 Socket 連線的問題之外,還需要考慮 Session 是否過期的問題。

傳送接收 前首先檢查連線是否有效,然後檢查會話是否過期:

  • 連線失效:則重新建立連線,並且重新建立 session
  • 連線有效,但會話過期:則重新建立 session
  • 連線有效,會話有效:PASS

同步還是非同步問題

選擇同步還是非同步模式是非常重要的,使用了錯誤的連線模式將無法達到預期效果。例如高併發需求沒能達到,例如程式的穩定性沒能提高等等,如何進行選擇需要結合實際的應用場景:

  • 在高併發且不關注執行結果的場景中使用非同步模式。
  • 在對程式執行的穩定性,對執行結果響應的準確性都有很高要求的場景下使用同步模式,並且需要保證一次 send 和 recv 的原子性。

NOTE:對於後者,應該儘可以能僅保證 send 和 recv 原子操作是同步的,以此來優化效率。

資料快取問題

Socket 的 recv 具有快取功能,如果其中一方傳送的資料量超過了另一方 recv 所允許一次接受的最大資料量,資料會被截短,並將剩餘的資料緩衝在接收端。再次呼叫 recv 時,剩餘的資料會從緩衝區取出並刪除。這一特性與 HTTP 連線方式有很大區別,表示 Socket 連線無法像 HTTP 連線那般,一次 Request 對應的一次 Response 就能完成一次操作單元,而是很可能需要任意次的 send 以及任意次的 recv 才能完成。換句話說就是,需要開發者來保證 send 和 recv 的完整性,你需要手動的整理、組合出完整的傳送和響應結果資料。經常的,為了得到一個完整的響應結果可能需要進行多次 recv。

完全斷開連線問題

呼叫 Socket 的 close 函式並不會馬上斷開 Socket 連線,一般的我們會在 close 之前呼叫 shutdown 函式來確保連線會被正常關閉。而且 shutdown 提供了多種不同的關閉方式:

  • SHUT_RD:關閉讀,不能使用 read/recv
  • SHUT_WR:關閉寫,不能使用 send/write
  • SHUT_RDWR:關閉讀寫,不能使用 send/write/recv/read

NOTE:在客戶端程式中,一般我們會選擇使用 SHUT_WR 方式,立即停止寫操作,但可以繼續將響應資料讀完。而在服務端程式中一般會選擇 SHUT_RD 模式,立即停止對客戶端請求的讀取,但會繼續完成響應。當然了,在某些對精度要求不要的場景中,SHUT_RDWR 是不錯的選擇。