TCP需要三次握手和四次挥手的原因解析(包能理解版!)

相信对于下述图表的内容,相信大家都已经背的滚瓜烂熟了。但是当我背完之后,其实我对他具体某一部分我其实并不理解,例如为什么需要三次握手,两次不行吗,类似的这种问题一直困扰着我,今天花费了一定的时间,也算是终于对这个部分有自己的理解,故写下本博客来记录学习,并且希望对大家理解有所帮助!好了,话不多说,我们来发车!

TCP建立连接:三次握手

三次握手时序图

为什么需要三次握手

防止超时到来的连接错误开启连接

这句话是什么意思呢,下面我给大家举个例子:

假如是二次握手就可以建立连接:

如图,当客户端向服务端请求服务,那么会首先发送第一条TCP连接请求(图中的序号1表明的请求),但是此时由于网络拥塞,导致序号1这条请求没有在超时时间内接收到TCP服务器的响应SYN-ACK报文,那么会超时重传这一条请求报文,此时发送第二条TCP连接请求(图中序号2表明的请求),网络好转,TCP服务器接受到了这条报文,于是发送SYN-ACK报文给客户端,并且开启连接状态(因为是二次握手所以此时已经开启了连接状态)。客户端收到了服务端的SYN-ACK报文后,也开始传输数据了。然后也是传输数据,到最后的释放连接。此时本次连接也完全结束了。就在大家以为万事平安的时候,客户端发送的第一条TCP连接请求,此时偷家啦!!!他其实一直没有离开,只是晚到了而已!正义也许会迟到,但是绝对不会忘记砸到你的头顶!(bushi...)那么此时服务端接受到了这条消息,他就以为客户端又想请求第二次的服务了,所以服务端会发送SYN-ACK报文给客户端,并且开启连接的状态,等待客户端发送数据,但是此时客户端其实是处于了关机的状态了,根本不会理会你,所以,就会白白浪费了服务端的资源,而且服务端也不敢释放连接,认为客户端在忙其他事情还没回应我,等了很久发送探测报文才知道服务端挂了。(好不容易心动一次,你让我输的这么彻底!)

两次握手,延迟报文到达让服务端处于干等状态

那么三次握手就可以很好解决这个问题啦。

同样是上面的情况,如果客户端发送的第一条TCP连接请求,是刚刚说的在已经结束第一次连接的时候到达的,那么服务端接收到了这条消息,同样的是服务端会发送SYN-ACK报文给客户端,不同的是此时服务端进入的是一种名为半连接的状态,他需要等到客户端返回第三次握手的ACK报文段才会完全和客户端建立通信,进入连接状态。那么在三次握手这种情况下,服务端会一直等待客户端返回ACK报文,可是客户端已经关机了,无法返回,那么客户端会在超过重传时间后,重传第二次的SYN-ACK报文给这个客户端......不停地重传,直到到达自己的最大重传次数,发现此时客户端还不回应ACK报文,那么就会自己关闭了。

所以两次握手和三次握手有一个什么区别呢:两次握手是直接进入了连接状态、而三次握手则是进入半连接状态,只有等到了客户端回应ACK报文才会进入连接状态,否则就会自己关闭连接,不会一直等客户端。

讲完三次握手,那肯定也得来聊聊四次挥手,一口气干完!

TCP释放连接:四次挥手

也是和上面三次握手一样,先给大家奉上一张四次挥手完整示意图~

四次挥手示意图

在第四次握手中,我觉得大家最困惑的是为什么需要第四次挥手,以及为什么需要等待2MSL的时间,虽然网络上已经有很多的结论,都说的很对,可是自己就完全不懂为什么。

首先我们要明确三点:

1:MSL是什么

MSL通俗一点来说,就是这个报文在整个网络中最大存活的时间,形象说就是地球上距离最远的两个点,你以最最最慢的速度去从一点到另一点的时间。所以说,1个MSL的时间内某一个数据报是一定可以从源地址到目标地址的,只是说这个数据报是否在传输过程中,出现某一位变了,导致这个数据报无效了而已。MSL怎么算的,说实话我也不懂,但我们明白这个概念就知道了。

2:服务端发送FIN-ACK报文的时候,其实是已经把所有服务端的数据已经完全传输给了客户端,甚至来说,如果服务端和客户端只通信一次的话,直接就可以到此结束了。完全不需要第四次挥手和2MSL的等待时间了。因为服务端发送FIN-ACK报文的前提,就是这个客户端已经完全接收了本次连接所需的数据。

2:TCP的三次握手和四次挥手中,单纯的ACK报文是不会发生超时重传的,只有对方的超时重传报文到达对方才会重传ACK报文,例如这里的四次挥手中客户端最后一次发送的ACK报文,他是不会主动超时重传的,只有服务端太长时间没有接收到来自客户端的ACK报文,导致服务端超时重传FIN-ACK报文,进而让客户端接收到超时重传的FIN-ACK报文,再重传ACK报文。

