1、TCP前置知识
1.1什么是TCP
- TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接:必须是一对一建立连接后才能通信可靠的:无论网络链路出现怎么样的变化,TCP可以保证报文一定能被对端收到字节流:流式协议,不带边界属性,接收端无法直接确认一组有效的用户信息,且TCP报文是有序的
UPD和TCP的区别
- TCP要面向连接,UDP不需要连接,即刻传输数据
- TCP是一对一服务,UDP支持一对一,一对多,多对多的交互通信
- TCP有拥塞控制、流量控制保证数据传输安全,UDP没有拥塞控制,不限流,即使网络拥堵,也不会影响UDP的发送速率
- TCP流式协议,没有边界,但保证顺序和可靠,UDP是一个包一个包发的,有边界,但是会丢包和乱序
- UDP实时性好,适用于游戏领域,TCP延迟ACK
1.2、TCP状态图

- 三次握手状态:
CLOSED:关闭状态,一般在四次挥手之后的状态SYN_SENT: 返送连接请求后,表示已经发送连接请求ESATABLISHED: 客户端收到SYN+ACK、或者服务端收到ACK后,表示已经建立TCP连接,可以开始通信了SYN_RECV: 服务端收到客户端的SYN连接请求后,此时需要向客户端也发起连接请求并响应回复SYN+ACKLISTEN: 服务端开启侦听之后,可以接收客户端发起的连接请求
- 四次挥手状态:
FIN_WAIT1: 客户端发送FIN报文之后,客户端状态CLOSE_WAIT: 服务器接收FIN报文之后,服务器状态FIN_WAIT2: 客户端接收服务器对FIN报文的应答之后客户端状态TIME_WAIT: 客户端接收到服务器FIN报文且做出应答ACK之后,客户端状态LAST_ACK: 服务器发送FIN报文后,服务器状态CLOSED: 服务器接收到客户端ACK报文后的状态,客户端TIEM_WAIT结束后的状态
1.3 TCP报文格式

- 源端口和目标端口就是从哪个应用发给哪个应用(断开用于标识应用)
- 序号
seq: 用于标识TCP发送出去的字节流,解决包乱序 - 确认号
ack:对已收到包进行确认,ack=seq+1,解决丢包问题 - 标志为:
SYN发起连接、ACK回复确认应答、RSTTCP连接出现异常需要强制断开、FIN表示后面不再发送数据,希望断开连接、URG紧急指针有效、PSH接收方应该尽快将这个报文交给引用层,TCP是面向连接的,双方通过TCP报文修改各自的TCP状态 - 窗口大小:通信双方要标识窗口大小,表示当前自己能接收多少数据,用于流量控制
2、从connect到三次握手
2.1、从connect到三次握手建立连接

