计算机网络(TCP篇)
TCP 和 UDP
TCP 基本认识
TCP 头格式
TCP
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
TCP 连接
连接定义
TCP 四元组
TCP 四元组可以唯一的确定一个连接。
最大 TCP 连接数
对 IPv4,
- 客户端的 IP 数最多为 2 的 32 次方
- 客户端的端口数最多为 2 的 16 次方
服务端单机最大 TCP 连接数约为 2 的 48 次方。
服务端最大并发 TCP 连接数远不能达到理论上限,受文件描述符限制(系统级、用户级、进程级)、文件描述符限制因素影响。
TCP缺陷
-
TCP 协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。
内核升级涉及到底层软件和运行库的更新,服务程序就需要回归测试是否兼容新的内核版本,所以服务器的内核升级比较保守和缓慢。
-
基于 TCP 实现的应用协议,都是需要先建立三次握手才能进行数据传输,比如 HTTP 1.0/1.1、HTTP/2、HTTPS。
- TCP 三次握手的延迟被 TCP Fast Open (快速打开)这个特性解决了,这个特性可以在「第二次建立连接」时减少 TCP 连接建立的时延。
- TCP Fast Open 需要服务端和客户端的操作系统同时支持才能体验到,很难被普及开来。
-
现在大多数网站都是使用 HTTPS 的,这意味着在 TCP 三次握手之后,还需要经过 TLS 四次握手后,才能进行 HTTP 数据的传输,这在一定程序上增加了数据传输的延迟。
针对 HTTPS 来说,TLS 是在应用层实现的握手,而 TCP 是在内核实现的握手,这两个握手过程是无法结合在一起的,总是得先完成 TCP 握手,才能进行 TLS 握手。
TCP 队头阻塞问题
- TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的。
- TCP 队头阻塞的问题,其实就是接收窗口的队头阻塞问题。
eg. 当接收窗口收到的数据不是有序的,比如收到第 33~40 字节的数据,由于第 32 字节数据没有收到, 接收窗口无法向前滑动,那么即使先收到第 33~40 字节的数据,这些数据也无法被应用层读取的。
UDP 头部格式
TCP 和 UDP 区别
应用场景
使用同一个端口
TCP & UDP(可以)
- 传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包。
- 传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。
多个 TCP 服务进程(不同IP可以)
- 如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use”。
- 因为只要客户端连接的服务器不同,端口资源可以重复使用的。
客户端端口选择的流程
MTU & MSS
-
IP 层进行分片传输,是非常没有效率的。
- 如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
- IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
-
为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值。
- 当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。
- 经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。
MSL
MSL 是 Maximum Segment Lifetime,报文最大生存时间,超过这个时间报文将被丢弃。
MSL 与 TTL 的区别
- MSL 的单位是时间,而 TTL 是经过路由跳数。
- MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
TIME_WAIT 等待的时间是 2MSL
网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
QUIC 协议(基于UDP协议实现可靠传输)
Packet Header
重要的字段
QUIC 也是需要三次握手来建立连接的,主要目的是为了协商连接 ID。
Packet Number
Packet Number
是每个报文独一无二的编号,它是严格递增的。
QUIC 使用的 Packet Number 单调递增的设计,可以让数据包不再像 TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。
QUIC Frame Header
- 引入 Frame Header 这一层,通过 Stream ID + Offset 字段信息实现数据的有序性,通过比较两个数据包的 Stream ID 与 Stream Offset ,如果都是一致,就说明这两个数据包的内容一致。
- 丢失的数据包和重传的数据包 Stream ID 与 Offset 都一致,说明这两个数据包的内容一致。
- 一个 Packet 报文中可以存放多个 QUIC Frame。
Stream
Stream 类型的 Frame 格式(Stream 可以认为就是一条 HTTP 请求):
没有队头阻塞的 QUIC
QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。
QUIC 流量控制
TCP 流量控制是通过让「接收方」告诉「发送方」,它(接收方)的接收窗口有多大,从而让 「发送方」根据「接收方」的实际接收能力控制发送的数据量。
TCP 连接建立(三次握手过程)
seq
:序列号,用于标识TCP报文段的顺序。ack
:确认编号(Acknowledgement Number),用于确认已接收到的数据字节的序列号。syn
:同步序列编号(Synchronize Sequence Numbers),用于建立TCP连接的握手过程中的同步标志。
ISN 随机产生
ISN
:初始序列号(Inital Sequence Number),连接建立时生成的初始序列号,是客户端随机产生的一个值。
ISN 随机生成算法:
- ISN = M + F(localhost, localport, remotehost, remoteport)
M
是一个计时器,这个计时器每隔 4 微秒加 1F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
TCP 连接全过程各种状态
LISTEN
:侦听来自远方的TCP端口的连接请求SYN-SENT
:再发送连接请求后等待匹配的连接请求(客户端)SYN-RECEIVED
:再收到和发送一个连接请求后等待对方对连接请求的确认(服务器)ESTABLISHED
:代表一个打开的连接FIN-WAIT-1
:等待远程TCP连接中断请求,或先前的连接中断请求的确认FIN-WAIT-2
:从远程TCP等待连接中断请求CLOSE-WAIT
:等待从本地用户发来的连接中断请求CLOSING
:等待远程TCP对连接中断的确认LAST-ACK
:等待原来的发向远程TCP的连接中断请求的确认TIME-WAIT
:等待足够的时间以确保远程TCP接收到连接中断请求的确认CLOSED
:没有任何连接状态
- 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。
- 客户端将第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
- 服务端将第二个报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
- 第三个报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
- 第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
为什么是“三次”
1. 避免历史连接(主要原因)
- 三次握手才可以阻止重复历史连接的初始化(主要原因)。
- 「旧 SYN 报文」称为历史连接。
- TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
2. 同步双方初始序列号
三次握手才可以同步双方的初始序列号。
TCP 协议的通信双方, 都必须维护一个「序列号」,序列号的作用:
3. 避免资源浪费
三次握手才可以避免资源浪费。
两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。\
总结
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
- 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
第一次握手丢失
tcp_syn_retries
:内核参数,控制客户端的 SYN 报文最大重传次数。可以自定义的,默认值一般是 5。- 如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发 「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的 。每次超时的时间是上一次的 2 倍。
第二次握手丢失
-
tcp_synack_retries
:内核参数,决定SYN-ACK 报文的最大重传次数。默认值是 5。 -
当第二次握手丢失了,客户端和服务端都会重传:
第三次握手丢失
- ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
TCP Fast Open(绕过三次握手发送数据)
在 Linux 3.7 内核版本之后,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。
工作方式
- 第一次发起 HTTP GET 请求的时候,还是需要正常的三次握手流程。
- 之后发起 HTTP GET 请求的时候,可以绕过三次握手,这就减少了握手带来的 1 个 RTT 的时间消耗。
tcp_fastopn
在 Linux 系统中,可以通过设置 tcp_fastopn 内核参数,来打开 Fast Open 功能:
- tcp_fastopn 各个值的意义:
- 0 关闭
- 1 作为客户端使用 Fast Open 功能
- 2 作为服务端使用 Fast Open 功能
- 3 无论作为客户端还是服务器,都可以使用 Fast Open 功能
TCP Fast Open 功能需要客户端和服务端同时支持,才有效果。
TCP 半连接和全连接队列
- 不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文,或返回 RST 包。
- 当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。
因队列长度的关系而被丢弃的条件
- 半连接队列最大值不是单单由 max_syn_backlog 决定,还跟 somaxconn 和 backlog 有关系。
SYN 攻击
方式
-
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满。
-
这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
避免方法
syncookies
开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接。
那么在应对 SYN 攻击时,只需要设置为 1 。
优化 TCP 三次握手
TCP 连接断开(四次挥手过程)
过程
相关函数(close & shutdown)
-
close 函数,同时 socket 关闭发送方向和读取方向,也就是 socket 不再有发送和接收数据的能力。
- 调用了 close 函数的一方的连接叫做**「孤儿连接」**, 用
netstat -p
命令,会发现连接对应的进程名为空。
- 调用了 close 函数的一方的连接叫做**「孤儿连接」**, 用
-
shutdown 函数,可以指定只关闭一个方向的连接:
SHUT_RD(0)
:关闭连接的**「读」**这个方向SHUT_WR(1)
:关闭连接的 「写」 这个方向, 这就是常被称为 「半关闭」 的连接。SHUT_RDWR(2)
:相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。
为什么是“四次”
服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的ACK 和 FIN 一般都会分开发送,因此是需要四次挥手。
- FIN 报文不一定得调用关闭连接的函数才会发送。
- 如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手。
四次挥手变成三次挥手
当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。
TCP 延迟确认机制是默认开启的,所以导致抓包时,看见三次挥手的次数比四次挥手还多。
TCP 延迟确认机制
为了解决 ACK 传输效率低问题,衍生出了 TCP 延迟确认。
TCP 延迟确认的策略:
- 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方
- 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送
- 如果在延迟等待发送 ACK 期间,对方的第二个数据报文又到达了,这时就会立刻发送 ACK
- 出现三次挥手现象,是因为 TCP 延迟确认机制导致的。
第一次挥手丢失
第二次挥手丢失
第三次挥手丢失
第四次挥手丢失
优化四次挥手
- 客户端和服务端双方都可以主动断开连接,通常先关闭连接的一方称为主动方,后关闭连接的一方称为被动方。
- 主动关闭连接的,才有 TIME_WAIT 状态。