新四季網

tcp的基礎知識(你好TCP重新認識TCP)

2023-07-24 09:21:52

面試官:請解釋一下TCP建立連接的兩次握手

面試者:......(不是三次嗎?)

面試官:請解釋一下TCP斷開連接的三次揮手

面試者:......(不是四次嗎?)

注意:以下都是在Linux環境測試,內核*5.10.16.3-microsoft-standard-WSL2*。

1. tcpdump命令的使用

在解釋TCP的建立連接過程和斷開連接過程之前,介紹一下網絡監測利器tcpdump;但是這裡不展開對tcpdump的使用,主要用最簡單的參數來獲取對我們下文解釋有需要的數據。

$ sudo tcpdump -i lo port 8090 # tcpdump需要root權限# -S 完整顯示seq# -i 選擇需要監聽的interface,這裡我們用lo(環回網口),本地測試# 整個命令的作用就是監聽環回網口上8090埠的網絡數據# 以下是我們獲取到的一條數據11:16:21.261142 IP localhost.49566 > localhost.8099: Flags [S], seq 81901745, win 65495, options [mss 65495,sackOK,TS val 200755255 ecr 0,nop,wscale 7], length 0

我們以此來解釋這條數據:

11:16:21.261142,表示這條數據收到的時間戳,默認是精確到微秒。

IP,表示這是一個IPv4的包。

localhost.49566 > localhost.8099:源端地址和埠 > 目的端地址和埠。

