使用tcpdump,netstat簡單看下tcp連線握手與揮手的過程和核心緩衝區的變化

這個冬天有點冷灬發表於2020-11-11
package main

import (
	"fmt"
	"net"
)

func main() {
	listener, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		fmt.Printf("listen failed err: %v", err)
		return
	}
	var a int
	// 阻塞在這裡
	fmt.Scanf("%d", &a)
	fmt.Println("start accept")
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("accept failed err: %v", err)
			break
		}
		go HandleConn(conn)
	}
}

func HandleConn(conn net.Conn) {
	defer conn.Close()
	buff := make([]byte, 1024)
	for {
		n, err := conn.Read(buff)
		if err != nil {
			fmt.Printf("read failed conn:%v err:%v\n", conn, err)
			break
		}
		msg := string(buff[0:n])
		fmt.Println(msg)
	}
}

核心緩衝區的變化

上面的程式碼listen了一個埠之後,並沒有去accept,因為被scanf阻塞住了。我們在linux下面跑上面程式碼之後,用netstat -antp 檢視下網路狀態:

tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      36535/server 

然後我們用tcpdump抓個包,使用nc連線下服務(也可使用telnet),命令分別如下:

tcpdump -nn -i lo port 8080
nc 127.0.0.1 8080

此時看到tcpdump抓的包為:

00:22:53.528171 IP 127.0.0.1.47150 > 127.0.0.1.8080: Flags [S], seq 4152235494, win 43690, options [mss 65495,sackOK,TS val 44901801 ecr 0,nop,wscale 7], length 0
09:56:09.032928 IP 127.0.0.1.8080 > 127.0.0.1.47150: Flags [S.], seq 3160960910, ack 4152235495, win 43690, options [mss 65495,sackOK,TS val 44901801 ecr 44901801,nop,wscale 7], length 0
00:22:53.528194 IP 127.0.0.1.47150 > 127.0.0.1.8080: Flags [.], ack 1, win 342, options [nop,nop,TS val 44901801 ecr 44901801], length 0

表示三次握手成功,使用netstat -antp檢視網路狀態結果為:

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:8080          127.0.0.1:47150         ESTABLISHED -

我們可以看到雖然我們的程式並沒有accept,但是連線已經在核心裡面建立好,但是連線的資源還沒交給程式(PID為空)。
我們隨便傳輸幾個字元(例如:123456),發現網路狀態變為:

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program namel
tcp        7      0 127.0.0.1:8080          127.0.0.1:47151         ESTABLISHED -

發現Recv-Q為7,表示核心緩衝區有7個位元組的資料沒有被讀出去(網上看一些資料說,如果核心緩衝被打滿,會有資料包丟失,我本地測下來發現,緩衝區滿了之後,接收端會告訴傳送端 win為0,此時傳送端不在傳送攜帶資料的包,但是這個點還是值得注意下的,業務處理的能力很慢,導致資料包無法及時處理,會有包丟失的情況)。
使用lsof 命令可以看到程式開啟的檔案描述符,命令為 lsof -p ${PID},結果為:

COMMAND   PID USER   FD      TYPE DEVICE SIZE/OFF      NODE NAME
server  37671 kang  cwd       DIR  253,2       22 105990016 /home/kang/test-tcp/server
server  37671 kang  rtd       DIR  253,0     4096       128 /
server  37671 kang  txt       REG  253,0  2252800 135546735 /tmp/go-build605900202/b001/exe/server
server  37671 kang  mem       REG  253,0  2107816 201328826 /usr/lib64/libc-2.17.so
server  37671 kang  mem       REG  253,0   142296 201404660 /usr/lib64/libpthread-2.17.so
server  37671 kang  mem       REG  253,0   164432 201328819 /usr/lib64/ld-2.17.so
server  37671 kang    0u      CHR  136,0      0t0         3 /dev/pts/0
server  37671 kang    1u      CHR  136,0      0t0         3 /dev/pts/0
server  37671 kang    2u      CHR  136,0      0t0         3 /dev/pts/0
server  37671 kang    3u     IPv4 113803      0t0       TCP localhost:webcache (LISTEN)
server  37671 kang    5u  a_inode    0,9        0      6835 [eventpoll]
server  37671 kang    6r     FIFO    0,8      0t0    113804 pipe
server  37671 kang    7w     FIFO    0,8      0t0    113804 pipe

看到37671 這個程式並沒有接收socket連線,這時我們讓程式開始accpet,輸出結果為:

start accept
123456

然後netstat看下網路狀態為:

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:8080          127.0.0.1:47151         ESTABLISHED 37671/server

發現連線已經交給了 37671這個程式,並且核心的Recv-Q資料也被程式讀出
執行lsof -p 37671結果為:

