快盘下载:好资源、好软件、快快下载吧!

快盘排行|快盘最新

当前位置:首页软件教程电脑软件教程 → 为什么书上说是-TCP-四次挥手,但是实际会出现-TCP-三次挥手?

为什么书上说是-TCP-四次挥手,但是实际会出现-TCP-三次挥手?

时间:2022-09-21 10:26:14人气:作者:快盘下载我要评论

为什么书上说是-TCP-四次挥手,但是实际会出现-TCP-三次挥手?

虽然我们在学习 TCP 挥手时;学到的是需要四次来完成 TCP 挥手;但是在一些情况下; TCP 四次挥手是可以变成 TCP 三次挥手的。

而且在用 wireshark 工具抓包的时候;我们也会常看到 TCP 挥手过程是三次;而不是四次;如下图; 

三次挥手?

先来回答为什么 RFC 文档里定义 TCP 挥手过程是要四次?再来回答什么情况下;什么情况会出现三次挥手?

为什么 TCP 挥手需要四次?

TCP 四次挥手的过程如下;

 三次挥手?

具体过程;

客户端主动调用关闭连接的函数;于是就会发送 FIN 报文;这个 FIN 报文代表客户端不会再发送数据了;进入 FIN_WAIT_1 状态;服务端收到了 FIN 报文;然后马上回复一个 ACK 确认报文;此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候;TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中;服务端应用程序可以通过 read 调用来感知这个 FIN 包;这个 EOF 会被放在已排队等候的其他已接收的数据之后;所以必须要得继续 read 接收缓冲区已接收的数据;接着;当服务端在 read 数据的时候;最后自然就会读到 EOF;接着 read() 就会返回 0;这时服务端应用程序如果有数据要发送的话;就发完数据后才调用关闭连接的函数;如果服务端应用程序没有数据要发送的话;可以直接调用关闭连接的函数;这时服务端就会发一个 FIN 包;这个 FIN 报文代表服务端不会再发送数据了;之后处于 LAST_ACK 状态;客户端接收到服务端的 FIN 包;并发送 ACK 确认包给服务端;此时客户端将进入 TIME_WAIT 状态;服务端收到 ACK 确认包后;就进入了最后的 CLOSE 状态;客户端经过 2MSL 时间之后;也进入 CLOSE 状态;

你可以看到;每个方向都需要一个 FIN 和一个 ACK;因此通常被称为四次挥手。

为什么 TCP 挥手需要四次呢?

服务器收到客户端的 FIN 报文时;内核会马上回一个 ACK 应答报文;但是服务端应用程序可能还有数据要发送;所以并不能马上发送 FIN 报文;而是将发送 FIN 报文的控制权交给服务端应用程序;

如果服务端应用程序有数据要发送的话;就发完数据后;才调用关闭连接的函数;如果服务端应用程序没有数据要发送的话;可以直接调用关闭连接的函数;

从上面过程可知;是否要发送第三次挥手的控制权不在内核;而是在被动关闭方;上图的服务端;的应用程序;因为应用程序可能还有数据要发送;由应用程序决定什么时候调用关闭连接的函数;当调用了关闭连接的函数;内核就会发送 FIN 报文了;所以服务端的 ACK 和 FIN 一般都会分开发送。

FIN 报文一定得调用关闭连接的函数;才会发送吗?

不一定。如果进程退出了;不管是不是正常退出;还是异常退出;如进程崩溃;;内核都会发送 FIN 报文;与对方完成四次挥手。

粗暴关闭 vs 优雅关闭

前面介绍 TCP 四次挥手的时候;并没有详细介绍关闭连接的函数;其实关闭的连接的函数有两种函数;

close 函数;同时 socket 关闭发送方向和读取方向;也就是 socket 不再有发送和接收数据的能力。如果有多进程/多线程共享同一个 socket;如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1;并不会导致 socket 不可用;同时也不会发出 FIN 报文;其他进程还是可以正常读写该 socket;直到引用计数变为 0;才会发出 FIN 报文。shutdown 函数;可以指定 socket 只关闭发送方向而不关闭读取方向;也就是 socket 不再有发送数据的能力;但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socket;shutdown 则不管引用计数;直接使得该 socket 不可用;然后发出 FIN 报文;如果有别的进程企图使用该 socket;将会受到影响。

如果客户端是用 close 函数来关闭连接;那么在 TCP 四次挥手过程中;如果收到了服务端发送的数据;由于客户端已经不再具有发送和接收数据的能力;所以客户端的内核会回 RST 报文给服务端;然后内核会释放连接;这时就不会经历完成的 TCP 四次挥手;所以我们常说;调用 close 是粗暴的关闭。

 为什么书上说是

当服务端收到 RST 后;内核就会释放连接;当服务端应用程序再次发起读操作或者写操作时;就能感知到连接已经被释放了;

如果是读操作;则会返回 RST 的报错;也就是我们常见的Connection reset by peer。如果是写操作;那么程序会产生 SIGPIPE 信号;应用层代码可以捕获并处理信号;如果不处理;则默认情况下进程会终止;异常退出。

相对的;shutdown 函数因为可以指定只关闭发送方向而不关闭读取方向;所以即使在 TCP 四次挥手过程中;如果收到了服务端发送的数据;客户端也是可以正常读取到该数据的;然后就会经历完整的 TCP 四次挥手;所以我们常说;调用 shutdown 是优雅的关闭。

三次挥手?

但是注意;shutdown 函数也可以指定「只关闭读取方向;而不关闭发送方向」;但是这时候内核是不会发送 FIN 报文的;因为发送 FIN 报文是意味着我方将不再发送任何数据;而 shutdown 如果指定「不关闭发送方向」;就意味着 socket 还有发送数据的能力;所以内核就不会发送 FIN。

什么情况会出现三次挥手?

当被动关闭方;上图的服务端;在 TCP 挥手过程中;「没有数据要发送」并且「开启了 TCP 延迟确认机制」;那么第二和第三次挥手就会合并传输;这样就出现了三次挥手。

三次挥手?

然后因为 TCP 延迟确认机制是默认开启的;所以导致我们抓包时;看见三次挥手的次数比四次挥手还多。

什么是 TCP 延迟确认机制?

当发送没有携带数据的 ACK;它的网络效率也是很低的;因为它也有 40 个字节的 IP 头 和 TCP 头;但却没有携带数据报文。为了解决 ACK 传输效率低问题;所以就衍生出了 TCP 延迟确认。TCP 延迟确认的策略;

当有响应数据要发送时;ACK 会随着响应数据一起立刻发送给对方当没有响应数据要发送时;ACK 将会延迟一段时间;以等待是否有响应数据可以一起发送如果在延迟等待发送 ACK 期间;对方的第二个数据报文又到达了;这时就会立刻发送 ACK

三次挥手?

延迟等待的时间是在 Linux 内核中定义的;如下图; 

 四次挥手,但是实际会出现

 关键就需要 HZ 这个数值大小;HZ 是跟系统的时钟频率有关;每个操作系统都不一样;在我的 Linux 系统中 HZ 大小是 1000;如下图;

四次挥手,但是实际会出现 

知道了 HZ 的大小;那么就可以算出;

最大延迟确认时间是 200 ms ;1000/5;最短延迟确认时间是 40 ms ;1000/25;
怎么关闭 TCP 延迟确认机制?

如果要关闭 TCP 延迟确认机制;可以在 Socket 设置里启用 TCP_QUICKACK;启用 TCP_QUICKACK;就相当于关闭 TCP 延迟确认机制。

// 1 表示开启 TCP_QUICKACK;即关闭 TCP 延迟确认机制
int value = 1;
setsockopt(socketfd, IPPROTO_TCP, TCP_QUICKACK, (char*)& value, sizeof(int));

实验验证

实验一

接下来;来给大家做个实验;验证这个结论;

当被动关闭方;上图的服务端;在 TCP 挥手过程中;「 没有数据要发送」并且「开启了 TCP 延迟确认机制」;那么第二和第三次挥手就会合并传输;这样就出现了三次挥手。

服务端的代码如下;做的事情很简单;就读取数据;然后当 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
;, 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
;, strerror(errno));
        return -1;
    }

    printf(;begin listen....
;);

    // 4. 开始监听
    if(listen(listenfd, 128))
    {
        fprintf(stderr, ;listen error:%s
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
a;, strerror(errno));
        exit(1);
    }

    printf(;accept success
;);

    char message[MAXLINE] = {0};
    
    while(1) {
        //6. 读取客户端发送的数据
        int n = read(clientfd, message, MAXLINE);
        if(n < 0) { // 读取错误
            fprintf(stderr, ;read error:%s
a;, strerror(errno));
            break;
        } else if(n == 0) {  // 返回 0 ;代表读到 FIN 报文
            fprintf(stderr, ;client closed 
;);
            close(clientfd); // 没有数据要发送;立马关闭连接
            break;
        }

        message[n] = 0; 
        printf(;received %d bytes: %s
;, 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
;, 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
;, strerror(errno));
        return -1;
    }

    printf(;connect success
;);


    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
;, strerror(errno));
        return -1;
    }

    printf(;already send %d bytes
;, 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 延迟确认机制;默认会开启;」;那么第二和第三次挥手就会合并传输;这样就出现了三次挥手。

实验二

我们再做一次实验;来看看关闭 TCP 延迟确认机制;会出现四次挥手吗?客户端代码保持不变;服务端代码需要增加一点东西。在上面服务端代码中;增加了打开了 TCP_QUICKACK ;快速应答;机制的代码;如下;

四次挥手,但是实际会出现

 编译好服务端代码后;就开始运行服务端和客户端的代码;同时用 tcpdump 进行抓包。抓包的结果如下;可以看到是四次挥手。

 

所以;当被动关闭方;上图的服务端;在 TCP 挥手过程中;「没有数据要发送」;同时「关闭了 TCP 延迟确认机制」;那么就会是四次挥手。

设置 TCP_QUICKACK 的代码;为什么要放在 read 返回 0 之后?

我也是多次实验才发现;在 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 延迟确认机制导致的。完;

 

相关文章

  • Server SAN_Windows存储卷设备

    Server SAN_Windows存储卷设备,目前,实现云环境中数据的高效存储是云计算提供服务的基本要求。云计算和云存储已经成为提供信息和在线功能的首选方法。...
  • ssh登录linux

    ssh登录linux,上一篇提到用ssh登录centos好处很多,许多linux系统管理员都在用ssh。在windows下ssh客户端主要有puTTY、Xshell、secureCR......

网友评论

快盘下载暂未开通留言功能。

关于我们| 广告联络| 联系我们| 网站帮助| 免责声明| 软件发布

Copyright 2019-2029 【快快下载吧】 版权所有 快快下载吧 | 豫ICP备10006759号公安备案:41010502004165

声明: 快快下载吧上的所有软件和资料来源于互联网,仅供学习和研究使用,请测试后自行销毁,如有侵犯你版权的,请来信指出,本站将立即改正。