Go網(wǎng)絡(luò)編程TCP抓包實操示例探究
Go 網(wǎng)絡(luò)編程模型
在實現(xiàn) Go 的 TCP 代碼前,我們先了解一下 Go 的網(wǎng)絡(luò)編程模型。
網(wǎng)絡(luò)編程屬于 IO 的范疇,其發(fā)展可以簡單概括為:多進程 -> 多線程 -> non-block + I/O 多路復(fù)用。
想必讀者在初學(xué) IO 模型時,一定對阻塞和非阻塞、同步和異步感到頭疼,而 I/O 多路復(fù)用的回調(diào)更是讓人抓狂。Go 在設(shè)計網(wǎng)絡(luò)模型時,就考慮到需要幫助開發(fā)者簡化開發(fā)復(fù)雜度,降低心智負擔(dān),同時滿足高性能要求。
Go 語言的網(wǎng)絡(luò)編程模型是同步網(wǎng)絡(luò)編程。它基于 協(xié)程 + I/O 多路復(fù)用 (linux 下 epoll,darwin 下 kqueue,windows 下 iocp,通過網(wǎng)絡(luò)輪詢器 netpoller 進行封裝),結(jié)合網(wǎng)絡(luò)輪詢器與調(diào)度器實現(xiàn)。
用戶層 goroutine 中的 block socket,實際上是通過 netpoller 模擬出來的。runtime 攔截了底層 socket 系統(tǒng)調(diào)用的錯誤碼,并通過 netpoller 和 goroutine 調(diào)度讓 goroutine 阻塞在用戶層得到的 socket fd 上。
Go 將網(wǎng)絡(luò)編程的復(fù)雜性隱藏于 runtime 中:開發(fā)者不用關(guān)注 socket 是否是 non-block 的,也不用處理回調(diào),只需在每個連接對應(yīng)的 goroutine 中以 block I/O 的方式對待 socket 即可。
例如:當(dāng)用戶層針對某個 socket fd 發(fā)起 read 操作時,如果該 socket fd 中尚無數(shù)據(jù),那么 runtime 會將該 socket fd 加入到 netpoller 中監(jiān)聽,同時對應(yīng)的 goroutine 被掛起,直到 runtime 收到 socket fd 數(shù)據(jù) ready 的通知,runtime 才會重新喚醒等待在該 socket fd 上準備 read 的那個goroutine。而這個過程從 goroutine 的視角來看,就像是 read 操作一直 block 在那個 socket fd 上似的。
一句話總結(jié):Go 將復(fù)雜的網(wǎng)絡(luò)模型進行封裝,放在用戶面前的只是阻塞式 I/O 的 goroutine,這讓我們可以非常輕松地實現(xiàn)高性能網(wǎng)絡(luò)編程。
TCP server
在 Go 中,網(wǎng)絡(luò)編程非常容易。我們通過 Go 的 net 包,可以輕松實現(xiàn)一個 TCP 服務(wù)器。
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 } } }() } }
根據(jù)邏輯,我們將以上代碼分成三個部分。
第一部分:端口監(jiān)聽。我們通過 net.Listen("tcp", ":8000")
開啟在端口 8000 的 TCP 連接監(jiān)聽。
第二部分:建立連接。在開啟監(jiān)聽成功之后,調(diào)用 net.Listener.Accept()
方法等待 TCP 連接。Accept
方法將以阻塞式地等待新的連接到達,并將該連接作為 net.Conn
接口類型返回。
第三部分:數(shù)據(jù)傳輸。當(dāng)連接建立成功后,我們將啟動一個新的 goroutine 來處理 c
連接上的讀取和寫入。本文服務(wù)器的數(shù)據(jù)處理邏輯是,客戶端寫入該 TCP 連接的所有內(nèi)容,服務(wù)器將原封不動地寫回相同的內(nèi)容。
TCP client
同樣,通過 net 包也能快速實現(xiàn)一個 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 連接到服務(wù)器正在監(jiān)聽的同一個 localhost:8000
地址。
第二部分:寫入數(shù)據(jù)。當(dāng)連接建立成功后,通過 c.Write()
方法寫入數(shù)據(jù) Hi, gopher?
給服務(wù)器。
第三部分:關(guān)閉連接。啟動一個新的 goroutine,在 10s 后調(diào)用 c.Close()
方法關(guān)閉 TCP 連接。
第四部分:讀取數(shù)據(jù)。除非發(fā)生 error,否則客戶端通過 c.Read()
方法(記住,是阻塞式的)循環(huán)讀取 TCP 連接上的內(nèi)容。
抓包分析
tcpdump 是一個非常好用的數(shù)據(jù)抓包工具,它可以幫助我們捕獲和查看網(wǎng)絡(luò)數(shù)據(jù)包。
現(xiàn)在,我們通過 tcpdump 來抓取上文 TCP 客戶端與服務(wù)器通信全過程數(shù)據(jù)。
tcpdump -S -nn -vvv -i lo0 port 8000
在本例中,通過使用 -i lo0
指定捕獲環(huán)回接口 localhost,使用 port 8000
將網(wǎng)絡(luò)捕獲過濾為僅與端口 8000 通信或來自端口 8000 的流量,-vvv
是為了打印更多的詳細描述信息,-S
顯示序列號絕對值。
當(dāng)運行 tcpdump 后,我們分別啟動服務(wù)端和客戶端代碼。
運行服務(wù)端代碼
$ 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
服務(wù)器和客戶端建立連接之后,從客戶端讀取到數(shù)據(jù) Hi, gopher?
。在 10s 后,由于客戶端關(guān)閉了連接,服務(wù)端讀取到了 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
客戶端和服務(wù)器建立連接之后,發(fā)送數(shù)據(jù)給服務(wù)端,服務(wù)端返回相同的數(shù)據(jù) Hi, gopher?
回來。在 10s 后,客戶端通過一個新的 goroutine 主動關(guān)閉了連接,因此阻塞在 c.Read
的客戶端代碼捕獲到了錯誤:use of closed network connection
。
那我們通過 tcpdump 抓取的本次通信過程如何呢?首先,我們先通過一張圖片回顧一下經(jīng)典的 TCP 通信全過程。
以下是 tcpdump 抓取的結(jié)果
$ 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
我們重點關(guān)注內(nèi)容 Flags []
,其中 [S]
代表 SYN 包,[F]
代表 FIN,[.]
代表對應(yīng)的 ACK 包。例如 [S.]
代表 SYN-ACK,[F.]
代表 FIN-ACK??梢院苊黠@看出 TCP 通信的全過程如下圖所示。
總結(jié)
本文簡單介紹了 Go 同步編程模式的網(wǎng)絡(luò)模型。有了 runtime 中網(wǎng)絡(luò)輪訓(xùn)器與調(diào)度器的參與,使用 Go 進行高性能網(wǎng)絡(luò)編程,高手與菜鳥開發(fā)者的差距被極大地縮小。
Go 原生的 net 庫對 socket 編程進行了很好地封裝,它提供的函數(shù)方法語義明朗,邏輯清晰?;谕骄幊棠J?,每個人都可以很容易地進行 TCP 網(wǎng)絡(luò)編程。利用 tcpdump 工具,我們能夠進行網(wǎng)絡(luò)分析和問題排查,建議實操掌握。
參考
http://chabaoo.cn/article/202349.htm
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-netpoller/
以上就是Go網(wǎng)絡(luò)編程TCP抓包實操示例探究的詳細內(nèi)容,更多關(guān)于Go網(wǎng)絡(luò)編程TCP抓包的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于Golang實現(xiàn)Excel表格的導(dǎo)入導(dǎo)出功能
最近項目開發(fā)中有涉及到Excel的導(dǎo)入與導(dǎo)出功能,特別是導(dǎo)出表格時需要特定的格式,所以本文給大家介紹了基于Golang實現(xiàn)Excel表格的導(dǎo)入導(dǎo)出功能,文中通過代碼示例和圖文介紹的非常詳細,需要的朋友可以參考下2023-12-12windows安裝部署go超詳細實戰(zhàn)記錄(實測有用!)
Golang語言在近年來因為其高性能、編譯速度快、開發(fā)成本低等特點逐漸得到大家的青睞,這篇文章主要給大家介紹了關(guān)于windows安裝部署go超詳細實戰(zhàn)的相關(guān)資料,需要的朋友可以參考下2023-02-02