【linux】网络基础(十一)TCP协议,listen的第二个参数,ESTABLISHED状态,SYN_RCVD状态,TIME_WAIT状态,流量控制
一、验证几个现象连接的建立和accept无关listen的第二个参数如何理解ESTABLISHED状态 + 全连接队列如何理解SYN_RCVD状态 + 半连接队列客户端和服务器对连接是否建立存在不一致的问题如何理解TIME_WAIT状态setsockopt为什么需要TIME_WAIT二、流量控制
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
前言
【linux】网络基础(十)TCP协议,TCP协议报文格式,标志位ACK SYN RST FIN PSH URG,超时重传机制,三次握手,四次挥手——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络基础(十一)TCP协议,listen的第二个参数,ESTABLISHED状态,SYN_RCVD状态,TIME_WAIT状态,流量控制
一、验证几个现象
连接的建立和accept无关

- 在上一篇文章中 详情请点击<——,小编讲解的accept的作用仅仅是将连接获取上来,那么换句话来说也就是意味着TCP三次握手连接的成功建立和上层调用的accept无关,三次握手成功我们会观察到客户端和服务器的连接状态都为ESTABLISHED,那么小编将上层调用的accept屏蔽掉,那么究竟能否连接成功状态为ESTABLISHED呢?下面小编来验证一下
- 那么为了更好的演示实验现象,那么这里小编就拿之前文章编写以及讲解的代码直接进行实例演示详情请点击<——
- 这里我们在Main.cc的main函数中将日志打印显示到显示器上,即采用日志的默认打印方式,那么就将日志打印到显示器上的代码lg.Enable(Classfile)注释掉
#include <iostream>
#include <memory>
#include "TcpServer.hpp"
#include "Log.hpp"
void Usage(const std::string str)
{
std::cout << "\n\tUsage: " << str << " port[1024+]\n" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(UsageError);
}
// lg.Enable(Classfile); //调整日志打印到文件中
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> server(new TcpServer(port));
server->InitServer();
server->StartServer();
return 0;
}
- 接下来在TcpServer.hpp的StartServer中进行了守护进程以及线程池这里我们都进行注释,并且for循环中调用了accept,这里小编并不想调用accept,所以小编将for循环内的代码都注释掉,让for循环中仅仅执行sleep(1)即可
- 即本文服务器不对客户端的请求报文进行任何处理,也不做任何响应,本文服务器的作用仅仅是测试在不调用accept的情况下,TCP的三次握手能否成功,能否观察到客户端和服务器进行三次握手成功后连接建立成功处于ESTABLISHED状态
- 所以如何观察呢?我们可以使用netstat工具,文章第二点的netstat工具进行的讲解,详情请点击<——
void StartServer()
{
// Daemon("/");
// ThreadPool<Task>::GetInstance()->Start();
// lg(Info, "tcpserver is running...");
for (;;)
{
sleep(1);
// struct sockaddr_in client;
// socklen_t len = sizeof(client);
// int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
// if (sockfd < 0)
// {
// lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
// continue;
// }
// uint16_t clientport = ntohs(client.sin_port);
// char clientip[32];
// inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d",
// sockfd, clientip, clientport);
}
}
- 其实这里最好的测试方案是使用两个云服务器,一个云服务器上运行服务器,另一个云服务器上运行客户端,这样的话两台云服务器的内网IP不一致,进行观察现象演示效果更佳,小编这里仅有一台云服务器,所以碍于设备限制,小编只能使用一台云服务器为大家带来演示
- 所以小编就复制ssh渠道,然后在左侧作为服务器,右侧作为客户端,当然在今天服务器使用固定端口8080,客户端则是使用的是系统随机绑定的端口

- 所以如上,客户端和服务器就连接成功了,那么我们如何查看客户端和服务器的连接状态呢,究竟是不是ESTABLISHED呢,下面我们再复制一个ssh渠道,然后使用netstat工具查看一下,首先使用n选项显示数字,然后使用t选项选择tcp,然后使用p选项显示程序名,那么紧接着我们知道显示的形式中包含源端口,目的端口,源IP,目的IP,所以为了其它服务产生干扰,所以我们将netstat的结果通过管道交给grep,然后使用grep过滤查找端口号8080即可,但是这样就没有头部字段信息了,所以我们使用netstat -ntp | head -2然后将两者通过&&进行合并即可,如下
netstat -ntp | head -2 && netstat -ntp | grep 8080

- 那么我们观察上图,下方左侧即为我们使用netstat工具查看的服务器和客户端的状态,那么首先我们可以看到第一栏是字段描述,那么紧接着向下依次是客户端和服务器
- 观察,客户端的本地地址,即IP是10.0.12.3其实这个才是云服务器真正的内网IP,接下来的39526是操作系统为客户端绑定的随机端口号,紧接着就是客户端连接的远端服务器的地址,即IP地址和端口号,这里的服务器的IP地址是124.220.4.187是经过云服务器供应商修饰过的公网IP不可绑定,真实的IP是内网IP即10.0.12.3,接下来是服务器的绑定的端口号8080,最关键的是客户端的连接状态为ESTABLISHED,即客户端三次握手连接成功
- 紧接着向下继续观察,服务器的本地地址,即IP是10.0.12.3,即这个是真实的内网IP,我们给服务器绑定的端口号是8080,无误,紧接着是服务器连接的远端的地址,即客户端的IP地址124.220.4.187,客户端由操作系统绑定的随机端口号39526无误,最关键的是服务器的连接状态为ESTABLISHED,即服务器三次握手连接成功
- 别忘了这个服务器的代码,小编已经将调用accept的代码注释了,所以我们可以得出,连接建立成功和上层有没有调用accept没有关系,TCP的三次握手是由双方操作系统自动完成的
listen的第二个参数

