TCP协议详解(2)
连接管理机制(三次握手和四次挥手)
==三次握手==
明白了三次握手之后!我们要谈一下TCP为什么要连接!——我们都说TCP面向连接所以可以保证可靠性!
那么为什么把连接建立好就可以保证可靠性呢?——因为TCP可以超时重传?确认应答?流量控制?
面向连接这件事本身是不能直接保证可靠性的!——面向连接本质就是交换几个报文创建一个结构体而已!
是间接保证可靠性的!——那么怎么间接保证可靠性呢?TCP是如何知道那个报文丢了?那个报文处于新建状态?连接状态?还是断开的状态?那些报文丢失还要重传,重传下一次的是多长?等等问题
==刚刚我们说的这一些特征,都是要维护在TCP的连接的结构体里面的!——正是因为有了三次握手的机制!所以给双方都形成了连接结构体的共识,这是因为有了连接结构体!才能更好的去完成超时重传!流量控制!确认应答等的数据基础!——三次握手是创建连接结构体的基础!所以三次握手间接保证了可靠性!==
==四次挥手==
为什么要四次挥手
这就是四次挥手的基本的样子!——==四次挥手也有可能变成三次挥手!——例如中间ACK+FIN这两个报文可以合起来!==
TCP状态转换
四次挥手状态的变化
在四次挥手期间任何一方都可能会断开连接!客户端或者服务端都可能!
==我们如何让服务端一直保持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_;
};
}
最后主动关闭连接的一方确实也进入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_;
};
}
==因为此时我们是主动的关闭服务器!——也就是说服务器是主动断开连接的一方!——那么这时候这个连接就会进入TIME_WAIT状态!==
==我们发现我们后面无论如何再进行bind,都会失败!==
那么这个问题有什么危害呢?实际爆发的场景有什么呢?
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
发送方怎么在第一次就知道对方的接收能力呢?——==我们在通信之前,早就做过了三次我握手!双方已经交换过报文了!TCP报文里面就有一个16位窗口大小,这样子双方就已经知道了对方接收缓冲区的大小了!==
滑动窗口
而我们上面谈论过确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段. 这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时 间重叠在一起了)
窗口一定会向右滑动吗?可以向左滑动么?
一个滑动窗口的左侧是已经发送,并且已经确认的报文!
如果start下标往左移动!那么会让已经发送且确认的数据在要求被确认一次!这是不合理的!——所以滑动窗口是不能也一定不会左移的!
如果接收方的上层一直不拿走接收缓冲区里面的数据!随着接收数据的变多,那么返回回来的16位窗口大小就会越来越小!——而滑动窗口就会相应的变小就是win_start下标一直++,win_end下标不变!==win_end下标长期处于不动的状态!==
==所以滑动窗口一定会向右移动吗?——不一定!可能向右移动,也可能会保持不动!==
窗口一定会一直不变吗?会变大吗?会变小吗?如果会变,确认的依据是会什么?
==窗口会一直不变吗?——肯定不会!他是浮动的!完全取决于对方的容量大小!可能不变!但是不会一直不变!==
会变大么?会变小吗?——都可能会!看服务器上层的处理速度!如果处理速度慢导致了拿取数据的速度,慢与接收数据的速度!那么窗口变小!
如果处理速度快!拿去数据的速度快与接收数据的速度!那么窗口变大
收到应答确认的时候,如果不会最左侧发送的报文的确认,而是中间的,结尾的怎么办?要滑动吗?
我们上面说的都是理想情况!但是万一我们收到是中间的或者结尾的报文呢?
因为我们接收方为了保证可靠性是会对接收到的报文进行排序!——如果我们接收到了不是最左侧报文的确认!==那么就只有一种可能丢包!==
==我们可以看到确认序号是十分的重要的!——除了告诉对的起始窗口之外!还是支持滑动窗口的滑动规则的指定!==
所以回到我们开始的问题:如果不会最左侧发送的报文的确认,而是中间的,结尾的怎么办?要滑动吗?
滑动窗口必须要滑动吗?会不会不动了呢?或者变为0
从上面的情况我们可以看出!不是必须要滑动的!——最极端的情况!我们发出去的全部报文都丢失!那么滑动窗口也只能不动了!等待超时重传!
可能会不动!也可能变为0
会一直向后滑动吗?如果空间不够了怎么办?
注意我们上面说过滑动窗口的大小 = 对方通知给我的字节接收能力的大小这个概念是不准确的!(或者说只正确一半)
我们上面说的所有策略,例如:超时重传,连接管理,丢包重传,按时重传,去重,滑动窗口,流量控制——目前所有的策略都是端到端的!
什么意思呢?就像是我们上面说的滑动窗口!是客户给服务器的!或者是服务器给客户的!——==是限定在你的主机与我的主机两个主机之间!==
==可是丢包的时候!除了接收方出问题!网络也可能出现问题!——也就是说如果中间这一部分出现问题了该怎么办?==