深入TCP连接
参考资料:https://mp.weixin.qq.com/s/rX3A_FA19n4pI9HicIEsXg
1.TCP基本认识
(1)TCP头部结构
下面介绍其中较为重要的部分:
- 序列号:建立连接时计算生成的随机数作为其初始值,通过SYN包传给接收端主机。每发送一次数据就会累加一次该数据字节数的大小,所以可以用来解决网络包乱序的问题
- 确认应答号:即下一次期望收到的数据序列号,发送端接收到这个确认应答就可以认为这个序号之前的数据已被正常接收,用以解决不丢包的问题
- 控制位(状态位):该字段中的每个比特分别表示以下通信控制含义
- ACK:表示接收数据序号字段有效,一般表示数据已被接收方收到
- RST:强制断开连接,用于异常中断的情况
- SYN:发送方和接收方相互确认序号,表示连接操作
- FIN:表示断开连接
- 窗口大小:接收方告知发送方窗口大小(窗口大小即缓存大小,标识当前处理能力,用于流量控制,拥塞控制)
(2)TCP基本定义
TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议
- 面向连接:十分专一,只支持一对一连接
- 可靠的:不管网络链路出现怎样的链路变化,TCP都可以保证一个报文一定能到达中断
- 字节流:消息无论多大都可以传输,而且一定是有序的,顺序不对的,重复的报文会被自动丢弃
TCP连接即用于保证可靠性和流量控制维护的某些状态信息包括:
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
TCP四元组可以唯一确定一个连接:
源地址
源端口
目标地址
目标端口
地址储存在IP头部中,作用是通过IP协议发送报文给对方主机
端口存储在TCP头部,作用是告诉TCP协议应该把报文发给哪个进程
服务器通常固定在某个本地端口监听(如80,443等),等待客户端的连接请求
TCP理论上的最大连接数与客户端的IP数和客户端的端口数有关
$$
最大TCP连接数 = 客户端的IP数 × 客户端的端口数
$$
当实际上服务端最大TCP连接还会受到文件描述符和内存限制
(3)TCP与UDP的区别
TCP与UDP的结构对比:
- 首部长度:TCP有首部长度,UDP则没有。因为TCP有可变长的选项字段,所以需要首部长度来记录;UDP的头部长度是不会变化的,所以需要去记录首部长度
- 包长度:UDP有首部长度,UDP则没有。TCP和UDP的数据长度都可以通过下面公式计算出来,所以包长度是不需要的,这里的UDP可能纯属是补全UDP的首部长度为4字节
$$
TCP/UDP数据长度 = IP总长度 - IP首部长度 - TCP/UDP首部长度
$$
TCP和UDP协议的应用:
TCP/UDP比较:
咳咳,正经详细的比较如下:
TCP/UDP编程模型对比:
2.TCP连接建立
(1)三次握手
TCP三次握手其实就是建立一个TCP连接,客户端和服务器之间需要3个数据包,握手的主要作用就是为了确认双方的接收和发送能力是否正常,初始序列号,交换窗口大小以及 MSS 等信息
SYN:发送方和接收方相互确认序号,表示连接操作; ACK:表示接收数据序号字段有效,一般表示数据已被接收方收到
- 初始状态下,客户端和服务端都处在CLOSED状态,先是服务端主动监听某个端口,处于LISTEN状态
- 第一次握手:客户端发送SYN报文,并进入SYN-SENT状态,等待服务器确认
- 第二次握手:服务器收到SYN报文后,需要向客户端发送ACK确认收到的报文;同时服务端也向客户端发送一个SYN报文(也就是说服务端向客户端发送了SYN+ACK报文),然后服务端进入SYN_RCVD状态
第三次握手:客户端收到SYN+ACK报文后,向服务端发送ACK确认收到的报文,客户端进入ESTABLISHED状态; 服务端收到客户端的ACK包后也会进入ESTABLISHED状态,完成三次握手
第三次握手是可以携带数据的,前面两次握手是不可以携带数据的,完成三次握手后,双方都处于ESTABLISHED状态,至此连接就已经建立完成了
在Linux系统可以通过netstat -napt
命令查看TCP连接状态
(2)需要三次握手的原因
我们需要三次握手才能初始化Socket,序列号和窗口大小并建立TCP连接,才能保证双方具有接收和发送的能力
需要三次握手的原因如下:
- 三次握手才能阻止历史重复连接的初始化
- 三次握手才能同步双方的初始序列化
- 三次握手才可以避免资源浪费
避免历史连接:防止旧的重复连接初始化造成混乱
网络环境错综复杂,在网络拥堵的情况下,一个「旧 SYN 报文」可能会比「最新的 SYN 」 报文早到达了服务端
第三次握手可以用来判断是否收到了自己期望的ACK:
- 如果是历史连接(序列号过期或超时)就发送RST报文中止历史连接
- 如果不是历史连接则第三次发送的报文是ACK报文,通信双方就会成功建立连接
如果是两次握手将无法判断出历史连接
同步双方初始序列号:TCP协议通信双方,都必须维护一个序列号,只有客户端和服务端之间通过SYN和ACK一来一回的确认,才能确保双方的初始化序列号能被可靠的同步
如果是两次握手只能保证一方的初始序列号可以被对方接收,没办法保证双方的初始序列号都能被确认接受
避免资源浪费:二次握手会建立多个冗杂的无效的连接,造成不必要的资源浪费
两次握手会造成消息滞留的情况,服务器接收到无用的SYN报文,因为没有ACK确认信号,服务器就会造成重复的资源分配
(3)初始化序列号ISN
客户端和服务端的初始序列号ISN是不相同的,因为网络的报文会延迟,会复制重发,也可能丢失。为了避免相互影响,客户端和服务端的初始序列号是随机且不同的
初始序列号ISN的随机生成算法是基于时钟的,如下
$$
ISN = M + F (localhost, localport, remotehost, remoteport)
$$
M
是一个计时器,这个计时器每隔 4 毫秒加 1F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择
(4)MTU与MSS
IP层分片的大小为MTU,TCP层分段的大小为MSS
MTU
:一个网络包的最大长度,以太网中一般为1500
字节。MSS
:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度
既然IP层可以将大于MTU数据进行分片,为什么还需要TCP层将大于MSS的数据分段呢?
因为IP层本身并没有超时重传机制,如果一个IP分片丢失了,那么整个IP报文的所有分片都要进行重传
所有为了达到最佳的传输效能,TCP协议在建立连接时通常要协商双方的MSS值,当TCP发现数据超过MSS时就会对数据进行分段,这样它生成的IP包长度就不会大于MTU了,当然也不需要IP层进行分片。
如果一个TCP分片丢失,就可以以MSS为单位重发数据
(5)TCP接收队列
在TCP三次握手中,我们如何分辨:哪些连接是半连接,哪些连接是全连接呢?
Linux通过维护两个队列来解决问题:
- 半连接队列(SYN队列)
- 全连接队列(accepet队列)
- 服务端收到客户端发起的SYN后,内核会将连接存储到半连接队列
- 服务端向客户端发送SYN+ACK
- 客户端收到SYN+ACK后,发送ACK到服务端
- 服务端收到客户端的ACK后,内核会把连接从半连接队列移除,将其添加到全连接队列,等待进程调用accept函数时把连接取出来
- 不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包
(6)SYN攻击与避免方式
SYN攻击实际上就是对服务端一直发送SYN包,但是不回第三次握手ACK,这样会使服务端有大量的处于 SYN_RECV
状态的 TCP 连接(即半连接状态),久而久之就会导致TCP半连接队列溢出
避免SYN方法如下
方法一:调整Linux相关参数
通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理
- 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:
1 | net.core.netdev_max_backlog |
- SYN_RCVD 状态连接的最大个数:
1 | net.ipv4.tcp_max_syn_backlog |
- 超出处理能时,对新的 SYN 直接回 RST,丢弃连接:
1 | net.ipv4.tcp_abort_on_overflow |
方法二:开启tcp_syncookies
当syncookies=1时,服务端开启 syncookies 功能,其可以在不使用 SYN 半连接队列的情况下成功建立连接(默认配置)
SYN队列即半连接队列 Accept队列即全连接队列
syncookies的运行原理:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功。其处理流程如下:
- 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」
- 计算出一个
cookie
值,再以 SYN + ACK 中的「序列号」返回客户端 - 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」
- 最后应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出的连接
3.TCP连接断开
(1)四次挥手
当我们的应用不再需要数据通信,就会发起断开TCP连接,建立一个连接需要三次握手,而终止一个连接需要经过四次挥手
SYN:发送方和接收方相互确认序号,表示连接操作; ACK:表示接收数据序号字段有效,一般表示数据已被接收方收到; FIN:表示断开连接
- 第一次挥手:客户端发起FIN包,客户端进入FIN_WAIT_1状态(虽然FIN包不携带数据,也需要消耗一个序号u)
- 第二次挥手:服务端收到FIN包,发出确认包ACK(ack=u+1),并带上自己的序号seq=v,服务端进入CLOSE_WAIT状态(这个时候客户端仍需要接收服务器发送的数据);客户端接收到服务端发送的ACK后,进入FIN_WAIT_2状态
- 第三次挥手:服务端数据发送完毕后,向客户端发送FIN包(seq=w,ack=u+1),半连接状态下服务器可能又发送一些数据,服务端此时进入LAST_ACK状态
- 第四次挥手:客户端收到服务端的FIN包后,发出确认包ACK(ACK=1, ack=w+1),此时客户端进入TIME_WAIT状态;服务端收到客户端确认包后进入CLOSED状态,而客户端需要等待2MSL后才进入CLOSED状态
四次挥手的本质是——客户端和服务器通过两对FIN-ACK报文通知对方自己要关闭了
三次握手中,在第二次握手时,接收端将一个ACK包和一个SYN包合并一起发送,所以减少了一次包的发送
四次挥手中,在主动关闭方(客户端)发送FIN包后,接收方(服务端)可能还要发送数据,不能立即关闭数据通道,所以服务端要先确认ACK,然后等到自己把数据发无可发后再发送FIN包
(2)需要四次挥手的原因
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接
服务端的ACK和FIN需要分开发送,是因为通常需要等待完成数据的发送和处理,所以会比三次握手多一次
(3)TIME_WAIT状态功能
为什么客户端在关闭连接时需要一个TIME_WAIT的状态呢?
其主要有以下两个原因:
- 防止旧连接的数据包
- 保证「被动关闭连接」的一方能被正确的关闭
防止旧连接的数据包:
如果没有TIME_WAIT或TIME_WAIT的时间过短,那么图中被延迟的过期的数据包可能会被客户端正常接收
而经过TIME_WAIT的2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
保证连接正确关闭:
TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
一旦客户端最后的ACK报文在网络丢失,如果没有TIME_WAIT或TIME_WAIT的时间过短,客户端很快就会进入了则就直接进入了 CLOSE
状态了,那么服务端则会一直处在 LASE-ACK
状态。这时如果客户端要发起新的连接,服务端会发送 RST
报文给客户端,连接建立的过程就会被终止
如果 TIME-WAIT 等待足够长时,一旦服务端没有收到四次挥手的最后一个 ACK
报文时,则会重发 FIN
关闭连接报文并等待新的 ACK
报文
(4)TIME_WAIT深入讲解
为什么TIME_WAIT等待的时间是两秒?
MSL:报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
TTL: IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机
如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 MSL
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时
TIME_WAIT 过多有什么危害?
过多的 TIME-WAIT 状态主要的危害有两种:
- 第一是内存资源占用;
- 第二是对端口资源的占用,如果TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接
TIME_WAIT 优化
- 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项:可以复用复用处于 TIME_WAIT 的 socket 为新的连接所用,引入时间戳后,重复的数据包会因为时间戳过期而被自然丢弃,同时当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉
- net.ipv4.tcp_max_tw_buckets:这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置
- 程序中使用 SO_LINGER ,应用强制使用 RST 关闭(也是一个危险行为,不推荐使用)
(5)TCP保活机制
如果已经建立了连接,但是客户端突然出现故障了怎么办?
这时候就要提到TCP的保活机制了
在一个规定的时间段(tcp_keepalive_time:保活时间)内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔(tcp_keepalive_intvl:每次检测间隔),发送一个「探测报文」,该探测报文包含的数据非常少,如果连续几个探测报文(tcp_keepalive_probes:检测次数)都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序
相关参数如下:
1 | cat /proc/sys/net/ipv4/tcp_keepalive_time |
按照系统默认的设置来计算在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接
开启了TCP保活可以考虑以下三种情况:
- 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来
- 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置
- 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡
前方施工中。。。
深入TCP机制
参考资料:https://mp.weixin.qq.com/s/HjOUsKn8eLfDogbBX3hPnA
1.重传机制
2.滑动窗口
3.流量控制
4.拥堵控制
TCP关键参数与优化
参考文章:https://mp.weixin.qq.com/s/ytV7RZSyFXyvPW_lKhv8hw