- 小编在当初使用listen的时候说,关于listen的第二个参数backlog我们默认先设置为10,等到后面合适的时机的时候,小编再进行讲解,现在,时机到了
- 那么我们先看现象,再看原理,我们将listen的第二个参数backlog由10调整为1,然后重新编译代码,然后运行服务器和客户端,这里的服务器运行起来之后,我们复制ssh渠道三次,那么在三个会话中调用客户端连接服务器
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"
const int defaultfd = -1;
const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";
const int backlog = 1; // 将backlog由10调整为1
extern Log lg;
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError
};
class TcpServer
{
public:
TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
: listensock_(defaultfd), port_(port), ip_(ip)
{}
void InitServer()
{
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
lg(Fatal, "create socket error, errno: %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, listensock_: %d", listensock_);
//防止偶发性服务器无法立即重启
int opt = 1;
setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(server.sin_addr));
socklen_t len = sizeof(server);
if (bind(listensock_, (struct sockaddr *)&server, len) < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, listensock_: %d", listensock_);
if (listen(listensock_, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "listen socket success, listensock_: %d", listensock_);
}
void StartServer()
{
for (;;)
{
sleep(1);
}
}
private:
int listensock_;
uint16_t port_;
std::string ip_;
};

- 所以如上,我们先启动服务器绑定8080端口,然后启动第一个客户端连接服务器,操作系统为客户端随机绑定的端口号是42964,同样的观察上图的下方左侧,客户端连接服务器成功,服务器和客户端双方都处于ESTABLISHED,无误

- 那么紧接着,小编右方上侧复制ssh渠道,启动第二个客户端连接服务器,操作系统为客户端随机绑定的端口号是43028,同样的观察上图的下方左侧,客户端连接服务器成功,服务器和客户端双方都处于ESTABLISHED,无误

- 那么紧接着,小编右方上侧复制ssh渠道,启动第三个客户端连接服务器,操作系统为客户端随机绑定的端口号是43076,同样的观察上图的下方左侧,客户端连接服务器成功,客户端处于ESTABLISHED状态,但是此时的服务器却是处于SYN_RECV状态

如何理解ESTABLISHED状态 + 全连接队列
- 所以对于为什么第三个客户端连接服务器,服务器端此时处于SYN_RECV状态我们该如何理解呢?listen系统调用的第二个参数backlog是什么意思呢?那么我们还要从TCP的连接管理谈起才能理解

- 所以我们先来看为什么客户端一和客户端二可以连接服务器成功,并且对应的服务器也连接成功,客户端和服务器端的连接状态都为ESTABLISHED,首先我们要知道,服务器收到多个客户端的连接,所以服务器要对这些连接成功的连接管理起来,如何管理?先描述再组织,所以就要针对每一个连接建立连接对象,然后使用数据结构将这些连接对象组织起来,使用什么数据结构?
- 队列,所以就使用队列将这些连接对象组织起来,所以从此以后对连接的管理就转化成对队列的增删查改,特别的,我们把这个管理连接对象的队列叫做全连接队列
- 而listen的第二个参数是backlog,listen的第二个参数backlog + 1的数值表示底层已经建立好的全连接队列的最大长度,那么小编将lisen的第二个参数backlog设置为1,所以底层全连接队列的最大长度就是backlog + 1 = 1 + 1 = 2
- 所以客户端一过来连接服务器连接成功,服务器就要为这个连接创建连接对象,那么使用全连接队列组织起来,所以底层全连接队列的长度为1,客户端二过来连接服务器连接成功,服务器就要为这个连接创建连接对象,那么使用全连接队列组织起来,所以底层全连接队列的长度为2,此时的全连接队列的长度为2,那么由于全连接队列的最大长度为2,所以也就意味着此时全连接队列满了,如果再有连接过来,那么将会无法入队列,连接失败
- 那么小编,我们建立连接队列,入全连接队列,然后全连接队列就被入满了,那么什么时候出全连接队列呢?别忘了上层还有accept,当上层调用accept的时候,就会从全连接队列的头部出队列,然后就可以将连接对象拿走,即出队列,然后将连接对象和文件打开对象关联起来,将连接对象放在文件打开对象中,所以此时accept就可以自然获得文件打开对象的文件描述符sockfd
- 所以从此以后,如果想要基于连接进行通信,那么首先就要找到连接对象,如果想要找到连接对象只需要找到网络文件描述符sockfd即可,所以我们也就理解了为什么accept的返回值是一个int类型的网络文件描述符sockfd
- 但是别忘了,小编对于服务器的accept的代码仍然是注释的,即上层没有调用accept出全连接对象,将连接对象拿走,所以这也就类似于生产者消费者模型,生产者入队列,而消费者不消费,所以会导致全连接队列空间满了,这里的空间即全连接队列的最大长度2,所以再来的连接对象会由于全连接队列的空间满了就无法入全连接队列了,所以此时我们来看客户端三的到来
如何理解SYN_RCVD状态 + 半连接队列

- 所以此时小编使用客户端三向服务器进行三次握手,发送SYN报文,服务器收到之后,将自身连接状态设置成SYN_RCVD,然后向对方发送SYN+ACK即连接确认应答报文,所以客户端收到后,就会将连接状态设置为ESTABLISHED,然后在自身底层创建连接对象初始化,然后向服务器发送ACK报文

- 所以对于客户端来讲客户端在三次握手中将第三次ACK报文发出,客户端就认为自己已经三次握手完成,建立连接成功,所以客户端在会在第三次ACK报文发出前创建连接对象,即在客户端的认知中,三次握手成功,建立连接成功,所以上图中的本地端口号为43076的客户端三认为连接建立成功,即倒数第二行,客户端三的连接状态为ESTABLISHED

- 但是对于服务器来讲,虽然服务器收到了来自客户端三的ACK报文,所以服务器就要为这个连接创建连接对象,但是服务器在创建连接对象前,会先检查全连接队列的当前长度是否等于全连接队列的最大长度,所以经过检查,全连接队列的当前长度是2等于全连接队列的最大长度2,全连接队列没有空间了,所以此时即使创建了连接对象也无法入全连接队列

- 所以此时服务器就将来自客户端三的ACK报文丢弃,并且服务器认为本次和客户端三的三次握手失败,连接失败,创建连接对象失败,而SYN_RCVD状态切换到ESTABLISHED状态前提必须是三次握手成功,连接成功,创建连接对象成功,所以此时SYN_RCVD状态切换到ESTABLISHED状态失败了,所以如上红色框内的服务器对于客户端三的连接状态才会停留在SYN_RCVD状态,即此时在服务器的认知中,客户端三和我服务器的三次握手失败,连接失败,连接对象创建失败
- 那么问题来了,服务器如何得知对于客户端三的连接状态会停留在SYN_RCVD状态,所以一定有对应的结构进行描述这个状态服务器才会得知,所以不仅仅连接建立成功的时候要创建连接对象,对于在连接处于SYN_RCVD的时候也会创建连接对象哦,我们把在SYN_RCVD状态的时候创建的连接对象称为半连接对象,处于ESTABLISHED状态的时候创建的连接对象称为全连接对象,所以才有的全连接队列组织全连接对象呀
- 那么如果三次握手,连接建立失败的时候,有的连接的状态是会处于SYN_RCVD状态,服务器可能会面对多个客户端的连接,所以也就意味着有可能多个客户端也会三次握手失败,连接建立失败,所以也就意味着服务器对应客户端的SYN_RCVD状态可能会存在多个,即半连接对象可能会有多个,那么服务器对应的操作系统要不要将这些半连接对象管理起来?要

- 要,如何管理?先描述在组织,那么已经描述成了半连接对象,所以仅需使用数据结构将半连接对象组织起来,使用什么数据结构?队列,所以就可以使用队列将半连接对象组织起来,我们将这个队列称为半连接对象,所以从此以后对处于连接状态处于SYN_RCVD状态的管理就转化成了对半连接队列的增删查改
- 同样的,我们还要理解一个点:如果SYN_RCVD状态切换到ESTABLISHED状态失败会停留在SYN_RCVD对应的半连接对象,然后将这个半连接对象入半连接队列。那么如果SYN_RCVD状态切换到ESTABLISHED状态成功了,则同样要创建半连接对象,并且将这个半连接对象入半连接队列,更关键的一步是,如果全连接队列中有空间的话,就可以将处于半连接队列中的节点出半连接队列,然后将这个节点入全连接队列中,即全连接队列中的节点来源于半连接队列中的节点

- 那么我们回归现象,刚刚我们看到了服务器的操作系统仍然在维持服务端对于客户端三的SYN_RECV连接状态,本质上是在维护底层半连接队列中的节点

- 所以此时过了一会儿,如上我们在使用netstat工具查看连接状态,奇怪的是服务端对于客户端三的SYN_RECV连接状态没有了,不难理解,站在我服务器的角度,你客户端三已经和我服务器进行三次握手失败了,并且建立连接已经失败了,要知道你半连接对象是要占用内存空间,即占用服务端操作系统的资源的,而操作系统不会做任何一件浪费时间和空间的事情,那么我服务端自然也没必要长时间维持对应的半连接对象了
- 所以服务端,不会长时间维护SYN_RECV状态,即服务器作为被建立连接的一方,连接状态处于SYN_RECV状态,那么对应也创建了半连接对象,并且将半连接对象入了半连接队列,即半连接队列的节点,不会长时间维护

- 那么listen的第二个参数backlog + 1表示的是全连接队列的最大长度,那么对于listen的第二个参数backlog我们可以设置成多少呢?一般为10,16,32,一般我们不会将listen的第二个参数backlog设置太长,那么为什么我们不能将listen的第二个参数backlog设置太长呢?
- 如果我们将listen的第二个参数backlog设置的很长,那么我们知道本质上就是将全连接队列的最大长度设置的很长,那么如果全连接队列为满的情况下,会存在很多很多的节点,那么每一个节点都要占用内存空间,即占用系统空间资源,所以如果很多很多的节点那么就会占用相当一大块的系统资源,所以今天如果服务器很忙,服务器正在对accept从全连接队列中获取上来的一个连接对象进行服务
- 同样的如果进行服务消耗的资源很多,进行大量的计算,拷贝等工作,所以也就意味着进行服务同样要消耗资源,所以本身服务器就在忙着对这个连接对象服务,那么就要消耗很多的系统资源,可是今天如果由于某种原因,系统资源快不足了,别忘了上层服务器为这个连接提供服务是要消耗很多资源的,即此时上层服务器提供服务十分的吃力,卡顿
- 所以也就造成了服务器很忙,几乎无法再accept从全连接队列中获取连接对象提供服务了,而此时你全连接队列还很长,那么也就势必会占用很多的系统资源,更关键的是你这个全连接队列占用了系统资源还不产生任何效益,只是贪婪的占着资源
- 那么在今天如果把这个全连接队列缩短,那么就可以把占用的系统资源腾出来给我服务器使用,那么我服务器提供服务也可也变快,那么对accept从全连接队列中获取上来的一个连接对象进行服务的速度也会变快,所以会整体上提高服务器服务的效率,所以我们何必让这个全连接队列这么长,我们要尽量将全连接队列短一点,好,小编那么我们可不可以不要这个全连接队列,即将listen的第二个参数设置为0,即backlog = 0,那么全连接队列的最大长度就为backlog + 1 = 0 + 1 = 1,所以即只有一个全连接对象呢?
- 不行,如果仅仅只有一个全连接对象,那么服务器使用accept获取上来提供服务,当提供服务完成的时候,那么此时恰好没有客户端来连接服务器,所以就会让服务空闲下来,所以也就会造成了服务器资源没有被充分利用
- 这也就类似于生产者消费者模型,服务器作为生产者,如果服务器将当前的服务提供完成,那么此时恰好没有客户端来连接服务器,但是不怕,因为有全连接队列,所以服务器就可以调用accept将连接对象出全连接队列继续进行下一个服务,将连接对象放入全连接队列中缓存起来,所以也就可以充分的利用服务器的资源,所以listen的第二个参数不能设置为0,同样的也不能设置的很长,应该适当的较短一些为10,16,32

客户端和服务器对连接是否建立存在不一致的问题
- 那么在上面客户端三连接服务器的时候,也是出现了客户端和服务器对于三次握手是否完成,连接是否建立存在不一致的问题,在客户端的认知中,三次握手完成,连接建立成功,所以此时如果客户端三处于ESTABLISHED状态,服务器处于SYN_RECV状态
- 那么客户端三直接向服务器发送数据报文,那么数据报文其实是会携带ACK标志位的,所以此时如果服务器处于SYN_RECV状态,那么服务器会继续先提取标志位ACK标志位置,完成三次握手,然后再提取数据进行通信,但是在今天不好意思,因为我上层并没有调用accept,所以连接对象不会出全连接队列,所以全连接队列仍然是满的,即全连接队列的最大长度仍然为2,全连接队列中的全连接对象有两个,一个是客户端一的,一个是客户端二的,只要客户端一和客户端二不进行四次挥手,那么此时全连接队列的当前长度就会等于全连接队列的最大长度,所以全连接队列没有空间了,所以此时客户端三发来的ACK报文还是会被服务器丢弃,然后继续服务器短时间维持在SYN_RECV状态
- 那么第二种情况,如果客户端三处于ESTABLISHED状态,服务器没有维持SYN_RECV状态,即客户端三向服务器直接发送数据的前服务器直接将SYN_RECV对应的处于半连接队列中的半连接对象释放了,那么客户端发送数据报文是不会发送成功的,因为此时服务器会认为并没有进行三次握手,连接并没有被建立成功,所以服务器会发送设置了RST标志位的报文告诉客户端进行重新服务器重新进行三次握手建立连接,但是不好意思,今天全连接队列仍然是满的,所以重新进行三次握手,建立连接还是失败,那么发送的数据会停留在客户端三的TCP发送缓冲区中,除非上层调用了accept将连接对象获取上去了,那么此时全连接队列的当前长度小于全连接队列的最大长度的时候,即此时全连接队列有空间,在通信通路顺畅的情况下,才会三次握手成功,三次握手成功,连接建立成功,数据报文才会被发送成功
如何理解TIME_WAIT状态

- 那么下面我们来看上方,设定左侧是服务器,右侧是客户端,那么如上,左侧服务器是先调用close断开连接,即服务器是主动断开连接的一方,所以对于服务器来讲,当服务器收到来自客户端发来的FIN报文,并且自身构建ACK报文发向客户端的时候,对于服务器来讲四次挥手就已经完成了,所以在四次挥手完成后,服务器作为主动断开连接的一方就要进入TIME_WAIT状态,若干时长后,自动释放连接对象
- 那么我们也想要在代码中进行调整,并且运行,期望可以模拟演示出服务器处于TIME_WAIT状态,所以接下来小编就要调整一下服务器的代码了,那么对于初始化服务器,这里小编将防止偶发性服务器无法立即重启的代码注释掉,当我们让服务器作为主动断开连接的一方就要进入TIME_WAIT状态,即此时服务器要进行等待一段时间之后才会释放连接对象,那么也就意味着服务器对应的IP地址和端口号由于是属于之前启动的服务器进程
- 而一个端口号不可以绑定多个进程,所以在这一段时间内,我们再重新启动一个服务器进程,那么服务器如果再想要使用之前服务器的IP地址和端口号就会失败,所以我们期望观察到服务器bind绑定失败的情况
void InitServer()
{
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
lg(Fatal, "create socket error, errno: %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, listensock_: %d", listensock_);
//防止偶发性服务器无法立即重启
// int opt = 1;
// setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(server.sin_addr));
socklen_t len = sizeof(server);
if (bind(listensock_, (struct sockaddr *)&server, len) < 0)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, listensock_: %d", listensock_);
if (listen(listensock_, backlog) < 0)
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "listen socket success, listensock_: %d", listensock_);
}
- 所以对于启动服务器的代码,当服务器收到一个连接并且成功的进行了三次握手建立连接,那么我们调用accept将连接对象和文件打开对象关联,并且将文件打开对象的网络文件描述符sockfd获取上来,打印日志信息即可

- 那么对于服务器小编采用ctrl+c的方式退出终止进程,文件打开对象的生命周期随进程,而这里的文件打开对象可是和对应的服务器和客户端建立连接的连接对象关联的,所以服务器进程终止,进程连接对象就要被关闭,类似于调用close关闭文件打开对象,即此时就类似于服务器调用了close,所以此时服务器主动退出连接
- 所以当服务器主动退出连接的时候,那么首先服务器就会进入FIN_WAIT_1状态,然后给客户端发送FIN状态,客户端收到之后自身要设置为CLOSE_WAIT状态,然后客户端给服务器发送ACK报文,进而服务器接收到之后就会处于FIN_WAIT_2,所以当小编先ctrl+c退出服务器的时候,期望观察到客户端进入CLOSE_WAIT状态,服务器进入FIN_WAIT_2状态

- 那么接下来小编将会ctrl+c终止客户端,同样的道理,客户端对应的连接也会被关闭,类似于客户端调用了close关闭连接,所以客户端会将自身状态设置为LASK_ACK,然后向服务器发送FIN报文,服务器收到FIN报文之后,就知道此时客户端想要和我断开连接,所以服务器就构建ACK报文然后发送给客户端,当服务器完成ACK报文的时候,在服务器的认知中,此时四次挥手已经完成,所以服务器进入TIME_WATI状态,即此时服务器作为主动退出连接的一方就会等待一段时间之后再调整为CLOSED状态,然后关闭文件打开对象,重点是这里的等待一段时间
- 接下来客户端作为被动退出连接的一方收到ACK报文,进入TIME_WATI状态,就会直接释放连接对象对应的文件打开对象,这个过程很快,所以此时我们无法观察到客户端的CLOSED状态,同样的服务器的CLOSED状态我们也无法观察到,但是服务器的连接状体当为TIME_WAIT的时候,服务器可是会等待一段时间的,所以此时小编期望可以观察到服务器的连接状态为TIME_WAIT状态
void StartServer()
{
for (;;)
{
sleep(1);
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d",
sockfd, clientip, clientport);
sleep(1000);
}
}
-
所以接下来小编运行代码,然后进行测试

