面試官:請解釋一下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 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,被動斷開方狀态遷移過程不變,如下圖:
我們繼續做實驗來驗證,我們修改服務端的代碼,在關閉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狀态變更圖
這是一張經典圖片,可以結合tcpdump工具,具體實驗一下。
文章有不足之處還請指正。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!