那么接下来我们来聊聊这两个方面。

为什么客户端需要等待2MSL才能完全关闭连接

        首先说结论:防止本次连接的报文影响下一次连接过程中的数据交换,把本次连接的所有FIN-ACK捕获。

        假如客户端不等待2MSL,接收到了服务端的SYN-ACK报文,直接发送第四次挥手的ACK报文,然后自己关机。那么可能会导致以下结果:

        服务端其实已经传了几次SYN-ACK报文了,但是客户端只是接受到了某一次,就发出一次ACK报文给服务端然后立刻关闭释放连接。有朋友又会说如果此次ACK报文没有被服务端接收到,丢失了,那么会不会导致服务端不能释放连接?其实不然,服务端重传一定次数后,就算没有收到客户端的ACK报文,也会自动释放连接。其实最最重要的是,残留在网络中的SYN-ACK报文,如果很短时间内,该客户端又和该服务端建立了连接,然后很快进入了第二次挥手和第三次挥手中的服务端单向给客户端发送数据的阶段,如果此时上次残留的FIN-ACK报文此时被客户端接收到了,那么就会导致客户端提前释放了本次连接,导致客户端无法接收本次连接剩下的数据。

        而2MSL等待时间则完美解决这个问题。因为服务端会在自己发送了第一个FIN-ACK报文后,超时重传后,会重复发送第二个FIN-ACK报文....依次类推,最坏的情况,如果一个1个MSL时间内都还没接收到客户端的ACK报文,就会服务端自己释放连接了,服务端也保证1个MSL左右把这些重传报文全都发送出去了。那么客户端接收到某一个FIN-ACK之后,立刻发送ACK报文,并且立刻进入2MSL的等待时间,而服务端发送FIN-ACK报文一定能在2MSL内全部被捕获。最最最极限就是服务端在客户端进入2MSL等待时间内的1MSL后,发送最后一个FIN-SYN,并且花费了1MSL才到达客户端,那么也会被客户端在本次连接内捕获。(其实在这个极限情况下,客户端会不会发送ACK我不是很清楚,但是残留在网络的ACK不会影响下次连接,假如这个ACK这么巧seq ack都是符合这个下一次连接的,并且此时也处于服务端发送了FIN-ACK报文等待客户端ACK报文这个阶段,导致服务端提前关闭。让客户端可能会收不到服务端的FIN-ACK报文,但是客户端也会超时自己关闭呀,例如什么探测报文...看一下本次连接是否还存活之类的(这个时间肯定比2MSL更长))

从客户端发出的第一个ACK报文后,就开始计时等待2MSL时间。下面有例程代码,让客户端绑定一个端口来访问服务端,即使正常完成四次挥手释放连接,也不能立刻再向该服务端请求服务。

为什么需要四次挥手

在我看来,第四次挥手的目的主要是节约服务端和客户端资源。至于为什么,那么我们依然按照三次握手的思维:反证法来说明。

假如是三次挥手

在三次挥手中,由客户端发起FIN报文请示关闭,那么服务端接收到这个报文就会响应发出ACK报文,当客户端接收到ACK报文时,就会进入FIN-WAIT2状态。此时服务端要尽快传输剩下还未发送的报文,这些数据传输完成后,由服务端主动发送FIN-ACK报文给客户端表示发送完成可以关闭连接。咋一听好像没什么,那么我来举例说明一下:

假如释放连接是三次挥手,并且FIN-ACK不发生重传,只会发生一次:

        那么我们就可以直接让服务端发送了FIN-ACK,直接自己关闭连接了。此时客户端就会有两种情况,第一种是很好运,接收到了这个报文然后自己也释放连接,这样没有问题。但是如果客户端没有接收到这个FIN-ACK报文,就会一直等待,发送探测报文检测是否连接还存活,此时客户端就会挂机太久浪费资源。

假如释放连接是三次挥手,并且FIN-ACK会发生N次:

        由于没有第四次挥手,服务端不能判断客户端是否接收到了FIN-ACK报文,所以固定一定时间内发送一个FIN-ACK报文。那么如果客户端已经接收到了第一个FIN-ACK报文就关闭了,那就太浪费服务端的资源了。

所以需要四次挥手:

        四次挥手可以保证效率是最高的,服务端发送第一个FIN-ACK可能就立刻让客户端捕获了,然后客户端发送ACK让服务端释放连接,自己等待2MSL后再释放连接。所以说,四次挥手和2MSL等待时间是不可缺少的。

例程代码

服务端:

#include <stdio.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>


#define handle_error(cmd,result) \
    if (result<0) \
    {           \
        perror(cmd); \
        return -1;   \
    }
    

