| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 网络协议 -> 灵魂拷问:TCP 四次挥手,可以变成三次吗? -> 正文阅读 |
|
[网络协议]灵魂拷问:TCP 四次挥手,可以变成三次吗? |
上周有位读者面试时,被问到:TCP 四次挥手中,能不能把第二次的 ACK 报文, 放到第三次 FIN 报文一起发送? ![]() 虽然我们在学习 TCP 挥手时,学到的是需要四次来完成 TCP 挥手,但是在一些情况下, TCP 四次挥手是可以变成 TCP 三次挥手的。 ![]() 而且在用 wireshark 工具抓包的时候,我们也会常看到 TCP 挥手过程是三次,而不是四次,如下图: ![]() 先来回答为什么 RFC 文档里定义 TCP 挥手过程是要四次? 再来回答什么情况下,什么情况会出现三次挥手? 为什么 TCP 挥手需要四次?TCP 四次挥手的过程如下: ![]() 具体过程:
你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。 为什么 TCP 挥手需要四次呢?服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序:
从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。
不一定。 如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。 粗暴关闭 vs 优雅关闭前面介绍 TCP 四次挥手的时候,并没有详细介绍关闭连接的函数,其实关闭的连接的函数有两种函数:
如果客户端是用 close 函数来关闭连接,那么在 TCP 四次挥手过程中,如果收到了服务端发送的数据,由于客户端已经不再具有发送和接收数据的能力,所以客户端的内核会回 RST 报文给服务端,然后内核会释放连接,这时就不会经历完成的 TCP 四次挥手,所以我们常说,调用 close 是粗暴的关闭。 ![]() 当服务端收到 RST 后,内核就会释放连接,当服务端应用程序再次发起读操作或者写操作时,就能感知到连接已经被释放了:
相对的,shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向,所以即使在 TCP 四次挥手过程中,如果收到了服务端发送的数据,客户端也是可以正常读取到该数据的,然后就会经历完整的 TCP 四次挥手,所以我们常说,调用 shutdown 是优雅的关闭。 ![]() 但是注意,shutdown 函数也可以指定「只关闭读取方向,而不关闭发送方向」,但是这时候内核是不会发送 FIN 报文的,因为发送 FIN 报文是意味着我方将不再发送任何数据,而 shutdown 如果指定「不关闭发送方向」,就意味着 socket 还有发送数据的能力,所以内核就不会发送 FIN。 什么情况会出现三次挥手?当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。 ![]() 然后因为 TCP 延迟确认机制是默认开启的,所以导致我们抓包时,看见三次挥手的次数比四次挥手还多。
当发送没有携带数据的 ACK,它的网络效率也是很低的,因为它也有 40 个字节的 IP 头 和 TCP 头,但却没有携带数据报文。 为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认。 TCP 延迟确认的策略:
![]() 延迟等待的时间是在 Linux 内核中定义的,如下图: 关键就需要 HZ 这个数值大小,HZ 是跟系统的时钟频率有关,每个操作系统都不一样,在我的 Linux 系统中 HZ 大小是 1000,如下图: ![]() 知道了 HZ 的大小,那么就可以算出:
如果要关闭 TCP 延迟确认机制,可以在 Socket 设置里启用 TCP_QUICKACK,启用 TCP_QUICKACK,就相当于关闭 TCP 延迟确认机制。 // 1 表示开启 TCP_QUICKACK,即关闭 TCP 延迟确认机制int value = 1; setsockopt(socketfd, IPPROTO_TCP, TCP_QUICKACK, ( char*)& value, sizeof( int)); 实验验证实验一接下来,来给大家做个实验,验证这个结论:
服务端的代码如下,做的事情很简单,就读取数据,然后当 read 返回 0 的时候,就马上调用 close 关闭连接。因为 TCP 延迟确认机制是默认开启的,所以不需要特殊设置。 #include <stdlib.h>#include <stdio.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <netinet/tcp.h> #define MAXLINE 1024 int main(int argc, char *argv[]) { // 1. 创建一个监听 socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd < 0) { fprintf( stderr, "socket error : %s\n", strerror(errno)); return -1; } // 2. 初始化服务器地址和端口 struct sockaddr_in server_addr; bzero(&server_addr, sizeof(struct sockaddr_in)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons( 8888); // 3. 绑定地址+端口 if(bind(listenfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) < 0) { fprintf( stderr, "bind error:%s\n", strerror(errno)); return -1; } printf( "begin listen....\n"); // 4. 开始监听 if(listen(listenfd, 128)) { fprintf( stderr, "listen error:%s\n\a", strerror(errno)); exit( 1); } // 5. 获取已连接的socket struct sockaddr_in client_addr; socklen_t client_addrlen = sizeof(client_addr); int clientfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_addrlen); if(clientfd < 0) { fprintf( stderr, "accept error:%s\n\a", strerror(errno)); exit( 1); } printf( "accept success\n"); char message[MAXLINE] = { 0}; while( 1) { //6. 读取客户端发送的数据 int n = read(clientfd, message, MAXLINE); if(n < 0) { // 读取错误 fprintf( stderr, "read error:%s\n\a", strerror(errno)); break; } else if(n == 0) { // 返回 0 ,代表读到 FIN 报文 fprintf( stderr, "client closed \n"); close(clientfd); // 没有数据要发送,立马关闭连接 break; } message[n] = 0; printf( "received %d bytes: %s\n", n, message); } close(listenfd); return 0; } 客户端代码如下,做的事情也很简单,与服务端连接成功后,就发送数据给服务端,然后睡眠一秒后,就调用 close 关闭连接,所以客户端是主动关闭方: #include <stdlib.h>#include <stdio.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> int main(int argc, char *argv[]) { // 1. 创建一个监听 socket int connectfd = socket(AF_INET, SOCK_STREAM, 0); if(connectfd < 0) { fprintf( stderr, "socket error : %s\n", strerror(errno)); return -1; } // 2. 初始化服务器地址和端口 struct sockaddr_in server_addr; bzero(&server_addr, sizeof(struct sockaddr_in)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr( "127.0.0.1"); server_addr.sin_port = htons( 8888); // 3. 连接服务器 if(connect(connectfd, (struct sockaddr *)(&server_addr), sizeof(server_addr)) < 0) { fprintf( stderr, "connect error:%s\n", strerror(errno)); return -1; } printf( "connect success\n"); char sendline[ 64] = "hello, i am xiaolin"; //4. 发送数据 int ret = send(connectfd, sendline, strlen(sendline), 0); if(ret != strlen(sendline)) { fprintf( stderr, "send data error:%s\n", strerror(errno)); return -1; } printf( "already send %d bytes\n", ret); sleep( 1); //5. 关闭连接 close(connectfd); return 0; } 编译服务端和客户端的代码: ![]() 先启用服务端: ![]() 然后用 tcpdump 工具开始抓包,命令如下: tcpdump -i lo tcp and port 8888 -s0 -w /home/tcp_close.pcap然后启用客户端,可以看到,与服务端连接成功后,发完数据就退出了。 ![]() 此时,服务端的输出: ![]() 接下来,我们来看看抓包的结果。 ![]() 可以看到,TCP 挥手次数是 3 次。 所以,下面这个结论是没问题的。
实验二我们再做一次实验,来看看关闭 TCP 延迟确认机制,会出现四次挥手吗? 客户端代码保持不变,服务端代码需要增加一点东西。 在上面服务端代码中,增加了打开了 TCP_QUICKACK (快速应答)机制的代码,如下: ![]() 编译好服务端代码后,就开始运行服务端和客户端的代码,同时用 tcpdump 进行抓包。 抓包的结果如下,可以看到是四次挥手。 所以,当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」,同时「关闭了 TCP 延迟确认机制」,那么就会是四次挥手。
我也是多次实验才发现,在 bind 之前设置 TCP_QUICKACK 是不生效的,只有在 read 返回 0 的时候,设置 TCP_QUICKACK 才会出现四次挥手。 网上查了下资料说,设置 TCP_QUICKACK 并不是永久的,所以每次读取数据的时候,如果想要立刻回 ACK,那就得在每次读取数据之后,重新设置 TCP_QUICKACK。 而我这里的实验,目的是为了当收到客户端的 FIN 报文(第一次挥手)后,立马回 ACK 报文,所以就在 read 返回 0 的时候,设置 TCP_QUICKACK。 当然,实际应用中,没人会在我这个位置设置 TCP_QUICKACK,因为操作系统都通过 TCP 延迟确认机制帮我们把四次挥手优化成了三次挥手了,这本来就是一件好事呀。 总结当被动关闭方在 TCP 挥手过程中,如果「没有数据要发送」,同时「没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。?? 所以,出现三次挥手现象,是因为 TCP 延迟确认机制导致的。 |
|
网络协议 最新文章 |
使用Easyswoole 搭建简单的Websoket服务 |
常见的数据通信方式有哪些? |
Openssl 1024bit RSA算法---公私钥获取和处 |
HTTPS协议的密钥交换流程 |
《小白WEB安全入门》03. 漏洞篇 |
HttpRunner4.x 安装与使用 |
2021-07-04 |
手写RPC学习笔记 |
K8S高可用版本部署 |
mySQL计算IP地址范围 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年2日历 | -2025/2/24 15:29:10- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |