Go網絡編程TCP抓包實操示例探究
Go 網絡編程模型
在實現 Go 的 TCP 代碼前,我們先了解一下 Go 的網絡編程模型。
網絡編程屬于 IO 的范疇,其發(fā)展可以簡單概括為:多進程 -> 多線程 -> non-block + I/O 多路復用。
想必讀者在初學 IO 模型時,一定對阻塞和非阻塞、同步和異步感到頭疼,而 I/O 多路復用的回調更是讓人抓狂。Go 在設計網絡模型時,就考慮到需要幫助開發(fā)者簡化開發(fā)復雜度,降低心智負擔,同時滿足高性能要求。
Go 語言的網絡編程模型是同步網絡編程。它基于 協(xié)程 + I/O 多路復用 (linux 下 epoll,darwin 下 kqueue,windows 下 iocp,通過網絡輪詢器 netpoller 進行封裝),結合網絡輪詢器與調度器實現。
用戶層 goroutine 中的 block socket,實際上是通過 netpoller 模擬出來的。runtime 攔截了底層 socket 系統(tǒng)調用的錯誤碼,并通過 netpoller 和 goroutine 調度讓 goroutine 阻塞在用戶層得到的 socket fd 上。
Go 將網絡編程的復雜性隱藏于 runtime 中:開發(fā)者不用關注 socket 是否是 non-block 的,也不用處理回調,只需在每個連接對應的 goroutine 中以 block I/O 的方式對待 socket 即可。

