華文網

Golang進階——TCP網路程式設計詳解

介紹

Golang是谷歌設計開發的語言,在Golang的設計之初就把高併發的性能作為Golang的主要特性之一,也是面向大規模後端服務程式。在伺服器端網路通信是必不可少的也是至關重要的一部分。

Golang內置的包例如net、net/http中的底層就是對TCP socket方法的封裝。

這裡簡單介紹一下TCP。TCP(Transmission Control Protocol)傳輸控制協議,是一種連線導向的、可靠的、基於位元組流的傳輸層通信協議,也叫做可靠的傳輸協議。屬於OSI七層模型中的傳輸層協定。相比可靠的就會有不可靠的——UDP(User Datagram Protocol)使用者資料包通訊協定,也叫做不可靠的傳輸協議。這裡的可靠和不可靠只是它們的側重點不同。TCP強調資料的完整性,UDP注重資料的即時行。

目前大部分的資料傳輸都使用的是TCP協定,而在一些視頻、直播、或者網路電話採用的就是UDP協定。

模型

言歸正傳,今天主要介紹如何使用Go語言進行TCP socket程式設計。目前主流web server一般均採用的都是”Non-Block + I/O多工”。不過I/O多工使用起來依舊很複雜,以至於後續出現了許多高性能的I/O多工框架, 比如libevent、libev、libuv等大大降低了開發的成本。不過Go的設計者似乎認為I/O多工的這種通過回檔機制割裂控制流 的方式還是很複雜,

為此Go語言將該“複雜性”隱藏在Runtime中了:開發者無需關注socket是否是 non-block的,也無需親自註冊檔描述符的回檔,只需在每個連接對應的goroutine中以“block I/O”的方式對待socket處理即可。一個經典的服務端例子如下:

雖然用戶層眼中看到的goroutine中的“block socket”,實際上是通過Go runtime中的netpoller通過Non-block socket + I/O多工機制“模擬”出來的,

真實的underlying socket實際上是non-block的,只是runtime攔截了底層socket系統調用的錯誤碼,並通過netpoller和goroutine 調度讓goroutine“阻塞”在用戶層得到的Socket fd上。

TCP連接建立

TCP Socket的連接的建立需要經歷用戶端和服務端的三次握手的過程。三次握手大致流程如下:

第一次

第一次握手:建立連接時,用戶端發送syn包(syn=j)到伺服器,並進入SYN_SENT狀態,等待伺服器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。

第二次

第二次握手:伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),

即SYN+ACK包,此時伺服器進入SYN_RECV狀態;

第三次

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

完成三次握手,用戶端與伺服器開始傳送資料

連接建立過程中,服務端是一個標準的Listen + Accept的結構(可參考上面的代碼),而在用戶端Go語言使用net.Dial()或net.DialTimeout()進行連接建立

阻塞Dial:

超時機制的Dial:

Socket通訊端讀寫

連接建立起來後,

我們就要在conn上進行讀寫,以完成業務邏輯。前面說過Go runtime隱藏了I/O多工的複雜性。語言使用者只需採用goroutine+Block I/O的模式即可滿足大部分場景需求。Dial成功後,方法返回一個net.Conn介面類別型變數值,這個介面變數的動態類型為一個*TCPConn:

TCPConn內嵌了一個unexported類型:conn,因此TCPConn”繼承”了conn的Read和Write方法,後續通過Dial返回值調用的Write和Read方法均是net.conn的方法:

基於goroutine的網路架構模型,存在在不同goroutine間共用conn的情況,那麼conn的讀寫是否是goroutine safe的呢?在深入這個問題之前,我們先從應用意義上來看read操作和write操作的goroutine-safe必要性。對於read操作而言,由於TCP是面向位元組流,conn.Read無法正確區分資料的業務邊界,因此多個goroutine對同一個conn進行read的意義不大,goroutine讀到不完整的業務包反倒是增加了業務處理的難度。對與Write操作而言,倒是有多個goroutine併發寫的情況。每次Write操作都是受lock保護,直到此次資料全部write完。因此在應用層面,要想保證多個goroutine在一個conn上write操作的Safe,需要一次write完整寫入一個“業務包”;一旦將業務包的寫入拆分為多次write,那就無法保證某個Goroutine的某“業務包”資料在conn發送的連續性

基於goroutine的網路架構模型,存在在不同goroutine間共用conn的情況,那麼conn的讀寫是否是goroutine safe的呢?在深入這個問題之前,我們先從應用意義上來看read操作和write操作的goroutine-safe必要性。對於read操作而言,由於TCP是面向位元組流,conn.Read無法正確區分資料的業務邊界,因此多個goroutine對同一個conn進行read的意義不大,goroutine讀到不完整的業務包反倒是增加了業務處理的難度。對與Write操作而言,倒是有多個goroutine併發寫的情況。每次Write操作都是受lock保護,直到此次資料全部write完。因此在應用層面,要想保證多個goroutine在一個conn上write操作的Safe,需要一次write完整寫入一個“業務包”;一旦將業務包的寫入拆分為多次write,那就無法保證某個Goroutine的某“業務包”資料在conn發送的連續性