tcp的基础知识(你好TCP重新认识TCP)
面试官:请解释一下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工具,具体实验一下。
文章有不足之处还请指正。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com