例如:當用戶層針對某個 socket fd 發(fā)起 read 操作時,如果該 socket fd 中尚無數據,那么 runtime 會將該 socket fd 加入到 netpoller 中監(jiān)聽,同時對應的 goroutine 被掛起,直到 runtime 收到 socket fd 數據 ready 的通知,runtime 才會重新喚醒等待在該 socket fd 上準備 read 的那個goroutine。而這個過程從 goroutine 的視角來看,就像是 read 操作一直 block 在那個 socket fd 上似的。
一句話總結:Go 將復雜的網絡模型進行封裝,放在用戶面前的只是阻塞式 I/O 的 goroutine,這讓我們可以非常輕松地實現高性能網絡編程。
TCP server
在 Go 中,網絡編程非常容易。我們通過 Go 的 net 包,可以輕松實現一個 TCP 服務器。
package main
import (
"log"
"net"
)
func main() {
// Part 1: create a listener
l, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatalf("Error listener returned: %s", err)
}
defer l.Close()
for {
// Part 2: accept new connection
c, err := l.Accept()
if err != nil {
log.Fatalf("Error to accept new connection: %s", err)
}
// Part 3: create a goroutine that reads and write back data
go func() {
log.Printf("TCP session open")
defer c.Close()
for {
d := make([]byte, 100)
// Read from TCP buffer
_, err := c.Read(d)
if err != nil {
log.Printf("Error reading TCP session: %s", err)
break
}
log.Printf("reading data from client: %s\n", string(d))
// write back data to TCP client
_, err = c.Write(d)
if err != nil {
log.Printf("Error writing TCP session: %s", err)
break
}
}
}()
}
}
根據邏輯,我們將以上代碼分成三個部分。
第一部分:端口監(jiān)聽。我們通過 net.Listen("tcp", ":8000")開啟在端口 8000 的 TCP 連接監(jiān)聽。
第二部分:建立連接。在開啟監(jiān)聽成功之后,調用 net.Listener.Accept()方法等待 TCP 連接。Accept 方法將以阻塞式地等待新的連接到達,并將該連接作為 net.Conn 接口類型返回。
第三部分:數據傳輸。當連接建立成功后,我們將啟動一個新的 goroutine 來處理 c 連接上的讀取和寫入。本文服務器的數據處理邏輯是,客戶端寫入該 TCP 連接的所有內容,服務器將原封不動地寫回相同的內容。
TCP client
同樣,通過 net 包也能快速實現一個 TCP 客戶端。
package main
import (
"log"
"net"
"time"
)
func main() {
// Part 1: open a TCP session to server
c, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatalf("Error to open TCP connection: %s", err)
}
defer c.Close()
// Part2: write some data to server
log.Printf("TCP session open")
b := []byte("Hi, gopher?")
_, err = c.Write(b)
if err != nil {
log.Fatalf("Error writing TCP session: %s", err)
}
// Part3: create a goroutine that closes TCP session after 10 seconds
go func() {
<-time.After(time.Duration(10) * time.Second)
defer c.Close()
}()
// Part4: read any responses until get an error
for {
d := make([]byte, 100)
_, err := c.Read(d)
if err != nil {
log.Fatalf("Error reading TCP session: %s", err)
}
log.Printf("reading data from server: %s\n", string(d))
}
}
將以上代碼分為四個部分。
第一部分:建立連接。我們通過 net.Dial("tcp", "localhost:8000")連接一個 TCP 連接到服務器正在監(jiān)聽的同一個 localhost:8000 地址。
第二部分:寫入數據。當連接建立成功后,通過 c.Write() 方法寫入數據 Hi, gopher? 給服務器。
第三部分:關閉連接。啟動一個新的 goroutine,在 10s 后調用 c.Close() 方法關閉 TCP 連接。
第四部分:讀取數據。除非發(fā)生 error,否則客戶端通過 c.Read() 方法(記住,是阻塞式的)循環(huán)讀取 TCP 連接上的內容。
抓包分析
tcpdump 是一個非常好用的數據抓包工具,它可以幫助我們捕獲和查看網絡數據包。
現在,我們通過 tcpdump 來抓取上文 TCP 客戶端與服務器通信全過程數據。
tcpdump -S -nn -vvv -i lo0 port 8000
在本例中,通過使用 -i lo0 指定捕獲環(huán)回接口 localhost,使用 port 8000 將網絡捕獲過濾為僅與端口 8000 通信或來自端口 8000 的流量,-vvv是為了打印更多的詳細描述信息,-S 顯示序列號絕對值。
當運行 tcpdump 后,我們分別啟動服務端和客戶端代碼。
運行服務端代碼
$ go run main.go 2021/09/20 19:41:17 TCP session open 2021/09/20 19:41:17 reading data from client: Hi, gopher? 2021/09/20 19:41:27 Error reading TCP session: EOF
服務器和客戶端建立連接之后,從客戶端讀取到數據 Hi, gopher? 。在 10s 后,由于客戶端關閉了連接,服務端讀取到了 EOF 錯誤。
運行客戶端代碼
$ go run main.go 2021/09/20 19:41:17 TCP session open 2021/09/20 19:41:17 reading data from server: Hi, gopher? 2021/09/20 19:41:27 Error reading TCP session: read tcp 127.0.0.1:57596->127.0.0.1:8000: use of closed network connection
客戶端和服務器建立連接之后,發(fā)送數據給服務端,服務端返回相同的數據 Hi, gopher? 回來。在 10s 后,客戶端通過一個新的 goroutine 主動關閉了連接,因此阻塞在 c.Read 的客戶端代碼捕獲到了錯誤:use of closed network connection。
那我們通過 tcpdump 抓取的本次通信過程如何呢?首先,我們先通過一張圖片回顧一下經典的 TCP 通信全過程。

