0
点赞
收藏
分享

微信扫一扫

TCP协议详解(2)(九千字长文详解)

TCP协议详解(2)

连接管理机制(三次握手和四次挥手)

QQ截图20231022215041_result

==三次握手==

image-20231022220713090

明白了三次握手之后!我们要谈一下TCP为什么要连接!——我们都说TCP面向连接所以可以保证可靠性!

那么为什么把连接建立好就可以保证可靠性呢?——因为TCP可以超时重传?确认应答?流量控制?

面向连接这件事本身是不能直接保证可靠性的!——面向连接本质就是交换几个报文创建一个结构体而已!

是间接保证可靠性的!——那么怎么间接保证可靠性呢?TCP是如何知道那个报文丢了?那个报文处于新建状态?连接状态?还是断开的状态?那些报文丢失还要重传,重传下一次的是多长?等等问题

==刚刚我们说的这一些特征,都是要维护在TCP的连接的结构体里面的!——正是因为有了三次握手的机制!所以给双方都形成了连接结构体的共识,这是因为有了连接结构体!才能更好的去完成超时重传!流量控制!确认应答等的数据基础!——三次握手是创建连接结构体的基础!所以三次握手间接保证了可靠性!==

==四次挥手==

为什么要四次挥手

image-20231023204107622

image-20231023203719967

image-20231023205836204

这就是四次挥手的基本的样子!——==四次挥手也有可能变成三次挥手!——例如中间ACK+FIN这两个报文可以合起来!==

TCP状态转换

四次挥手状态的变化

在四次挥手期间任何一方都可能会断开连接!客户端或者服务端都可能!

image-20231023214256725

==我们如何让服务端一直保持CLOSED_WAIT状态呢?——我们只要让服务器端不要进行close函数调用即可!客户端调用close服务器会被动的进行两次握手!但是服务器端不调用close我们就可以让其不对发起请求!那么服务器端就会一直处于CLOSE_WAIT状态==

//这个是让客户端主动断开的测试
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

namespace server
{
 enum
 {
     USAGE_ERR = 1,
     SOCKET_ERR,
     BIND_ERR,
     LISTEN_ERR,
     ACCEPT_ERR
 };
 const static int gbacklog = 5;
 const static uint16_t gport = 8080;
 using func_t = std::function<void(const int &, int &)>;

 class httpServer
 {
 public:
     httpServer(func_t func,const uint16_t &port = gport)
         : port_(port), listensock_(-1),func_(func)
     {
     }

     void initServer()
     {
         //1.首先就是要创建套接字
         //1.1创建一个套接字!
         listensock_ = socket(AF_INET,SOCK_STREAM,0);
         if(listensock_ == -1)
         {
             exit(SOCKET_ERR);
         }

         //1.2进行绑定!
         struct sockaddr_in peer;
         bzero(&peer,sizeof(peer));
         peer.sin_family = AF_INET;
         peer.sin_addr.s_addr = INADDR_ANY;
         peer.sin_port = htons(port_);
         int n = bind(listensock_,(struct sockaddr*)&peer,sizeof(peer));

         if(n == -1)
         {
             printf("bind error\n");
             exit(BIND_ERR);
         }
        if (listen(listensock_, gbacklog) <  0)
        {
            exit(LISTEN_ERR);
        }

     }

     void start()
     {

         for (;;)
         {
             signal(SIGCHLD, SIG_IGN); // 直接忽略子进程信号,那么操作系统就会自动回收

             struct sockaddr_in peer;
             socklen_t len = sizeof(peer);
             int sock = accept(listensock_, (struct sockaddr *)&peer, &len);

             std::cout << sock << std::endl;
             if (sock == -1)
             {
                 continue; 
             }

             pid_t id = fork();
             if (id == 0)
             {
                 close(listensock_);

                 HandlerHttp(sock);
                 ///////////////////////////////////////////
                 //close(sock);//不要进行close!
                 //exit(0);
                 //////////////////////////////////////////////////
             }
             close(sock);

         }
     }

     ~httpServer()
     {
     }

 private:

     void HandlerHttp(int sock)
     {
         while(1)
         {
             sleep(1);
         }
     }

 private:
     int listensock_; // tcp服务端也是要有自己的socket的!这个套接字的作用不是用于通信的!而是用于监听连接的!
     uint16_t port_;//tcp服务器的端口
     func_t func_;

 };


}

image-20231023221854354

最后主动关闭连接的一方确实也进入TIME_WAIT!

==如果我们的服务器出现了大量CLOSE_WAIT状态那么那么就有如下几种情况!==

1.服务器有bug!没有做close文件描述符的动作!——那么服务器就无法完成四次挥手!

2.服务器有压力!一直在推送消息个client!导致来不及close!

TIME_WAIT导致的bind失败

我们在进行socket编程的时候,我们可以发现一个现象,那就是——服务器有时候可以立即重启,有时候无法立即重启!——为什么会出现这种情况呢?==因为bind绑定端口失败了!==

//这是服务器主动断开
#pragma once
#include<iostream>
#include<functional>
#include<string>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

namespace server
{
 enum
 {
     USAGE_ERR = 1,
     SOCKET_ERR,
     BIND_ERR,
     LISTEN_ERR,
     ACCEPT_ERR
 };
 const static int gbacklog = 5;
 const static uint16_t gport = 8080;
 using func_t = std::function<void(const int &, int &)>;

 class httpServer
 {
 public:
     httpServer(func_t func,const uint16_t &port = gport)
         : port_(port), listensock_(-1),func_(func)
     {
     }