-
所以首先小编启动服务器绑定8080端口号,然后启动客户端连接服务器,那么我们使用netstat工具观察连接状态,服务器和客户端都处于ESTABLISHED,即此时客户端和服务器都完成了三次握手,并且已经建立连接成功


-
如上,那么接下来我们退出客户端,成功的观察到了客户端此时处于CLOSE_WAIT状态,服务器此时处于FIN_WAIT2状态

-
那么接下来小编退出客户端,所以此时服务器作为主动退出连接的一方,就要维持一段时间的TIME_WAIT状态,无误

-
所以我们这里在服务器仍然处于TIME_WAIT状态的时候,那么对于服务器一端来讲,服务器和客户端的连接还没有被彻底断开,所以此时服务器的IP地址和端口号8080仍然还没有释放,仍然被未释放的连接对象对应的文件打开对象所占用,那么接下来小编再次启动新的服务器进程,此时新启动的服务器进程使用的还是原来的服务器的IP地址和端口号8080,但是此时别忘了原来的服务器的IP地址和端口号8080仍然被占用,所以这里的小编新启动的服务器进程就会绑定失败
-
所以TCP三次握手连接后,主动断开连接的一方,在四次挥手完成后,会进入TIME_WAIT状态,等待若干时长会自动释放,所以此时对于这里主动断开连接的一方,即服务器来讲,此时连接没有被完全断开,那么IP地址和端口号port依旧被占用,IP地址和端口号port依旧被正在使用,所以如果此时我想立即重启服务器就会失败,因为那么IP地址和端口号port被占用
-
所以问题就是,主动断开连接的一方,由于IP地址和端口号port被占用,无法立即重启,小编,小编,如果主动断开连接的一方是服务器,即此时服务无法重启,那么我们就等待一会儿不就好了,等待一会之后服务器将状态调整从TIME_WAIT调整为CLOSED状态的时候就会释放打开对象,此时IP地址和端口号port就可以被再次绑定了
-
但是如果今天的服务器不是普通的服务器,而是拼多多的服务器呢?TIME_WAIT状态一般要求等待60秒到120秒,这可是足足最多两分钟呀,而使用拼多多的用户又十分多,那么每一个用户在自己的手机上打开的拼多多都是作为拼多多客户端来使用的,所以此时就会造成了拼多多客户端无法连接服务器,那么用户也就无法交易买东西了,所以呢?
-
假设拼多多一秒的成交商品的价格总和是100万,那么120秒,算了不按照120,保守估计按照60秒来算,所以60 * 100万 = 6000万,所以此时仅仅因为服务器处于TIME_WAIT状态,服务器无法立即重启,所以拼多多就损失了6000万交易量中的平台抽成,大约几百万,所以仅仅是由于服务器主动断开连接,进而服务器处于TIME_WAIT状态,服务器无法立即重启,造成的损失就会很大,所以呢?我们是否有方法可以让服务器立即重启呢?
-
有的,我们是有方法的那么就是在初始化服务器的时候调用setsockopt函数
setsockopt