void *read_from_client_then_write(void * args)
{
    int client_fd = *(int *)args;
    ssize_t count=0,send_count=0;
    char * read_buf = NULL,*write_buf = NULL;
    read_buf = (char*)malloc(sizeof(char)*100);
    write_buf = (char*)malloc(sizeof(char)*100);
    while (count = recv(client_fd,read_buf,100,0))
    {
        if (count<0)
        {
            perror("recv");
        }
        printf("recv message is: %s\n",read_buf);
        strcpy(write_buf,"received!\n");
        send_count = send(client_fd,write_buf,100,0);
        if (send_count<0)
        {
            perror("send");
        }
        
    }
    printf("the client release!\n");
    shutdown(client_fd,SHUT_WR);
    close(client_fd);
    free(read_buf);
    free(write_buf);
    return NULL;
}


int main(int argc, char const *argv[])
{
    int sockfd,temp_result;
    struct sockaddr_in server_addr,client_addr;
    memset(&server_addr,0,sizeof(server_addr));
    memset(&client_addr,0,sizeof(client_addr));

    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(7777);
    inet_pton(AF_INET,"0.0.0.0",&server_addr.sin_addr);

    sockfd = socket(AF_INET,SOCK_STREAM,0);
    handle_error("socket",sockfd);

    temp_result = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
    handle_error("bind",temp_result);

    temp_result = listen(sockfd,1024);
    handle_error("listen",temp_result);

    socklen_t clientaddr_len;

    while (1)
    {
        int client_fd = accept(sockfd,(struct sockaddr*)&client_addr,&clientaddr_len);
        handle_error("accept",client_fd);
        
        printf("connect with the ip : %s ,port: %d\n",inet_ntoa(client_addr.sin_addr),\
        ntohs(client_addr.sin_port));

        pthread_t t_index;
        pthread_create(&t_index,NULL,read_from_client_then_write,(void*)&client_fd);

        pthread_detach(t_index);
    }
    close(sockfd);
    return 0;
}

客户端:

#include <stdio.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>


#define handle_error(cmd,result) \
    if (result<0) \
    {           \
        perror(cmd); \
        return -1;   \
    }

void* read_from_server(void * args)
{
    int sockfd = *(int *)args;
    char * read_buf = (char*)malloc(sizeof(char)*100);
    ssize_t count  = 0;
    while (count = recv(sockfd,read_buf,100,0))
    {
        fputs(read_buf,stdout);
    }
    printf("disconnect!\n");
    free(read_buf);
    return NULL;
}

void* write_to_server(void * args)
{
    int sockfd = *(int*)args;
    char * write_buf = (char*)malloc(sizeof(char)*100);
    
    while (fgets(write_buf,100,stdin)!=NULL)
    {
        send(sockfd,write_buf,100,0);
        
    }
    // shutdown 服务端才会知道你关闭了,或者整个程序ctrl c ,不然客户端是不会知道你关闭了,那么就会一直等,不释放资源
    shutdown(sockfd,SHUT_WR);
    // close(sockfd);
    printf("cmd close\n");
    free(write_buf);
    return NULL;
}


int main(int argc, char const *argv[])
{
    int sockfd,temp_result;
    struct sockaddr_in server_addr,my_addr;
    memset(&server_addr,0,sizeof(server_addr));
    memset(&my_addr,0,sizeof(my_addr));
    if (strcmp(argv[1],"ser1")==0)
    {
        server_addr.sin_port = htons(7777);
    }
    else if (strcmp(argv[1],"ser2")==0)
    {
        server_addr.sin_port = htons(8888);
    }
    else
    {
        printf("error!\n");
        return -1;
    }
    server_addr.sin_family = AF_INET;
    inet_pton(AF_INET,"127.0.0.1",&server_addr.sin_addr);

    //强制让客户端绑定某一个端口
    my_addr.sin_port = htons(6666);
    my_addr.sin_family = AF_INET;
    inet_pton(AF_INET,"192.168.5.196",&my_addr.sin_addr);

    sockfd = socket(AF_INET,SOCK_STREAM,0);
    handle_error("socket",sockfd);

    temp_result = bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
    handle_error("bind",temp_result);

    temp_result = connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
    handle_error("connect",temp_result);

    pthread_t t_read,t_write;
    pthread_create(&t_read,NULL,read_from_server,(void*)&sockfd);
    pthread_create(&t_write,NULL,write_to_server,(void*)&sockfd);

    pthread_join(t_read,NULL);
    pthread_join(t_write,NULL);
    printf("close\n");
    // close(sockfd);
    return 0;
}
短时间同一请求两次服务,客户端报错被占用

解决方案:

客户端不需要手动bind,操作系统会为我们bind空闲端口。

参考链接:

https://zhuanlan.zhihu.com/p/398890723

计算机网络微课堂(有字幕无背景音乐版)_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值