     void initServer()
     {
         //1.首先就是要创建套接字
         //1.1创建一个套接字!
         listensock_ = socket(AF_INET,SOCK_STREAM,0);
         if(listensock_ == -1)
         {
             exit(SOCKET_ERR);
         }

         //1.2进行绑定!
         struct sockaddr_in peer;
         bzero(&peer,sizeof(peer));
         peer.sin_family = AF_INET;
         peer.sin_addr.s_addr = INADDR_ANY;
         peer.sin_port = htons(port_);
         int n = bind(listensock_,(struct sockaddr*)&peer,sizeof(peer));

         if(n == -1)
         {
             printf("bind error\n");
             exit(BIND_ERR);
         }
        if (listen(listensock_, gbacklog) <  0)
        {
            exit(LISTEN_ERR);
        }

     }

     void start()
     {

         for (;;)
         {
             signal(SIGCHLD, SIG_IGN); // 直接忽略子进程信号,那么操作系统就会自动回收

             struct sockaddr_in peer;
             socklen_t len = sizeof(peer);
             int sock = accept(listensock_, (struct sockaddr *)&peer, &len);

             std::cout << sock << std::endl;
             if (sock == -1)
             {
                 continue; 
             }

             pid_t id = fork();
             if (id == 0)
             {
                 close(listensock_);

                 HandlerHttp(sock);
                 close(sock);
                 exit(0);
             }
             close(sock);

         }
     }

     ~httpServer()
     {
     }

 private:

     void HandlerHttp(int sock)
     {
         char buffer[1024];
         recv(sock,buffer,sizeof(buffer),0);
     }

 private:
     int listensock_; // tcp服务端也是要有自己的socket的!这个套接字的作用不是用于通信的!而是用于监听连接的!
     uint16_t port_;//tcp服务器的端口
     func_t func_;

 };


}

image-20231024104335477

==因为此时我们是主动的关闭服务器!——也就是说服务器是主动断开连接的一方!——那么这时候这个连接就会进入TIME_WAIT状态!==

==我们发现我们后面无论如何再进行bind,都会失败!==

那么这个问题有什么危害呢?实际爆发的场景有什么呢?

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.

因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

发送方怎么在第一次就知道对方的接收能力呢?——==我们在通信之前,早就做过了三次我握手!双方已经交换过报文了!TCP报文里面就有一个16位窗口大小,这样子双方就已经知道了对方接收缓冲区的大小了!==

滑动窗口

而我们上面谈论过确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候

Quicker_20231024_160323_result

既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时 间重叠在一起了)

Quicker_20231024_160513_result

窗口一定会向右滑动吗?可以向左滑动么?

一个滑动窗口的左侧是已经发送,并且已经确认的报文!

如果start下标往左移动!那么会让已经发送且确认的数据在要求被确认一次!这是不合理的!——所以滑动窗口是不能也一定不会左移的!

如果接收方的上层一直不拿走接收缓冲区里面的数据!随着接收数据的变多,那么返回回来的16位窗口大小就会越来越小!——而滑动窗口就会相应的变小就是win_start下标一直++,win_end下标不变!==win_end下标长期处于不动的状态!==

==所以滑动窗口一定会向右移动吗?——不一定!可能向右移动,也可能会保持不动!==

image-20231024201917602

窗口一定会一直不变吗?会变大吗?会变小吗?如果会变,确认的依据是会什么?

==窗口会一直不变吗?——肯定不会!他是浮动的!完全取决于对方的容量大小!可能不变!但是不会一直不变!==

image-20231024202651534

会变大么?会变小吗?——都可能会!看服务器上层的处理速度!如果处理速度慢导致了拿取数据的速度,慢与接收数据的速度!那么窗口变小!

如果处理速度快!拿去数据的速度快与接收数据的速度!那么窗口变大

收到应答确认的时候,如果不会最左侧发送的报文的确认,而是中间的,结尾的怎么办?要滑动吗?

我们上面说的都是理想情况!但是万一我们收到是中间的或者结尾的报文呢?

因为我们接收方为了保证可靠性是会对接收到的报文进行排序!——如果我们接收到了不是最左侧报文的确认!==那么就只有一种可能丢包!==

image-20231024212214830

image-20231024212342511

==我们可以看到确认序号是十分的重要的!——除了告诉对的起始窗口之外!还是支持滑动窗口的滑动规则的指定!==

所以回到我们开始的问题:如果不会最左侧发送的报文的确认,而是中间的,结尾的怎么办?要滑动吗?

滑动窗口必须要滑动吗?会不会不动了呢?或者变为0

从上面的情况我们可以看出!不是必须要滑动的!——最极端的情况!我们发出去的全部报文都丢失!那么滑动窗口也只能不动了!等待超时重传!

可能会不动!也可能变为0

会一直向后滑动吗?如果空间不够了怎么办?

image-20231024214037923

注意我们上面说过滑动窗口的大小 = 对方通知给我的字节接收能力的大小这个概念是不准确的!(或者说只正确一半)

我们上面说的所有策略,例如:超时重传,连接管理,丢包重传,按时重传,去重,滑动窗口,流量控制——目前所有的策略都是端到端的!

什么意思呢?就像是我们上面说的滑动窗口!是客户给服务器的!或者是服务器给客户的!——==是限定在你的主机与我的主机两个主机之间!==

==可是丢包的时候!除了接收方出问题!网络也可能出现问题!——也就是说如果中间这一部分出现问题了该怎么办?==

image-20231024215109199

举报

相关推荐

0 条评论