tft每日頭條

 > 生活

 > tcp的基礎知識

tcp的基礎知識

生活 更新时间:2024-12-21 19:09:28

面試官:請解釋一下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 <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <arpa/inet.h> #include <strings.h> #include <string.h> #include <iostream> #include <fcntl.h> #include <unistd.h> 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 <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdlib.h> #include <arpa/inet.h> #include <strings.h> #include <string.h> #include <iostream> 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 << "connect error.\n"; exit(-1); } char buffer[1024]; while (true) { memset(buffer, 0, 1024); std::cin >> buffer; ret = send(sock, buffer, strlen(buffer), 0); if (ret == -1) { std::cerr << "send error.\n"; exit(-1); } else if (ret == 0) { std::cerr << "peer closed.\n"; exit(-1); } } return 0; }

編譯server和client

$ g server.cpp -o server $ g client.cpp -o client

生成可執行文件server和client。

3. tcp數據傳輸過程監測

首先我們運行tcpdump

$ sudo tcpdump -i lo port 8099 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes

然後再開一個命令行窗口運行server

$ ./server

我們這時候可以觀察tcpdump命令沒有任何輸出,然後運行client

$ ./client

這時候tcpdump有如下輸出,讀者在自己的主機上運行的結果有些參數是不一樣的,但是整個過程一樣

14:27:08.099236 IP localhost.49624 > localhost.8099: Flags [S], seq 1562745839, win 65495, options [mss 65495,sackOK,TS val 212202094 ecr 0,nop,wscale 7], length 0 14: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 0 14: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的基礎知識(你好TCP重新認識TCP)1

要開啟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 << "connect error.\n"; exit(-1); } */ // 發送報文改成 char buffer[1024]; memset(buffer, 0, 1024); std::cin >> 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 0 15: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 0 15: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 5 15: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 5 16: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 0 16: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 0 20: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 0 20: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,被動斷開方狀态遷移過程不變,如下圖:

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

我們繼續做實驗來驗證,我們修改服務端的代碼,在關閉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 << std::endl; }

tcpdump會得到類似以下的結果:

19:51:10.123413 IP localhost.45046 > localhost.8099: Flags [F.], seq 6, ack 1, win 512, options [nop,nop,TS val 4005024437 ecr 4005020558], length 0 19:51:10.173607 IP localhost.8099 > localhost.45046: Flags [.], ack 7, win 512, options [nop,nop,TS val 4005024487 ecr 4005024437], length 0 19: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 0 19: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 << std::endl; }

我們看看tcpdump的結果:

20:18:06.959692 IP localhost.45128 > localhost.8099: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 4006641273 ecr 4006640344], length 0 20:18:06.959760 IP localhost.8099 > localhost.45128: Flags [.], ack 2, win 512, options [nop,nop,TS val 4006641273 ecr 4006641273], length 0 20: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 0 20: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狀态變更圖

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

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

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

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved