文章目录
- 1. 示意图
- 2. 三次握手
- 3. 四次挥手
- 4. 三次和四次问题
- 4.1 为什么三次握手
- 4.2 为什么四次挥手
- 5. 状态变化实验
- 5.1 三次握手实验
- 5.2 四次挥手实验
1. 示意图
Tips:
不管是握手还是挥手,发送的都是完整的
TCP
报头,这不过这些标记位被设置了
2. 三次握手
TCP
可靠性保证里面有一个在建立连接之前需要进行三次握手:
打个比方:
小潘(客户端):小王,我喜欢你,我们在一起吧!(发起请求,第一次握手)
小王(服务端):好呀好呀(做出应答),我也喜欢你,那我们在一起吧!(第二次握手)
小潘(客户端):吼吼吼,我们现在是情侣啦~(做出应答,第三次握手)
- 客户端向服务端发送
SYN
请求建立连接,发送完毕之后状态变为SYN_SENT
同步发送 - 服务端收到请求之后,状态变为
SYN_RECV
同步收到,并做出ACK
确认应答,并捎带SYN
请求建立连接 - 客户端收到应答之后,发送
ACK
,状态变为ESTABLISHED
,表示客户端连接建立完毕;服务端收到ACK
之后,状态变为ESTABLISHED
,服务端连接建立完毕
三次握手时的三个函数:
-
客户端向服务端发起连接调用
connect
函数,建立的本质是connect
要求客户端构建SYN
请求报文发送给服务端connect
函数是负责发起三次握手,握手的过程由双方操作系统自己完成发送
SYN
之后就进入阻塞状态,三次握手完毕才会返回 -
accept
本身并不参与三次握手,只会把建立好的连接拿过来,如果底层没有建立好的连接,会一直阻塞住
三次握手成功之后,调用write
、read
这些系统调用进行通信
本质也不是发送接收数据,将数据写到
TCP
发送缓冲区(将数据从TCP
接收缓冲区读上来)
3. 四次挥手
正常数据通信完毕之后,四次挥手断开连接
打个比方,小潘和小王打视频,嘴巴讲话输出数据流,眼睛看、耳朵听着接收信息
… … 巴拉巴拉巴拉巴拉讲了很久之后 … …
四次挥手断开连接:
小潘:我说完啦(嘴巴不输出了(写端关闭),眼睛可以看、耳朵还在听着(读端未关闭),第一次挥手,发送
FIN
包)小王:好的,但我还没讲完,你听我再说会吧!(确认应答,第二次挥手)
// ... ... //小王继续巴拉巴拉说 // ... ...
小王:现在我也说完啦!(嘴巴不输出了(关闭写端),眼睛可以看、耳朵可以听(读端未关闭),第三次挥手。发送
FIN
包)小潘:点点头,知道双方确认结束视频,准备挂断视频(发送的不是数据流,而是控制信息,第四次挥手)
- 客户端向服务端发送
FIN
,表明没有要发送的数据,要断开连接,进入` - 因为要保证可靠性,服务端向客户端发送
ACK
确认应答表明收到 - 当服务端也没有数据发送之后,向客户端发送
FIN
- 客户端收到之后,发送
ACK
确认应答表明收到
4. 三次和四次问题
事实上TCP
建立连接的时候也是四次握手,只不过将第二次报文被捎带应答了:
对于四次挥手,也可也合并成三次挥手,客户端说我要断开连接啦FIN
(第一次挥手),服务端说好的,我也要和你断开连接ACK+FIN
(第二次挥手),最后客户端说好的,那我们断开连接吧ACK
(第三次挥手)
从最朴素的角度看,三次握手和四次挥手本质是一来一回的一种可靠性,既双方至少可靠的给对方发送了一次消息。
那为什么各种教材或者书籍都写的是三次握手和四次挥手呢?
-
客户端向服务端发送
SYN
建立连接请求,服务端一定会给客户端发送ACK
应答,然后服务端也要和客户端建立连接,因为不存在协商上时间差的问题,所以ACK+SYN
压缩在一起是必然的。因为服务端就是为客户端做服务的,就是等着客户端来连接,所以当客户端发起连接请求的时候,服务端必须无偿同意!这个可不敢比作是男女朋友关系
-
至于四次挥手,这里面
ACK
和FIN
要分开,这是因为有协商的成分在,当客户端要与服务端断开连接说“服务端,我没什么和你要和你说的了,我要断开连接”,服务端收到之后说“可我还有话要给你说啊”,然后服务端将消息发送完毕之后发送FIN
,客户端答复ACK
,此时才真正断开连接所以在一方想断开连接,另一方并不想断开连接,所以想让第二次和第三次挥手压缩成一个,是具有巧合性的!
4.1 为什么三次握手
对于三次握手它能够保证无论是客户端还是服务端在通信之前,双方至少做过一次可靠收发,这叫验证全双工通路是否通畅!
这里至少一次收发,表示的是可靠的收发,如果是2次握手:
这里虽然双方都有一次收和发,但是对于服务端来讲,只表明自己有接收能力,并不知道是否具有发送能力,因为发出去的报文,没有应答!
假设进行一次握手:
客服端发送一个
SYN
请求,服务端就建立一个连接;如果客户端恶意向服务端发送大量SYN
请求,服务端就要建立大量的连接可是服务端维护这些连接是有成本的,服务端需要将这些连接管理起来,这样就十分容易将服务端连接资源打满
假设进行两次握手:
当客户端发送
SYN
请求,服务端收到之后向客户端发送ACK
确认,在发送之后,服务端先将连接建立好,当客户端收到ACK
之后再建立连接。可是如果客户端之间丢弃这个ACK
报文,那这其实是是和一次握手的问题一样。就算这个客户端不是恶意行为,当客户端出现异常时,并没有建立连接,可是服务端还是要将连接维护一段时间。如果有一千万的客户端连接,其中10%的客户端异常了,这就表明服务端需要长时间维护这10%无用的连接。
所以让服务器作异常兜底,是行不通的!
三次握手:
三次握手,第一次握手和第二次握手都是有对于的应答,所以并不担心是否丢失,就算丢失了三次握手未成功,连接还未建立
最担心的就是第三次的应答的丢失,但是对于第三次握手,是客户端发出的,发出之后客户端以为连接建立完毕,当服务端收到之后再建立连接,如果没收到则不建立连接,认为三次握手没有成功。这样就将建立连接失败的成本嫁接到了客户端!
这就能在一定程度上保证服务端的稳定性,既奇数次握手,确保一般情况下握手失败连接成本在客户端!
为什么不是5、7、9次呢?
因为三次是验证全双工的最小次数!
4.2 为什么四次挥手
断开连接的本质是双方没有数据给对方发送,所以必须可靠告诉对方。
四次挥手即双方都能得知对方不想发送消息的意愿,需要协商断开连接
客户端没有消息给服务端发送,可是服务端还是有消息给客服端发送,客户端收到
ACK
之后,还是能收到服务端的消息的(关闭写端不关闭读端)
客户端:发送FIN
之后,状态变为FIN_WAIT_1
服务端:收到之后状态变为CLOSE_WAIT
,发送ACK
客户端:收到之后状态变为FIN_WAIT_2
FIN_WAIT_2
表示不会再给对方发送数据,最后发送的ACK
并不是数据,而是管理报文
服务端:发送FIN
之后,状态变为LAST_ACK
客户端:收到之后状态变为TIME_WAIT
,发送ACK
服务端:状态变为CLOSE
5. 状态变化实验
Tips:
以下实验是基于此篇文章写的
tcp
套接字代码:Linux网络编程——tcp套接字研究的是三次握手和四次挥手,之间的
IO
过程省略采用2台服务器进行测试
#pragma once
#include"Log.hpp"
#include<iostream>
#include<cstring>
#include<sys/wait.h>
#include<unistd.h>
#include<signal.h>
#include<pthread.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"threadPool.hpp"
#include"Task.hpp"
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 1; //不要设置太大
Log log;
enum{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LITSEN_ERR
};
class TcpServer;
class ThreadData
{
public:
ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *t)
:t_sockfd_(fd), t_clientip_(ip), t_clientport_(port), t_tsvr_(t)
{}
public:
int t_sockfd_;
std::string t_clientip_;
uint16_t t_clientport_;
TcpServer *t_tsvr_; //需要this指针
};
class TcpServer
{
public:
TcpServer(const uint16_t &port, const std::string &ip = defaultip)
:listensockfd_(defaultfd)
,port_(port)
,ip_(ip)
{}
//初始化服务器
void Init()
{
//创建套接字
listensockfd_ = socket(AF_INET, SOCK_STREAM, 0); //sock_stream提供字节流服务--tcp
if(listensockfd_ < 0)
{
log(Fatal, "create socket, errno: %d, errstring: %s",errno, strerror(errno));
exit(SOCKET_ERR);
}
log(Info, "create socket success, sockfd: %d",listensockfd_);
// int opt = 1;
// setsockopt(listensockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); //防止偶发性服务器无法进行立即重启
//本地套接字信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
//填充网络信息
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
//bind
int bd = bind(listensockfd_, (struct sockaddr*)&local, sizeof(local));
if(bd < 0)
{
log(Fatal, "bind error, errno: %d, errstring: %s",errno, strerror(errno));
exit(BIND_ERR);
}
log(Info, "bind success");
//tcp面向连接, 通信之前要建立连接
//监听
if(listen(listensockfd_, backlog) < 0)
{
log(Fatal, "listen error, errno: %d, errstring: %s",errno, strerror(errno));
exit(LITSEN_ERR);
}
log(Info, "listen success");
}
void Start()
{
log(Info, "server is running...");
while(true)
{
sleep(1);
//获取新链接
// struct sockaddr_in client;
// socklen_t len = sizeof(client);
// int sockfd = accept(listensockfd_, (struct sockaddr*)&client, &len);
// if(sockfd < 0)
// {
// log(Warning, "accpet 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));
// log(Info, "get a new link..., sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip, clientport);
//根据新链接进行通信
//... ...
//sleep(1);
}
}
void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
{
char buffer[4096];
while(true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_str = "tcpserver echo# ";
echo_str += buffer;
write(sockfd, echo_str.c_str(), echo_str.size());
}
else if(n == 0)
{
log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
break;
}
else
{
log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
break;
}
memset(buffer, 0, sizeof(buffer));
}
}
~TcpServer(){}
private:
int listensockfd_;
uint16_t port_;
std::string ip_;
};
5.1 三次握手实验
运行服务端:
服务端状态:
本主机客户端和其他客户端连接:
查看连接情况:
这里发现连接建立成功和上层是否accpet
无关,是由双方操作系统自主完成
listen第二个参数
将listen
第二个参数设置为1:
这里发现客户端以为连接成功,可是服务端出现了一个SYN_RECV
,并没有三次握手成功。
这是因为listen
第二个参数表明连接的最大个数+1
三次握手成功之后,服务端会在底层建立好连接,不过这个连接可以不拿上去(
accpet
),对于这些建立好的连接没有被拿上去,所以操作系统需要将这里连接维护起来——先描述再组织,采取队列的形式管理这些建立好的连接,叫做全连接队列这队列的最大长度,就是由
listen
第二个参数决定的
我们这里设置的是backlog = 1
,所以连接队列最长为backlog + 1
,没有accept
拿走连接,所以连接2个客户端之后就满了(生产消费者模型)
SYN_RECV
状态变为ESTABLISHED
状态,必须要收到ACK
,可是这里的ACK
已经发送了(因为客户端的状态已经变为ESTABLISHED
状态),但由于listen
第二个参数的设置,服务端连接队列满了,所以将收到的ACK
直接丢弃了服务端并不会长时间维持
SYN_RECV
状态,这叫半连接队列,半连接的节点会隔一段时间被释放掉这个半连接队列也有长度,由内核自己定
真正意义SYN洪水:
要进入全连接队列,首先要先进入半连接队列,虽然说握手失败之后半连接会过一段时间被释放,但是也耐不住恶意请求一直发
SYN
将半连接占满,这就导致正常的连接请求进不来,这才是真正意义上的SYN
洪水
为什么listen
第二个参数不能太长,为什么不能没有?
- 如果全连接队列太长,这就会导致有些连接来不及被上层处理,但还是要被系统长时间维护
来不及处理说明服务器已经很忙了,之后还有新连接到来,然后系统还要分资源出来维护这个队列,所以不能设置太长,这样就能再匀出资源给上层使用
- 如果直接将这个全连接队列删掉,全力支持上层处理,当它空闲的时候,这一部分就又浪费了。
就好比商场的餐饮店,不仅店子里有吃饭完的位置,门口也有椅子,想吃饭的人里面有位置就进去吃,没位置就可以坐在椅子上等一会,当有客人离席的时候,可以立马补上。如果门口没有椅子,里面满了,客人只能站着等,这样用户体验不好,走了,这样就会损失一批客户。以上都是为了服务器资源充分利用
5.2 四次挥手实验
设置服务端获取连接5秒之后断开:
while(true)
{
sleep(1);
//获取新链接
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensockfd_, (struct sockaddr*)&client, &len);
if(sockfd < 0)
{
log(Warning, "accpet 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));
log(Info, "get a new link..., sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip, clientport);
sleep(5);
//关闭
close(sockfd);
log(Info, "close sockfd..., sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip, clientport);
//....
}
现象:主动要求断开连接的一方,在四次挥手完毕之后,会进入TIME_WAIT
状态,等待若干时长之后,自动释放
如果在TIME_WAIT
状态下关闭服务端,然后再重新启动,发现服务起不来:
报错信息:绑定失败
这是因为TIME_WAIT
状态表示连接并没有彻底断开,ip
和port
是正在被使用的,所以服务器挂掉之后无法立即再启动
比如说在节假日高峰期,出行人数非常非常非常多,售票软件没抗住,服务器挂掉了,此时服务器上是存在这大量的
TIME_WAIT
状态的,此时服务器就无法立即重启,这就出事故了。
如果因为TIME_WAIT
问题导致服务器无法立即重启,可以设置setsockopt
,允许地址复用:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd
:文件名描述符
level
:设置层级,一般是SOL_SOCKET
套接字层
optname
:设置选项
optval
:指向一个缓冲区
optlen
:缓冲区大小
//初始化服务器
void Init()
{
//创建套接字
listensockfd_ = socket(AF_INET, SOCK_STREAM, 0); //sock_stream提供字节流服务--tcp
if(listensockfd_ < 0)
{
log(Fatal, "create socket, errno: %d, errstring: %s",errno, strerror(errno));
exit(SOCKET_ERR);
}
log(Info, "create socket success, sockfd: %d",listensockfd_);
int opt = 1;
setsockopt(listensockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); //防止偶发性服务器无法进行立即重启
//本地套接字信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
//填充网络信息
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
//bind
int bd = bind(listensockfd_, (struct sockaddr*)&local, sizeof(local));
if(bd < 0)
{
log(Fatal, "bind error, errno: %d, errstring: %s",errno, strerror(errno));
exit(BIND_ERR);
}
log(Info, "bind success");
//tcp面向连接, 通信之前要建立连接
//监听
if(listen(listensockfd_, backlog) < 0)
{
log(Fatal, "listen error, errno: %d, errstring: %s",errno, strerror(errno));
exit(LITSEN_ERR);
}
log(Info, "listen success");
}
为什么客户端退出之后再连接可以直接连上?
因为客户端的端口是系统随机指定的,每次都会换端口
而服务器的上的某个服务,端口号是固定的
一个报文从客户端发到服务端,这从客户端发出去到服务端收到之前,这个报文都是在网络当中,但是在网络中是有存活时间的,存活最长时间称为MSL
TIME_WAIT
的持续时间是2MSL
:
-
在断开连接的时候,可能历史上还有残留的数据,所以要等这些历史的数据在网络通信当中消散,这一来一回的最大时间就是
2MSL
在准备断开连接的这个时间点,并不是要让对方收到这个数据(因为
tcp
有超时重传、按序到达机制,改补发的早就补发了),而是让对方丢弃这些数据。如果历史残留数据没有消散,在某些情况下,例如又重新连接,采用了同样的ip和端口(极端情况),这样就会影响下一次通信! -
四次挥手的时候,也是要保证挥手成功,前两次挥手双方的连接都还未彻底释放,如果失败还有机会补发;但如果最后一次
ACK
挥手报文丢失,而主动断开连接的一方又立即释放了连接,那对方就会一直处于LAST_ACK
状态,这时候对方就算重新补发FIN
,但是人家已经退了,所以在TIME_WAIT
等待期间,如果ACK
丢失,还能收到对方补发的FIN
,这就能确保四次挥手正常退出
数据在网络当中是毫米级别,但为什么
TIME_WAIT
一般是30s~60s呢?这里分为最大传送时长和最大存在时长。
传输是毫秒级别,但是存在时长(例如在网络中阻塞了)是由系统决定的
cat /proc/sys/net/ipv4/tcp_fin_timeout # 可以自己改