一、TCP
1.1 TCP的包结构
TCP常见的字段有:
- 序列号
seq
- 确认号
ack
- 错误连接
RST
:重置连接(复位) - 窗口大小:接收方的缓冲区大小,进行流量控制
- 请求建立一个连接
SYN
:同步序列号 - 请求关闭一个连接
FIN
:结束连接
TCP四元组:
- 源地址
- 源端口
- 目的地址
- 目的端口
而五元组相比四元组多了协议类型,即TCP或UDP,其中只要五元组有一个不同,就是不同的通信,因此TCP和UDP的端口号是不冲突的,允许有TCP端口号==UDP端口号
其中TCP像三次握手四次挥手、ACK确认和序列号、重传机制、滑动窗口流量控制、拥塞控制等都是保证其可靠传输的重要机制
1.2 TCP的三次握手
1)客户端和服务端建立socket
TCP通信中,客户端创建与服务端的socket连接需要经过以下步骤:
创建客户端socket
connect连接服务端socket 而创建用于监听的服务端socket需要经过以下步骤:
创建服务端socket文件描述符
bind绑定服务端socket
listen监听服务端socket
accept等待接受客户端socket
那么理论上最多能支持多少个连接?
- 总理论连接数=IP地址数端口号数=2^32216=248
但是实际上受文件描述符个数、内存大小、进程数等限制,导致实际连接数远远小于这个数,一般只支持65535个连接
2)TCP的三次握手的状态流转
TCP的三次握手过程如下:
- 第一次握手-客户端:发送
SYN+Seq
请求连接->进入SYN_SENT状态- 随机生成客户端的
Seq
序列号
- 随机生成客户端的
- 第二次握手-服务端:返回
SYN+(ACK=Seq_client+1)+Seq
确认连接->进入SYN_RCVD状态- 随机生成服务端的
Seq
序列号 - 将该客户端的socket信息加入半连接队列
- 随机生成服务端的
- 第三次握手-客户端:发送
(ACK=Seq_server)+Seq
确认连接->进入ESTABLISHED状态- 在ESTABLISHED状态下,如果收到
RST
则进入CLOSED状态
- 在ESTABLISHED状态下,如果收到
- 服务端:接收到
ACK+Seq
确认连接->进入ESTABLISHED状态- 将该客户端的socket信息从半连接队列移入全连接队列
SYN
洪泛攻击:攻击方短时间内发送大量SYN
请求,导致服务端半连接队列满了,无法再接受新的连接,从而导致拒绝服务攻击
解决方法:
- 增大半连接队列
- 开启SYN
Cookies绕过半连接队列,将
SYN
请求的Seq
序列号加密后发送给客户端,客户端返回ACK
时解密后才能建立连接
3)TCP的三次握手的原因
a. 避免历史连接造成资源浪费
如果是两次握手无法阻止历史重复连接的问题:
- 客户端给服务端发送
SYN
请求连接,但是因为网络原因导致服务端在客户端重试发送完后还没收到数据(重传的序列号是一样的) - 服务端只有两次握手,因此在发送ACK后不知道客户端有没有接收到,就直接进入ESTABLISHED状态建立了连接且允许发送数据,此时只有客户端发送RST时该历史连接才能断开
- 此时客户端会发起一个新的连接
- 而在这段时间内由于服务端已经建立连接了,所以已经可以给客户端发消息了,这是一种资源浪费
三次握手的重新连接过程:
两次握手的重新连接过程:
b. 保证通信双方均具备收发能力&&序列号被确认
如果只有两次握手,那么只能保证服务端具备收发能力,而客户端并不一定具备收发能力;同时也不能保证两方的序列号都被确认
4)TCP的三次握手失败分别会发生什么
- 第一次握手丢包:客户端启动重试机制
- 第二次握手丢包:客户端和服务端都会重试
- 第三次握手丢包:服务端会重试
1.3 TCP的四次挥手
1)TCP的四次挥手的状态流转
挥手前客户端和服务端都处于ESTABLISHED状态
TCP的四次挥手过程如下:
- 第一次挥手-客户端:发送
FIN
请求关闭连接->进入FIN_WAIT_1状态- 发送
FIN
并调用close
函数,属于系统调用
- 发送
- 第二次挥手-服务端:接收到
FIN
请求关闭连接->发送ACK
确认->进入CLOSE_WAIT状态- 在CLOSE_WAIT状态下会发送未发完的数据
- 发送
ACK
属于内核操作
- 第三次挥手-服务端:发送
FIN
请求关闭连接,并调用close函数关闭连接->进入LAST_ACK状态- 发送
FIN
并调用close
函数,属于系统调用
- 发送
- 第四次挥手-客户端:收到服务端的
FIN
请求关闭连接->发送ACK
确认->进入TIME_WAIT状态- 在TIME_WAIT状态下会等待2MSL时间,等待可能丢失的ACK确认
- 2MSL是为了保证服务端收到客户端的ACK确认,如果服务端没有收到ACK确认,那么会重传
FIN
请求关闭连接 - TIME_WAIT状态下的socket会等待2MSL时间,然后进入CLOSED状态
- 客户端发送
ACK
确认属于内核操作
- 服务端:收到客户端的
ACK
确认->进入CLOSED状态
2)TCP的四次挥手的原因
TCP挥手不能跟握手一样只有三次,相比握手中服务端将SYN+ACK
合并成一个步骤,而挥手需要分成两个步骤,因为客户端发送FIN
后只是代表不会再发送数据了(还可以接收)
但是服务端可能还有数据没发送完,所以需要将FIN
和ACK
分开,在FIN
和ACK
中间,服务端处于CLOSE_WAIT状态,这个状态内会发送剩余的数据,然后再发送FIN
请求关闭连接
如果只有三次握手,那么服务端发完FIN
后也关闭连接无法发送剩余数据了,会导致这部分数据丢失
3)为什么TIME_WAIT状态要等待2MSL
MSL指的是报文最大生存时间,也就是说如果超过这个时间代表报文消失在网络中了
将TIME_WAIT设置为2MSL也就是允许报文丢失一次
客户端发送ACK到服务端时最大报文时间是1MSL,而当超时消亡时,服务端也发现收不到ACK了,因此会重新发送FIN,又需要最大经过1MSL时间,所以总共是2MSL
当客户端再次收到服务端第二次发的FIN
时TIME_WAIT时间会重新计时(重置为2MSL)
4)TIME_WAIT过多
TIME_WAIT是主动发起挥手方才会出现的状态
a. 客户端TIME_WAIT过多
客户端TIME_WAIT过多是可以通过端口复用解决的,开启tcp_tw_reuse
,客户端会在connect的时候找一个TIME_WAIT超过1s的连接进行复用
所以一般我们都建议通过客户端主动发起挥手,而不是服务端主动发起挥手
b. 服务端TIME_WAIT过多
- 情况1:使用短连接,导致每次服务端响应完后会主动发起挥手断连
- 情况2:长连接满了,最大长连接数设置得过小,导致连接满了,服务端主动关闭已经连接的,来腾出文件描述符给新新连接
5)服务端CLOSE_WAIT过多
CLOSE_WAIT是在服务端返回FIN
且close
关闭了连接时才会转成LAST_ACK状态等待客户端的ACK
确认
因此可以判断CLOSE_WAIT过多可能是因为没有调用close
关闭连接,导致服务端一直处于CLOSE_WAIT状态
1.4 TCP的可靠性
TCP像三次握手四次挥手、ACK确认和序列号、重传机制、滑动窗口流量控制、拥塞控制等都是保证其可靠传输的重要机制
1.4.1 TCP的滑动窗口与流量控制
TCP滑动窗口分为发送端窗口和接收端窗口,主要作用是防止发送方发送过快导致接收方无法接收,并导致接收方被淹没
滑动窗口内的数据就是缓冲区,可以不用等上一个的ACK,连续发送滑动窗口大小的数据
发送窗口swnd
原则上是约等于接收窗口rwnd
的,但是由于发送方维护者拥塞窗口cwnd
,因此发送窗口swnd
的大小实际上是
swnd = min(rwnd, cwnd)
1.4.2 TCP的超时重传与拥塞控制
1.4.2.1 重传
1)超时重传:时间驱动
超时重传就是最朴素的,一个包发送出去后,如果在一定时间内没有收到ACK确认,那么就会重传这个包;
由于是以时间为驱动的,因此可能会出现重传较慢的问题
这里有RTT为数据发送到接收ACK所需的往返时间,而RTO为重传超时时间,一般来说有:
RTO > RTT
2)快速重传:数据驱动
发送方收到3个重复的ACK就会进行快速重传,但是该重传一个呢?还是重传后面所有的包呢?
- 重传一个:如果下一个包也丢失的话,就要再次等3个相同ACK后才会重传
- 重传后面所有的包:如果后面的包没丢失,那么会浪费网络资源
解决办法:用SACK
来解决,即选择性重传,只重传丢失的包
1.4.2.2 拥塞控制
拥塞控制一般要经过以下步骤:
- 慢启动(窗口大小指数上升),直到到达慢启动门限
- 拥塞避免(窗口大小线性上升)
- 拥塞发生(更新慢启动门限、更新拥塞窗口)
- 方法1:超时重传
- 将慢启动门限设置为拥塞窗口的一半
- 将拥塞窗口设置为1,会导致发送速度断崖式变慢
- 从慢启动开始指数增长
- 方法2:快速重传
- 将慢启动门限设置为拥塞窗口的一半
- 将拥塞窗口设置为跟慢启动门限
- 从拥塞避免开始线性增长
- 方法1:超时重传
1.5 TCP的拆包和粘包
1.5.1 拆包
一个网络包的最大大小为MTU,一般为1500字节
网络包中除开IP头和TCP头外外的数据包称为MSS,一般为1460字节,MSS中也包含了HTTP整个协议的头和content等内容
MTU = IP头 + TCP头 + MSS
由于要求数据的最大大小为MSS,因此如果数据包大于MSS,那么就会拆包,将数据包拆分成多个包发送(也就是应用层的一个数据包被拆成多段进行发送)
1.5.2 粘包
TCP有一个Negle算法,会将多个小包合并成一个大包发送,这样可以减少网络包的数量,提高网络的利用率
但是由于TCP基于字节流是没有数据边界的,因此可能会出现粘包问题
解决办法是在应用层加一些边界(如HTTP、HTTPS协议)
- 定长包:每个包的长度是固定的,不足的用空格填充
- 特殊字符隔离:在每个包的末尾加上特殊字符,如
\r\n
二、TCP和UDP
2.1 TCP和UDP的区别
1)可靠性上
- TCP:面向连接的协议,提供可靠传输,通过序列号和ACK确认机制保证数据的可靠传输
- UDP:无连接的协议,提供不可靠传输
2)传输方式上
- TCP:字节流传输,包没有边界(存在拆包粘包问题)
- UDP:数据报传输,一个一个包发送的,包之间天然有边界
3)传输效率上
- TCP:由于需要等待连接、等待重传、等待ACK确认等确保可靠性的机制,因此传输效率比UDP低
- UDP:传输效率高
4)应用场景
- TCP:适用于要求可靠传输的场景,如文件传输、HTTP、HTTPS等
- UDP:适用于实时性要求高的场景,如视频直播、语音通话、DNS等
- 如果视频用UDP,丢失数据只会丢失某一瞬间的画面,而TCP会等待重传,导致卡顿甚至卡死