COMMAND   PID USER   FD      TYPE DEVICE SIZE/OFF      NODE NAME
server  37671 kang  cwd       DIR  253,2       22 105990016 /home/kang/test-tcp/server
server  37671 kang  rtd       DIR  253,0     4096       128 /
server  37671 kang  txt       REG  253,0  2252800    816394 /tmp/go-build283231499/b001/exe/server
server  37671 kang  mem       REG  253,0  2107816 201328826 /usr/lib64/libc-2.17.so
server  37671 kang  mem       REG  253,0   142296 201404660 /usr/lib64/libpthread-2.17.so
server  37671 kang  mem       REG  253,0   164432 201328819 /usr/lib64/ld-2.17.so
server  37671 kang    0u      CHR  136,0      0t0         3 /dev/pts/0
server  37671 kang    1u      CHR  136,0      0t0         3 /dev/pts/0
server  37671 kang    2u      CHR  136,0      0t0         3 /dev/pts/0
server  37671 kang    3u     IPv4 115369      0t0       TCP localhost:webcache (LISTEN)
server  37671 kang    4u     IPv4 115372      0t0       TCP localhost:webcache->localhost:47151 (ESTABLISHED)
server  37671 kang    5u  a_inode    0,9        0      6835 [eventpoll]
server  37671 kang    6r     FIFO    0,8      0t0    115370 pipe
server  37671 kang    7w     FIFO    0,8      0t0    115370 pipe

close wait狀態的出現

我們把上述程式碼 defer conn.Close() 註釋掉,使用nc發起一個連線,主動關閉,使用tcpdump抓包情況如下:

01:26:31.145725 IP 127.0.0.1.47153 > 127.0.0.1.8080: Flags [F.], seq 1, ack 1, win 342, options [nop,nop,TS val 48719418 ecr 48713789], length 0
01:26:31.146179 IP 127.0.0.1.8080 > 127.0.0.1.47153: Flags [.], ack 2, win 342, options [nop,nop,TS val 48719419 ecr 48719418], length 0

這時候我們發現 127.0.0.1.8080(被動關閉的一端)收到FIN的包之後,回了ack但是並沒有傳送FIN包告訴主動關閉的一端可以關閉了,然後通過netstat看到狀態後發現,被動關閉的一端處於 close wait的狀態:

tcp        0      0 127.0.0.1:8080          127.0.0.1:47153         CLOSE_WAIT  39634/server

主動關閉的一端處於FIN_WAIT2的狀態

tcp        0      0 127.0.0.1:47153         127.0.0.1:8080          FIN_WAIT2   - 

close wait的狀態代表,收到了對方的關閉請求,自己這邊沒有主動發FIN包的導致的。

time wait的狀態

我們把上述程式碼 defer conn.Close() 註釋掉,按上述流程抓到tcpdump的包為:

02:25:50.976745 IP 127.0.0.1.47161 > 127.0.0.1.8080: Flags [.], ack 1, win 342, options [nop,nop,TS val 52279249 ecr 52279249], length 0
02:25:54.424235 IP 127.0.0.1.47161 > 127.0.0.1.8080: Flags [F.], seq 1, ack 1, win 342, options [nop,nop,TS val 52282697 ecr 52279249], length 0
02:25:54.424466 IP 127.0.0.1.8080 > 127.0.0.1.47161: Flags [F.], seq 1, ack 2, win 342, options [nop,nop,TS val 52282697 ecr 52282697], length 0
02:25:54.424476 IP 127.0.0.1.47161 > 127.0.0.1.8080: Flags [.], ack 2, win 342, options [nop,nop,TS val 52282697 ecr 52282697], length 0

發現資料又完整的四次揮手的狀態,此時使用netstat 看下狀態發現發起關閉端處於time wait的狀態

tcp        0      0 127.0.0.1:47161         127.0.0.1:8080          TIME_WAIT   - 

這個time wait的狀態出現在發起關閉的一端,是為了防止對方可能因為未收到最後發出的ack包而重發fin,主動發起的一端好進行重發ack,而time wait的持續時長為2MSL。

為什麼需要四次揮手

這是由tcp的半關閉造成的,既然一個tcp連線時全雙工的(即資料在兩個方向上能同時傳遞),因此每個方向都必須單獨地進行關閉揮手過程中(被動關閉的一方的 fin跟ack是可以放到一起同時傳送的,也就是四次揮手流程中的第二次跟第三次的資料包可以合併為一個資料包)。需要三次握手同理。

TCP三次握手與四次揮手流程圖:

在這裡插入圖片描述

兩端同時開啟

兩個應用程式同時彼此執行主動開啟的情況是有可能的,儘管發生的可能性極小。例如,主機A中的一個應用程式使用本地埠7777,並於主機B的埠8888執行主動開啟。主機B中的應用程式則使用本地埠8888,並與主機A的埠7777執行主動開啟。TCP特意設計為了可以處理同時開啟,對於同時開啟它僅建立一條連線而不是兩條連線。連線流程圖如下:
在這裡插入圖片描述

兩端同時關閉

兩端同時關閉的流程圖如下:
在這裡插入圖片描述

相關文章