以下是 tcpdump 抓取的結果
$ tcpdump -S -nn -vvv -i lo0 port 8000
tcpdump: listening on lo0, link-type NULL (BSD loopback), capture size 262144 bytes
19:41:17.109462 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)
127.0.0.1.57596 > 127.0.0.1.8000: Flags [S], cksum 0xfe34 (incorrect -> 0x18e6), seq 2046827845, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 678438397 ecr 0,sackOK,eol], length 0
19:41:17.109547 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64, bad cksum 0 (->3cb6)!)
127.0.0.1.8000 > 127.0.0.1.57596: Flags [S.], cksum 0xfe34 (incorrect -> 0x8b10), seq 1697569320, ack 2046827846, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 678438397 ecr 678438397,sackOK,eol], length 0
19:41:17.109558 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.57596 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0xec19), seq 2046827846, ack 1697569321, win 6379, options [nop,nop,TS val 678438397 ecr 678438397], length 0
19:41:17.109567 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.8000 > 127.0.0.1.57596: Flags [.], cksum 0xfe28 (incorrect -> 0xec19), seq 1697569321, ack 2046827846, win 6379, options [nop,nop,TS val 678438397 ecr 678438397], length 0
19:41:17.109767 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 63, bad cksum 0 (->3cb7)!)
127.0.0.1.57596 > 127.0.0.1.8000: Flags [P.], cksum 0xfe33 (incorrect -> 0xfb32), seq 2046827846:2046827857, ack 1697569321, win 6379, options [nop,nop,TS val 678438397 ecr 678438397], length 11
19:41:17.109781 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.8000 > 127.0.0.1.57596: Flags [.], cksum 0xfe28 (incorrect -> 0xec0e), seq 1697569321, ack 2046827857, win 6379, options [nop,nop,TS val 678438397 ecr 678438397], length 0
19:41:17.109862 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 152, bad cksum 0 (->3c5e)!)
127.0.0.1.8000 > 127.0.0.1.57596: Flags [P.], cksum 0xfe8c (incorrect -> 0xface), seq 1697569321:1697569421, ack 2046827857, win 6379, options [nop,nop,TS val 678438397 ecr 678438397], length 100
19:41:17.109872 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.57596 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0xebab), seq 2046827857, ack 1697569421, win 6378, options [nop,nop,TS val 678438397 ecr 678438397], length 0
19:41:27.113831 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.57596 > 127.0.0.1.8000: Flags [F.], cksum 0xfe28 (incorrect -> 0xc49f), seq 2046827857, ack 1697569421, win 6378, options [nop,nop,TS val 678448392 ecr 678438397], length 0
19:41:27.113910 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.8000 > 127.0.0.1.57596: Flags [.], cksum 0xfe28 (incorrect -> 0x9d93), seq 1697569421, ack 2046827858, win 6379, options [nop,nop,TS val 678448392 ecr 678448392], length 0
19:41:27.114089 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.8000 > 127.0.0.1.57596: Flags [F.], cksum 0xfe28 (incorrect -> 0x9d92), seq 1697569421, ack 2046827858, win 6379, options [nop,nop,TS val 678448392 ecr 678448392], length 0
19:41:27.114187 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 52, bad cksum 0 (->3cc2)!)
127.0.0.1.57596 > 127.0.0.1.8000: Flags [.], cksum 0xfe28 (incorrect -> 0x9d93), seq 2046827858, ack 1697569422, win 6378, options [nop,nop,TS val 678448392 ecr 678448392], length 0
我們重點關注內容 Flags [],其中 [S] 代表 SYN 包,[F] 代表 FIN,[.] 代表對應的 ACK 包。例如 [S.] 代表 SYN-ACK,[F.] 代表 FIN-ACK??梢院苊黠@看出 TCP 通信的全過程如下圖所示。

總結
本文簡單介紹了 Go 同步編程模式的網絡模型。有了 runtime 中網絡輪訓器與調度器的參與,使用 Go 進行高性能網絡編程,高手與菜鳥開發(fā)者的差距被極大地縮小。
Go 原生的 net 庫對 socket 編程進行了很好地封裝,它提供的函數方法語義明朗,邏輯清晰?;谕骄幊棠J剑總€人都可以很容易地進行 TCP 網絡編程。利用 tcpdump 工具,我們能夠進行網絡分析和問題排查,建議實操掌握。
參考
http://www.fzitv.net/article/202349.htm
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-netpoller/
以上就是Go網絡編程TCP抓包實操示例探究的詳細內容,更多關于Go網絡編程TCP抓包的資料請關注腳本之家其它相關文章!
相關文章
Go中Goroutines輕量級并發(fā)的特性及效率探究
這篇文章主要為大家介紹了Go中Goroutines輕量級并發(fā)的特性及效率探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12
gin自定義中間件解決requestBody不可重讀(請求體取值)
這篇文章主要介紹了gin自定義中間件解決requestBody不可重讀,確保控制器能夠獲取請求體值,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-10-10