- 所以第一个参数传入网络文件描述符sockfd,这里是服务器的监听文件描述符listensock,然后第二个是设置为SOL_SOCKET,即socket编程,那么第三个参数则是SO_REUSEADDR|SO_REUSEPORT,即重复使用地址,这里的地址是指IP地址和端口号port,那么第四个设置选项是选项,所以这里我们定义一个opt为1,然后传入地址即可,那么最后传入选项的地址即可
- 使用了setsockopt之后,就可以让当服务器主动退出的时候,即使服务器的连接处于TIME_WAIT状态,也可以立即进行重启
int opt = 1;
setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
- 同样的我们也要注意,一般服务器一旦启动,服务器就不会关闭,如果出现断电,那么服务器会退出终止,同样的,如果服务器负载过大,连接服务器的客户端过多服务器也可能会出现异常终止退出,这时候就会造成服务器主动退出连接的情况,如果没有设置setsockopt那么服务器就会无法立即重启,但是只要我们调用了setsockopt即可设置,让服务器支持立即重启
- 为什么小编这里主要是围绕服务器来进行讲解的呢?难道客户端不会进入TIME_WAIT状态吗?会
- 客户端如果作为主动断开连接的一方,那么同样的也会进入TIME_WAIT状态,但是别忘了客户端的端口号port不是固定的,客户端的端口号是操作系统随机绑定的端口号,范围是1024到65535,所以当我们关闭一个已经连接的客户端之后,立即重启客户端进程,那么客户端连接服务器使用的又会是操作系统为它绑定的随机端口,和之前的端口号冲突的概率极低极低,就算冲突了也不怕,那么继续重启客户端即可,此时操作系统又继续会为这个客户端绑定随机端口号
- 那么为什么服务器不采用随机端口号呢?因为服务器是要提供服务的,客户端想要和服务器进行连接,就必须要知道服务器的端口号,所以服务器的端口号必须固定,并且广泛的被客户端知道,所以这里的服务器就只能采用固定关口好了
为什么需要TIME_WAIT
- TIME_WAIT状态所等待的时间一般是60秒到120秒,所以呢?为什么需要让主动断开连接的一方进入TIME_WAIT状态呢?如下

- 第一,可以让通信双方的历史数据得以消散,那么我们思考一下,有没有可能客户端和服务器进行通信的时候,即如上图的红色框内,突然客户端退出了,所以此时可能服务器发送的数据报文此时刚刚发送到网络中,还没有来得及被客户端接收,但是此时客户端退出了,所以数据报文就成了残留在网络中的历史数据
- 如果没有TIME_WAIT这个状态,那么此时客户端就可以立即重启,那么此时客户端就会去连接服务器,那么操作系统就会为客户端分配随机端口号,可是如果本次操作系统为客户端分配的随机端口号和上一次操作系统为客户端分配的随机端口号相同,所以本次客户端的IP地址和端口号就和上一次客户端一致
- 那么本次客户端是要和服务器进行新的通信,发送和接收新的报文数据了,那么当客户端和服务器进行TCP三次握手之后,此时客户端就和服务器建立了连接了,由于客户端要访问的服务器的IP地址和端口号没有改变,很巧的是客户端的IP地址和端口号也没有改变,所以当初服务器发送给客户端的残留的历史数据报文,那么此时被客户端收到了,那么此时历史数据报文就对本次通信产生了干扰
- 所以此时当客户端退出连接的时候,让客户端进入TIME_WAIT状态,那么此时TIME_WAIT状态可以尽可能的确保客户端收到残留在网络中的历史数据报文,然后让客户端将这个历史数据报文丢弃,以免对后面的通信产生干扰,那么为什么数据报文会残留在网络中呢?
- 可能是由于路由器的缘故,导致数据报文残留阻塞到了路由器中,当路由器状况较好的时候,数据报文就会被发送出去,所以数据报文是可以由于网络拥塞,数据报文在网络中残留的,可是我们知道为什么网络通信,例如我们手机发送和接收消息,上网浏览一般是很快的,即网络通信发送数据一般是毫秒ms级别的,可是为什么TIME_WAIT状态的时间却是60秒到120秒呢?

- 因为网络通信正常通信是毫秒ms级别的,可是如果网络拥塞导致数据报文在网络中残留下来,如果网络十分的拥塞,那么此时就会导致数据报文在网络中残留的时间比较长,即非正常通信,网络状况不好的时候的通信,数据报文可以残留在网络中存在较长时间,我们将这个时间叫做数据报文的最大生存时间MSL,在linux操作系统中默认MSL是30秒,通常TIME_WAIT状态的时间是2MSL,即在linux中2MSL是60秒,即TIME_WAIT状态的时间是60秒,可是我们知道操作系统不会做任何一件浪费时间和空间的事情,所以针对网络状态的不同,TIME_WAIT的时间也是会进行一定程度上的动态调整的
- 所以TIME_WAIT状态的时间是2MSL,即此时而一个MSL的时间是数据报文在网络中的最大生存时间,即此时是服务器向客户端发送数据报文,可以确保客户端收到,而此时我们使用的是2MSL,即客户端将收到的数据报文,然后将这个数据报文进行简单的应答给服务器,尽量避免服务器因为接收不到确认应答而造成服务器触发超时重传机制
- 所以TIME_WAIT状态的时间是2MSL,即数据报文一来确认应答报文一回的时间,并且是考虑并且采用了网络状况不好的时候的数据报文可能会阻塞到网络中最大生成时间,所以有了TIME_WAIT状态可以尽可能的让通信双方的历史数据消散,尽可能的避免对下次通信产生影响

- 接下来是第二个原因,可以让通信双方断开连接,进行四次挥手的时候具有较好的容错性,那么左侧客户端主动关闭连接的时候,当对方也想要和我断开连接,所以对方服务器会给我发送FIN报文,可是如果这个FIN报文在网络中丢失了呢?所以此时客户端就收不到IFN报文,进而服务器就无法收到来自客户端的ACK报文应答,所以此时四次挥手在服务端就无法很好的断开连接,因为服务器此时收不到来自客户端的ACK应答报文
- 那么此时既然服务器此时收不到来自客户端的ACK应答报文,所以服务器就会认为服务器发送给客户端的FIN报文丢失,触发超时重传机制,所以如果此时客户端没有停留在TIME_WAIT状态,即此时客户端直接退出了,所以自然客户端就收不到IFN报文,进而服务器就无法收到来自客户端的ACK报文应答了,所以服务器就会触发超时重传机制,然后串行多次发送FIN报文,但是由于客户端的连接已经关闭,所以服务器就会超时重传失败,所以此时四次挥手在服务端就无法很好的断开连接,同样的我们也要看到这时候服务器进行多次的超时重传会消耗一定的网络资源
- 那么如果此时客户端停留在TIME_WAIT状态的话,那么尽管服务器发送的第一次FIN报文丢失,服务器触发了超时重传机制,此时客户端就可以对来自服务器的第二次FIN报文,然后客户端就可以对服务器进行应答确认发送ACK报文,所以此时服务器就可以正常收到来自客户端的ACK报文,然后服务器此时就会认为四次挥手完成,然后释放连接对象,然后退出连接,所以TIME_WAIT状态可以让通信的双方退出连接具有较好的容错性
- 这里我们理解容错性,并不是一定可以让服务器收到ACK报文,如果来自客户端的ACK报文次次都在网络中丢失,那么服务器也照样接收不到来自客户端的ACK报文,但是也有应对方案,即服务器认为FIN报文丢失之后,那么经过多次超时重传FIN报文之后,仍然接收不到来自客户端的ACK报文,所以服务器此时超时重传失败,那么服务器就会认为连接异常,然后自动释放连接
- 有了TIME_WAIT状态的存在我们可以尽可能的减少四次挥手的异常情况,让双方进行四次挥手,尽可能的成功,对于一些情况无法处理,但是绝大部分的情况可以处理,所以处理了之后,就可以让让双方进行四次挥手,尽可能的成功,同样的也可以尽可能的避免服务器超时重传失败,消耗网络资源
二、流量控制
- 其实在之前的文章中,小编已经讲解过流量控制了,第三点流量控制,详情请点击<——,那么这里小编再加深拓展讲解一下流量控制
- 接收端处理数据报文的速度是有限的,这时候如果发送端发送数据报文的速度太快,那么就会将接收端的接收缓冲区打满,那么这时候如果发送端再次进行发送,就会造成丢包,进而引起发送端进行丢包的超时重传等问题
- 因此TCP根据接收端的接收处理能力,来决定发送端的发送数据报文的速度(流量,数据报文携带数据的大小),那么这种机制就叫做流量控制,如下
(一)接收端将自己接收缓冲区的剩余空间大小通过ACK应答报文中的16位窗口大小的字段告诉给发送端
(二)窗口大小字段越大,那么说明网络吞吐量越高
(三)接收端一旦发现自己的接收缓冲区的大小快满了,那么就会将窗口大小设置成一个更小的值通知给发送端
(四)发送端接收到这个窗口大小之后就会减缓自己的发送速度,即减缓发送数据报文的速度(流量,数据报文携带数据的大小)
- 如果接收端的接收缓冲区的已经满了,即此时接收端的接收缓冲区的剩余空间大小为0,那么此时接收端就会将窗口大小设置为0然后通过ACK报文告诉给发送端
- 那么这时候发送端不再发送数据报文,而是定义发送一个窗口探测数据段(本质上就是一个报文,报文不携带有效载荷数据,仅仅包含报头,那么基于确认应答机制,当接收端收到一个报文之后就会进行发送ACK确认应答报文,所以这个ACK确认应答报文中可是有完整的报头的,所以报头中的16位窗口大小也自然被设置为当前接收端接收缓冲区的剩余空间大小,所以自然接收端就可以得知发送端的窗口大小),所以发送端就得知了接收端的窗口大小
- 同样的,如果接收端一旦将接收缓冲区的空间腾出来了,那么此时接受端会主动发送报文给发送端,所以发送端同样也可以得知接收端的窗口大小,然后接收端可以发送数据报文,别忘了接收端收到了发送端的报文还要进行确认应答,所以就会通过捎带应答机制在数据报文中设置ACK标志位
- 所以通过接受端主动通知,以及发送端的定期窗口探测可以让发送端尽可能快的得知接收端的窗口大小
- 同样的,我们还要看到报头中的标志位的PSH字段 第二点的6个标志位中的PSH,详情请点击<——,其实一般情况下不会到接收端的接收缓冲区的剩余空间大小为0的情况,而是在接收端的接收缓冲区的剩余空间大小较小的时候,此时被发送端得知,那么此时发送端就会给接收端发送携带了PSH标志位的报文,催促接收端尽快的将接收缓冲区的空间腾出来

- 实际上关于接收缓冲区大小,即窗口大小都是可以进行协商的,即在TCP报文中40个字节的选项包含了一个窗口扩大因子M,那么接收缓冲区的实际窗口大小是窗口字段的值左移M位,如果M设置为1,那么接收缓冲区的窗口大小原本是16位对应65535个字节,那么此时就是将16位左移一位变成17位大小,那么此时17位大小就是对应2 * 65535 = 131070字节
- 那么最初的发送方和接收方如何得知要发送的数据应该处于什么范围呢?如果发送的数据过多就有可能发送的数据大小超过对方的接收缓冲区的剩余空间的大小,进而造成丢包等问题,所以双方在正式通信前如何得知双方的接收缓冲区的剩余空间的大小呢?即协商双方的接收能力?

- 是通过的TCP的三次握手进行协商的,左侧客户端作为发送端,右侧服务器作为接收端,首先进行第一次握手,客户端向服务端发送报文,其中这个报文中是一定包含完整报头的并且不携带有效载荷数据,所以客户端也一定会将自己的接收缓冲区的剩余空间的大小设置进报头的16位窗口大小中,所以此时服务端就可以得知客户端的接收缓冲区的大小
- 那么紧接着第二次握手,服务端向客户端发送报文,其中这个报文中是一定包含完整报头的并且不携带有效载荷数据,所以服务端也一定会将自己的接收缓冲区的剩余空间的大小设置进报头的16位窗口大小中,所以此时客户端就可以得知服务端的接收缓冲区的大小
- 所以三次握手的前两次握手,我们不能仅仅看到双方进行了握手,还要看到双方也交换了报文,前两次握手中的报文只含有报头,不包含有效载荷数据,即此时通信双方已经得知了对方的接收能力
- 所以第三次握手,客户端给服务器发送的ACK报文中可以携带数据,因为客户端此时已经知道了服务器的接收缓冲区的大小了,所以完全可以进行流量控制,所以此时服务器收到了来自客户端的携带了数据的ACK报文,那么会先提取报头,然后获得报头中的ACK字段完成三次握手,此时完成了三次握手就可以进行通信了,并且ACK报文中携带了数据,然后就可以将数据放到服务器的接收缓冲区中,此时我们也能看到第三次握手的ACK报文携带数据是TCP的捎带应答机制
- 所以我们看TCP不能仅仅看到TCP的可靠性,同样的也要看到TCP在为了提高效率做的努力,例如捎带应答机制,例如一次发送一批数据报文等
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
更多推荐




所有评论(0)