Flags [S],這是一個sync包,其它的標誌有S (SYN), F (FIN), P (PUSH), R (RST), U (URG), W(ECN CWR), E (ECN-Echo) or '.' (ACK), or `none' 沒有標誌設置。

seq 81901745,發送端的序號是81901745。

win 65495:發送端的滑動窗口大小是65495。

options [mss 65495,sackOK,TS val 200755255 ecr 0,nop,wscale 7]:一些TCP選項。

length 0:有效載荷為0。

2. 簡單的服務端和客戶端程序

程序為了說明鏈路建立和斷開的過程,為了簡單起見,沒有複雜的網絡變成過程。為了篇幅,有些代碼寫在了一行。

/** * server.cpp */#include #include #include #include #include #include #include #include #include #include int main(int argc, char* argv[]) { int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr; bzero(&serv_addr, sizeof serv_addr); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(8099); serv_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); int reuseaddr = 1; setsockopt(sock, SOCK_STREAM, SO_REUSEADDR, &reuseaddr, sizeof reuseaddr); // 為了埠復用,不影響tcp過程 int ret = bind(sock, (struct sockaddr *)&serv_addr, sizeof serv_addr); if (ret == -1) { std::cerr << "bind error.\n"; exit(-1); } ret = listen(sock, 1024); // backlog : 全連接隊列大小 if (ret == -1) { std::cerr << "listen error.\n"; exit(-1); } struct sockaddr_in peer_addr; int len = sizeof peer_addr; char buffer[1024]; int acc_socket = accept(sock, (struct sockaddr *)&peer_addr, (socklen_t *)&len); if (acc_socket == -1) { std::cerr << "accept error.\n"; exit(-1); } std::cout << "accepted: " << inet_ntoa(peer_addr.sin_addr) << ", port: " << ntohs(peer_addr.sin_port) << std::endl; while (true) { memset(buffer, 0, 1024); ret = recv(acc_socket, buffer, 1024, 0); if (ret == -1) { std::cerr << "recv error.\n"; close(acc_socket); exit(-1); } else if (ret == 0) { close(acc_socket); std::cout << "end of file.\n"; exit(0); } std::cout << buffer << std::endl; } return 0;}

/** * client.cpp */#include #include #include #include #include #include #include #include int main(int argc, char* argv[]) { int sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_addr; bzero(&serv_addr, sizeof serv_addr); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(8099); serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect(sock, (struct sockaddr *)&serv_addr, sizeof serv_addr); if (ret == -1) { std::cerr <> buffer; ret = send(sock, buffer, strlen(buffer), 0); if (ret == -1) { std::cerr << "send error.\n"; exit(-1); } else if (ret == 0) { std::cerr < localhost.8099: Flags [S], seq 1562745839, win 65495, options [mss 65495,sackOK,TS val 212202094 ecr 0,nop,wscale 7], length 014:27:08.099256 IP localhost.8099 > localhost.49624: Flags [S.], seq 1375681665, ack 1562745840, win 65483, options [mss 65495,sackOK,TS val 212202094 ecr 212202094,nop,wscale 7], length 014:27:08.099265 IP localhost.49624 > localhost.8099: Flags [.], ack 1, win 512, options [nop,nop,TS val 212202094 ecr 212202094], length 0

以上輸出表示 客戶端從localhost的49624埠發送了一個sync報文到服務端localhost的8099埠,報文序號是1562745839; 服務端發送了一個sync的ack到客戶端,報文序號是1375681665,應答序號是1562745840(表示服務端下一個可接收的序號是1562745840); 客戶端發送了一個ack,表示客戶端收到了服務端的應答,可以接收服務端的下個序號是1;

注意上面的最後一條ack,序號1,這是tcpdump簡化了序號,為了方便閱讀,如果我們需要顯示完整的需要,只需要在tcpdump的命令行裡加上參數-S即sudo tcpdump -S -i lo port 8099,那麼最後一次客戶端發送到服務端的ack就應該是這個樣子

14:27:08.099265 IP localhost.49624 > localhost.8099: Flags [.], ack 11375681666, win 512, options [nop,nop,TS val 212202094 ecr 212202094], length 0

到目前我們看到的還是TCP建立連接的三次握手過程,那我們的兩次握手過程呢?

4. 兩次握手主角TCP Fast Open

這就要請出另外一個主角,TCP Fast Open,這是谷歌的一個團隊提出的,他們覺得TCP的三次握手太耗時了,就提出了這麼一個方案,減少一次ack的時間,現在RFC 7413中有解釋。

以下圖片展示了三次握手和兩次握手的流程對比:

要開啟TCP Fast Open,Linux內核版本至少需要3.7。

使用命令行:

$ sysctl net.ipv4.tcp_fastopen # 查看當前的tcp_fastopen開啟狀態net.ipv4.tcp_fastopen = 1 # 當前系統默認為1# 0 關閉fast open# 1 作為客戶端時開啟# 2 作為服務端時開啟# 3 客戶端和服務端都開啟$ sudo sysctl -w net.ipv4.tcp_fastopen=3 # 客戶端和服務端都開啟net.ipv4.tcp_fastopen = 3

然後我們修改我們的server.cpp和client.cpp:

/** * server.cpp */// ......int reuseaddr = 1;setsockopt(sock, SOCK_STREAM, SO_REUSEADDR, &reuseaddr, sizeof reuseaddr);// 增加以下代碼, 注意要在listen之前設置int qlen = 5; //fast open 隊列setsockopt(sock, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));// ......

/** * client.cpp */// 在Linux內核版本4.11前,用sendto MSG_FASTOPEN標誌, 不需要再調用connect/* 注掉 int ret = connect(sock, (struct sockaddr *)&serv_addr, sizeof serv_addr); if (ret == -1) { std::cerr <> buffer;int ret = sendto(sock, buffer, strlen(buffer), MSG_FASTOPEN, (struct sockaddr *)&serv_addr, sizeof(serv_addr));// 在Linux內核版本4.11之後,系統提供了TCP_FASTOPEN_CONNECT選項int enable = 1;// connect前如下設置int ret = setsockopt(sock, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, &enable, sizeof(enable));// 跟平常一樣調用ret = connect(socket, saddr, saddr_len);

編譯之後啟動服務端和客戶端,並且客戶端發送hello給服務端。

我們看看這樣修改之後tcpdump的輸出結果:

15:39:47.509201 IP localhost.49664 > localhost.8099: Flags [S], seq 1993298214, win 65495, options [mss 65495,sackOK,TS val 216561503 ecr 0,nop,wscale 7,tfo cookiereq,nop,nop], length 015:39:47.509215 IP localhost.8099 > localhost.49664: Flags [S.], seq 1034872048, ack 1993298215, win 65483, options [mss 65495,sackOK,TS val 216561504 ecr 216561503,nop,wscale 7,tfo cookie 13bcbb0891552445,nop,nop], length 015:39:47.509228 IP localhost.49664 > localhost.8099: Flags [P.], seq 1993298215:1993298220, ack 1034872049, win 512, options [nop,nop,TS val 216561504 ecr 216561504], length 515:39:47.509255 IP localhost.8099 > localhost.49664: Flags [.], ack 1993298220, win 512, options [nop,nop,TS val 216561504 ecr 216561504], length 0

我們可以看到, 客戶端從localhost的49664埠發送了一個sync報文到服務端localhost的8099埠,報文序號是1993298214,並求情一個cookie; 服務端發送了一個sync的ack到客戶端,報文序號是1034872048,應答序號是1993298215(表示服務端下一個可接收的序號是1993298215); 客戶端發送了一個包,有效載荷長度5。 服務端發送ack給客戶端。

我們退出服務端和客戶端,再重新啟動服務端和客戶端,並且客戶端向服務端發送hello,繼續看tcpdump的輸出:

16:52:34.575420 IP localhost.49702 > localhost.8099: Flags [S], seq 3941954492:3941954497, win 65495, options [mss 65495,sackOK,TS val 220928570 ecr 0,nop,wscale 7,tfo cookie 13bcbb0891552445,nop,nop], length 516:52:34.575456 IP localhost.8099 > localhost.49702: Flags [S.], seq 1440641212, ack 3941954498, win 65483, options [mss 65495,sackOK,TS val 220928570 ecr 220928570,nop,wscale 7], length 016:52:34.575467 IP localhost.49702 > localhost.8099: Flags [.], ack 1, win 512, options [nop,nop,TS val 220928570 ecr 220928570], length 0

我們可以看到,這次客戶端向服務端發送SYN時同時帶了數據包和cookie,不用再做三次握手,節省了很多時間。

寫到這裡,基本上TCP兩次握手的問題已經差不多了。我們來看看另一個問題,TCP斷開連接時的三次揮手.

5. 三次揮手

細心的讀者在做實驗的時候應該已經發現了,我們從客戶端用ctrl c退出程序斷開連接的時候tcpdump會得到以下結果:

20:42:28.422854 IP localhost.44612 > localhost.8099: Flags [F.], seq 6, ack 1, win 512, options [nop,nop,TS val 3952421244 ecr 3952416809], length 020:42:28.422915 IP localhost.8099 > localhost.44612: Flags [F.], seq 1, ack 7, win 512, options [nop,nop,TS val 3952421244 ecr 3952421244], length 020:42:28.422934 IP localhost.44612 > localhost.8099: Flags [.], ack 2, win 512, options [nop,nop,TS val 3952421244 ecr 3952421244], length 0

這是tcp的延遲ack造成了我們看到的揮手報文只有三次,收到報文報文後不立即應答ack,當我們在程序裡面close socket的時候會發送FIN,這時候,FIN和ACK會作為一個包一起發送出去,只需要這個包的FIN和ACK標誌位都設置值就行了。

所以三次揮手只是第二步和第三步的報文合併了,主動斷開方的tcp狀態從FIN_WAIT1直接跳過FIN_WAIT2變成TIME_WAIT,被動斷開方狀態遷移過程不變,如下圖:

我們繼續做實驗來驗證,我們修改服務端的代碼,在關閉socket之前sleep一段時間

while (true) { memset(buffer, 0, 1024); ret = recv(acc_socket, buffer, 1024, 0); if (ret == -1) { std::cerr << "recv error.\n"; close(acc_socket); exit(-1); } else if (ret == 0) { std::this_thread::sleep_for(std::chrono::seconds(3)); // 增加這行,關閉socket之前先休眠3秒 close(acc_socket); std::cout << "end of file.\n"; exit(0); } std::cout << buffer < localhost.8099: Flags [F.], seq 6, ack 1, win 512, options [nop,nop,TS val 4005024437 ecr 4005020558], length 019:51:10.173607 IP localhost.8099 > localhost.45046: Flags [.], ack 7, win 512, options [nop,nop,TS val 4005024487 ecr 4005024437], length 019:51:13.123675 IP localhost.8099 > localhost.45046: Flags [F.], seq 1, ack 7, win 512, options [nop,nop,TS val 4005027437 ecr 4005024437], length 019:51:13.123694 IP localhost.45046 > localhost.8099: Flags [.], ack 2, win 512, options [nop,nop,TS val 4005027437 ecr 4005027437], length 0

我們看到這是四次揮手的過程,而且第一個FIN收到之後50ms左右才發出ACK,這就是延遲ACK等待的時間,不同的機器測出來數值不同,同一臺機器多次測試結果也不一定相同。

我們把延遲ACK關閉了來看看結果是什麼樣的,首先修改服務端的代碼:

int quickack = 1; while (true) { memset(buffer, 0, 1024); ret = recv(acc_socket, buffer, 1024, 0); // 關閉延遲ack setsockopt(acc_socket, IPPROTO_TCP, TCP_QUICKACK, &quickack, sizeof(quickack)); if (ret == -1) { std::cerr << "recv error.\n"; close(acc_socket); exit(-1); } else if (ret == 0) { close(acc_socket); std::cout << "end of file.\n"; exit(0); } std::cout << buffer < localhost.8099: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 4006641273 ecr 4006640344], length 020:18:06.959760 IP localhost.8099 > localhost.45128: Flags [.], ack 2, win 512, options [nop,nop,TS val 4006641273 ecr 4006641273], length 020:18:06.959789 IP localhost.8099 > localhost.45128: Flags [F.], seq 1, ack 2, win 512, options [nop,nop,TS val 4006641273 ecr 4006641273], length 020:18:06.959818 IP localhost.45128 > localhost.8099: Flags [.], ack 2, win 512, options [nop,nop,TS val 4006641273 ecr 4006641273], length 0

被動關閉端收到FIN後立馬發送了ACK,我們再close的時候就只發送了FIN。

到此我們的TCP斷開連接三次揮手過程也講完了。

6. 總結

TCP經過多年的發展,已經和最開始的實現有些改進,增加不少的奇技淫巧,感興趣的同學可以直接看源碼。不過現在的源碼是越來越複雜了,而且各個操作的實現有些細微的差異,考驗各位的功力了,附上一張經典圖片。

TCP狀態變更圖

這是一張經典圖片,可以結合tcpdump工具,具體實驗一下。

文章有不足之處還請指正。

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了