- 一开始客户端和服务端都处于
CLOSED状态。 - 服务端先要开启侦听,TCP状态从
CLOSED变成LISTEN,可以开始接收连接进行三次挥手开启侦听流程: int listenfd = socket() -> bind(listenfd,ip+port) -> listen(listenfd,backlog)
- 第一次握手:客户端
connect()系统调用之后,从用户态切换到内核态,TCP协议栈组织三次握手包,向服务器发送SYN报文连接,客户端从CLOSED状态进入SYN_SENT - 第二次握手:服务器收到
SYN连接,并返回客户端ACK+SYN回复请求并也发起连接,服务器TCP状态从LISTEN转变到SYN_RECV,在这里会有一个半连接队列(SYN队列),存放着当前客户端和服务器的TCP连接 - 第三次握手:客户端收到
ACK+SYN,并对服务器报文返回ACK,客户端TCP状态从SYN_SENT转变为ESATABLISHED,服务器接收到ACK,TCP状态从SYN_RECV转变为ESATABLISHED,三次握手连接建立完成,服务器这里有个全连接队列(accept队列),将半连接队列中的TCP连接节点加入到全连接队列 - 客户端处于
ESATABLISHED时,connect返回0 表示连接成功,服务器处于ESATABLISHED时,accept从全连接队列中取出一个TCP连接节点,并给这个连接节点分配一个clientfd用于和客户端通信
2.2 TIP:
-
accept发生在三次握手完成后,TCP连接节点在全连接队列,accept从全连接队列中取出一个TCP连接节点,并给这个连接节点分配一个clientfd用于和客户端通信; -
listen函数的第二个参数backlog=半连接队列数+全连接队列数, -
listen和bind为什么要单独设计? 接口越单一,后续的可调度性和可重复利用的概率越高 -
bind为什么绑定ip? 一台机器一般有多个网卡,一个网卡对应一个ip -
每次标志位携带
ACK,就是对接收报文的应答,对应的确认号ack为接收到报文的序列号seq+1,表示自己已经就受到报文 -
第一次握手丢失:
- 服务器没有对
SYN报文进行应答(可能是服务器没有收到),客户端会进行5-6次SYN(每次重传的seq是一样的),每隔一段时间进行尝试一次,直到超时返回-1,errno为ETIMEOUT,connect连接建立失败
- 服务器没有对
-
第二次握手丢失:
- 客户端没有收到
ACK,会觉得第一次握手丢失,重传SYN - 客户端不会对服务器的
SYN+ACK报文进行应答,服务端收不到三次握手,服务端也会每隔一段重发SYN+ACK(重复五次),并且等待一段时间(SYN Timeout)之后丢弃这个没有完成的连接(断开连接)
- 客户端没有收到
-
第三次握手丢失:
- 客户端发起第三次握手,进入
ESATABLISHED状态 - 服务端接受不到第三次握手,会触发重传机制,直到收到第三次握手,或者是超时断开连接
- 就算第三次握手的
ACK丢掉了,只要客户端发送数据,服务器就会进入ESATABLISHED状态, 因为数据包中的ack和第三次握手的ack相同都是对第二次握手的确认
- 客户端发起第三次握手,进入
-
SYN Flood攻击: 大量客户端没有回应ack导致 半连接队列中的TCP连接溢出, 后续建立连接的请求会被内核丢弃
-
如果避免SYN Flood攻击:
增大TCP半连接队列;开启tcp_syncookies:可以在不使用syn半连接队列的情况下成功建立连接,当syn半连接队列满了之后,后续的SYN包不会丢失,会根据算法计算出一个cookie值,cookie值会放到第二次握手的报文里面,服务器在收到ACK的时候会检查包的合法性,如果合法就放到accept全连接队列中- 减少
SYN+ACK重传次数:加快断开处于SYN_RECV状态的TCP连接
-
前两次握手不可以携带数据,第三次握手可以携带数据,因为客户端握手后,就进入了
ESATABLISHED状态 -
为什么是三次握手:- 为了防止旧的重复连接初始化造成混乱,客户端泵机,网络拥塞,导致第一个
SYN报文没有送到, 客户端重启会重传SYN报文,但是拥塞的SYN报文会先到,而每次SYN报文序列号不一样,服务端对旧 SYN回复SYN+ACK, 客户端会直接回复RST, 服务端收到RST会释放旧连接,客户端只会对新的SYN+ACK报文进行ACK, 服务端收到ACK双方连接建立成功 - 为什么两次握手无法解决历史
SYN问题,服务端对旧连接的建立会浪费资源:如果服务端在一次握手的时候进入ESATABLISHED状态,说明连接建立(分配socket文件句柄), 服务端可以对客户端收发数据,但是服务端不知道这个是历史连接,在收到客户端会回复的RST前,服务端可以发送数据,从而导致资源浪费, 服务端收到RST才知道是历史连接从而断开连接(释放sock文件句柄,资源浪费),如果多次由于网络阻塞导致的历史SYN, 会导致服务器资源严重浪费 - 三次握手可以保证双方序列号同步,服务端
ACK确认客户端序列号,客户端ACK确认服务端序列号,而序列号可以保证TCP传输有序性的关键 - 三次握手就可以建立可靠的连接,四次握手没有必要
- 为了防止旧的重复连接初始化造成混乱,客户端泵机,网络拥塞,导致第一个
2、从close到四次挥手
2.1 前置关于close和shutdown
close:- 减少socket文件描述符的引用,当引用为0的时候,就会关闭连接(socket读写通道,并触发四次挥手发送FIN报文),收回socket文件句柄
shutdown:- 可以只关闭socket读通道(SHUT_RD),或者只关闭写通道(SHUT_WR),或者同时关闭读写通道(SHUT_RDWR),调用完shutdown之后还需要调用close关闭socket文件句柄,但是shutdown没有资源计数的概念,会直接影响所有的sockfd
shutdown(sd, SHUT_WR):关闭socket写通道,触发四次挥手,发送FIN报文给对端,告诉对端我不再发数据了,但是可以读;shutdown(sd, SHUT_RD):关闭socket读通道,TCP协议栈不给对端发送网络消息,本端进入read操作会返回错误shutdown(sd, SHUT_RDWR): 同时关闭读写通道,也会触发四次挥手,TCP发送FIN报文
2.2 从close到四次挥手断开连接
四次挥手的客户端和服务器是相对的,先断开连接,谁就是客户端
- 前提socket文件描述符的引用只有1,客户端
close, 关闭连接, 回收句柄资源,触发TCP四次挥手 - 第一次挥手:客户端调用
close触发四次挥手,TCP协议栈发送FIN报文, 告诉服务端没有数据要发了,客户端进入FIN_WAIT1状态,等待服务端应答 - 第二次挥手:服务端接收到客户端的
FIN报文,向客户端发送ACK报文应答,进入CLOSE_WAIT状态,TCP协议栈会给FIN报文插入EOF文件结束符,触发读事件,服务器read的时候会返回0,从而知道对端关闭连接,客户端收到ACK报文,进入FIN_WAIT2状态,等待服务端第三次挥手(FIN报文) - 第三次挥手:服务器调用
close(),也组织FIN报文发给客户端,服务器进入LAST_ACK等待客户端的最后的挥手ACK - 第四次握手:客户端收到服务器的
FIN报文,发送最后挥手应答ACK,进入TIME_WAIT状态,服务器接收到ACK进入到CLOSED状态,服务端已经完成连接关闭,客户端需要等待2MSL进入CLOSED状态,这样客户端也完成连接关闭
2.3 TIP
-
ACK是TCP协议栈回复出去的,close是应用程序调用的,close的调用权在应用层 -
为什么要四次挥手:
- 客户端向服务器发送
FIN报文,表示的是不在发送但是还可以接收数据 - 服务器回复
ACK之后可能还需要处理数据和发送,等服务器不需要发送的时候,才给客户端发送FIN指令,表示同意关关闭连接 - 由于服务端可能还需要等待处理没有完成的数据,以至于服务端回复的
ACK和FIN是分开的,因此需要四次挥手
- 客户端向服务器发送
-
四次挥手可以是三次嘛?:
- close粗暴关闭:客户端读写对端全部关闭,服务端发送发
ACK之后,还想发送数据,客户端协议栈会发送RST报文,服务器会直接关闭连接 - shutdown优雅关闭:会经历完整的四次挥手,只关闭写,不关闭读,服务器在
CLOSE_WAIT状态还是可以给对端发送数据的,客户端收到了会回复ACK, 当服务器数据处理完成没有要发的,就会调用close关闭连接挥手资源,发送FIN报文 - TCP延迟确认机制(默认开启)会导致第二次握手和第三次握手合并,从而出现三次握手
- close粗暴关闭:客户端读写对端全部关闭,服务端发送发
-
第一次挥手丢失:
- 第一次挥手客户端向服务器发送
FIN报文,并进入FIN_WAIT1状态,如果客户端收不到服务器的第二次挥手ACK, 客户端会重传FIN报文,每隔一段事件重传一次,超时没有收到ACK,客户端直接进入CLOSED状态
- 第一次挥手客户端向服务器发送
-
第二次挥手丢失:
- 服务器发送
ACK, 进入CLOSE_WAIT状态,服务器发送的ACK丢失, 客户端收不到ACK会每隔段不停重传FIN,超时客户端直接进入CLOSED状态
- 服务器发送
-
第三次挥手丢失:
对于服务器而言:- 服务器发送
FIN,进入LAST_ACK,FIN丢失,服务器收不到ACK, 服务器就会重发,超时断开连接
- 服务器发送
对于客户端而言:- close状态下:客户端收到
ACK之后进入FIN_WAIT2状态,这个状态只会维持60s, 超时没有等到服务器的FIN, 客户端直接关闭连接进入CLOSED状态 - shutdown只关闭写通道状态下:还是可以读数据的,客户端会一直处于
FIN_WAIT2状态死等服务器的FIN报文
- close状态下:客户端收到
-
第四次挥手丢失:
- 客户端发送
ACK,进入TIME_WAIT状态, 如果服务器没有收到ACK, 依旧处于LAST_ACK状态,会重发FIN报文,如果四次挥手一直丢失,则服务器超过重传次数,就会直接关闭连接进入CLOSED状态,客户端在维持TIME_WAIT状态消失后也会进入CLOSED状态 - 客户端的
TIME_WAIT状态就是为了防止ACK丢失
- 客户端发送
-
TIME_WAIT状态:- 处于
TIME_WAIT的客户端每次收到FIN指令都会重新发送ACK并重新计时 - 可以确保服务端,正确的关闭,也就是防止
ACK丢失 - 防止旧连接数据,被新连接接收,新旧连接在相同端口
- 缺点: 资源被占用、端口被占用
- 处于
-
出现大量
close_waitCLOSE_WAIT处于服务端返送完ACK之后到close之前,主要问题是没有及时close,可能是由于业务逻辑写的不对,close前面存在耗时操作
-
出现大量
TIME_WAIT- 服务端主动断开很多TCP连接才会出现大量
TIME_WAIT - 一般出现在,大量HTTP短链接,服务器主动断开连接
- 服务端主动断开很多TCP连接才会出现大量
参考文章:
https://xiaolincoding.com/network/3_tcp/tcp_interview.html#tcp-%E5%A4%B4%E6%A0%BC%E5%BC%8F%E6%9C%89%E5%93%AA%E4%BA%9B







![[个人笔记] vCenter设置时区和NTP同步](https://img-blog.csdnimg.cn/0ad0291e25144b7bab5ed47572f14c75.png)












