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
回复确认应答、RST
TCP连接出现异常需要强制断开、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_wait
CLOSE_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