tcp连接加深
参考:
标识符含义
- SYN: 建立连接
- FIN: 关闭连接
- ACK: 响应
- PSH: 有数据传输
- RST: 连接重置
三次握手
流程图
建立连接(客户端B和服务器A通信)
- TCP 连接建立之前, 双方都处于 CLOSED 状态
- 主动发起 TCP 连接的一方向另一方发送 SYN = 1, Sequence Number = k 的握手请求, 这是第一个握手, 此 Segment 发出以后, 主动发起 TCP 连接的一方由 CLOSED 状态转变为 SYN - SENT 状态
- 被动打开的一方在收到另一方发来的握手请求后, 若同意建立连接, 则发送 SYN = 1, ACK = 1, Acknowledgment Number = k + 1, Sequence Number = m 的 TCP Segment, 当该 Segment 发出以后, 被动打开的一方由 LISTEN 状态转变为 SYN - RCVD 状态, 这是第二个握手
- 主动打开的一方在收到另一方的握手响应之后, 发送 ACK = 1, Acknowledgment Number = m + 1, Sequence Number = k + 1 的 TCP Segment, 当该 Segment 发出以后, 主动发起 TCP 连接的一方由 SYN - SENT 状态转变为 ESTAB - LISTEN 状态, 这是第三个握手 (第三个握手消息可以包含有数据, 如果没有数据则不消耗序号, 即若该握手消息中没有携带数据, 则下一个 TCP Segment 仍可以使用值为 k + 1 的 Sequence Number)
- 主动发起 TCP 连接的一方在发送第三个握手消息之后便进入连接已建立的状态了, 它可以开始向接收方发送正式的数据
- 被动打开的一方在收到主动发起的一方发送的第三个握手消息之后也进入 ESTAB - LISTEN 状态, 此时也可以正式开始收发数据
建立连接服务侧处理图
三次握手原因
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免服务端资源浪费
四次挥手
流程图
关闭连接(客户端B和服务器A通信)
- A 向 B 发送 FIN = 1, Sequence Number = k 的 TCP Segment, 当该 Segment 发出以后, A 由 ESTAB - LISTEN 状态转变为 FIN - WAIT - 1 状态 (第一次挥手)
- B 收到 A 发来的第一次挥手 Segment 后, 向 A 发送 ACK = 1, Acknowledgment Number = k + 1, Sequence Number = m 的 TCP Segment, 当该 Segment 发出以后, B 由 ESTAB - LISTEN 状态转变为 CLOSE - WAIT 状态 (第二次挥手)
- A 收到 B 发来的第二次挥手 Segment 之后, 便由 FIN - WAIT - 1 状态转变为 FIN - WAIT - 2 状态, 此时由 A → B 上的连接可以认为已经释放, 但 B 仍然可以给 A 发送消息
- B 如果不想关闭连接, 可以持续正常地向 A 发送 TCP Segment, 假设在某个时间点上, B 也想关闭 TCP 连接, 则 B 向 A 发送 FIN = 1, ACK = 1, Acknowledgment Number = k + 1, Sequence Number = j 的 Segment, B 发出以后, 它由 CLOSE - WAIT 状态转变为 LAST - ACK 状态 (第三次挥手)
- A 收到 B 发来的 FIN = 1 的 Segment 后, 向 B 发送 ACK = 1, Acknowledgment Number = j + 1, Sequence Number = k + 1 的 Segment, 该 Segment 发出以后, A 由 FIN - WAIT - 2 状态转变为 TIME - WAIT 状态 (第四次挥手)
- B 收到 A 发送的第四次挥手消息之后, 便由 LAST - ACK 状态转变为 CLOSED 状态, 对 B 来说, TCP 连接已彻底释放
- 但 A 仍需要等待一段时间, RFC 793 建议的等待时长为 2min * 2, 其中 2 min 是 MSL (Maximum Segment Lifetime, 即估计一个 TCP Segment 从发出以后在被接收之前在网络中存活的最长时间), 这里 A 在最后一次发出 Segment 之后仍需要等待 2 * MSL 才可以彻底释放连接
TIME_WAIT作用
- 防止旧连接的数据包
- 保证连接正确关闭,防止服务端长时间处于LAST-ACK状态
TIME_WAIT过多
- 第一是内存资源占用;
- 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口
重传输机制
超时重传
RTT: Round-Trip Time 往返时延
RTO: Retransmission Timeout 超时重传时间
- 估计往返时间,通常需要采样以下两个:
- 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。
- 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。
- 如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。
RTO具体计算参考: rfc2988
快速重传
TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。 在上图,发送方发出了 1,2,3,4,5 份数据:
- 第一份 Seq1 先送到了,于是就 Ack 回 2;
- 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
- 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
- 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。 快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。
SACK 方法
SACK( Selective Acknowledgment 选择性确认)
这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。
Duplicate SACK
Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
D-SACK 有这么几个好处:
- 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是「发送方」的数据包被网络延迟了;
- 可以知道网络中是不是把「发送方」的数据包给复制了; 在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)
滑动窗口
引入原因
为每个包确认应答,效率较低,引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。
窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
累计确认
发送299和399到服务端,假设没有收到ACK300,只收到ACK400,也认为400前的数据都收到了
包文字段
TCP 头里有一个字段叫 Window,也就是窗口大小。通常窗口的大小是由接收方的窗口大小来决定的。发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
TCP发送方滑动窗口
使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)
- SND.WND:表示发送窗口的大小(大小是由接收方指定的);
- SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
- SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
- 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
- 可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)
TCP接收方滑动窗口
其中三个接收部分,使用两个指针进行划分:
- RCV.WND:表示接收窗口的大小,它会通告给发送方。
- RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
- 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。
接收窗口和发送窗口大小并不是完全相等,接收窗口的大小是约等于发送窗口的大小的
流量控制
原因
防止接收方处理不过来,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。
流量控制原理
- TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。
- 如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭
窗口关闭潜在的危险和解决
- 问题: 当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了。这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
- 解决:
- 为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
- 如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
窗口探测 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器; 如果接收窗口不是 0,那么死锁的局面就可以被打破了。 窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。
拥塞控制
原因
前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….
于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络
实现
- 拥塞窗口: cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd 变化的规则: - 只要网络中没有出现拥塞,cwnd 就会增大;
- 但网络中出现了拥塞,cwnd 就减少;
拥塞控制主要是四个算法
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
慢启动
- TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?
- 慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
- 触发:
有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。 当 cwnd < ssthresh 时,使用慢启动算法。 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。
拥塞避免算法
- 当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。
- 一般来说 ssthresh 的大小是 65535 字节。
- 那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
- 拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
- 就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
- 当触发了重传机制,也就进入了「拥塞发生算法」。
拥塞发生
- 当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:超时重传 快速重传
- 当发生了「超时重传」:
- 这个时候,ssthresh 和 cwnd 的值会发生变化:
ssthresh 设为 cwnd/2, cwnd 重置为 1
- 接着,就重新开始慢启动,慢启动是会突然减少数据流的。这真是一旦「超时重传」,马上回到解放前。但是这种方式太激进了,反应也很强烈,会造成网络卡顿。
- 这个时候,ssthresh 和 cwnd 的值会发生变化:
- 当发生了「快速重传」:
- TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
cwnd = cwnd/2 ,也就是设置为原来的一半; ssthresh = cwnd; 进入快速恢复算法
- TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
快速恢复
- 快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈
- 正如前面所说,进入快速恢复之前,cwnd 和 ssthresh 已被更新了:
cwnd = cwnd/2 ,也就是设置为原来的一半; ssthresh = cwnd;
- 然后,进入快速恢复算法如下:
拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了); 重传丢失的数据包; 如果再收到重复的 ACK,那么 cwnd 增加 1; 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
拥塞算法示意图
socket编程
socket编程流程
- 服务端和客户端初始化 socket,得到文件描述符;
- 服务端调用 bind,将绑定在 IP 地址和端口;
- 服务端调用 listen,进行监听;
- 服务端调用 accept,等待客户端连接;
- 客户端调用 connect,向服务器端的地址和端口发起连接请求;
- 服务端 accept 返回用于传输的 socket 的文件描述符;
- 客户端调用 write 写入数据;服务端调用 read 读取数据;
- 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。
listen(linux)源码分析
这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的socket,后续用来传输数据。 所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。 Linux内核中会维护两个队列:
- 未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
- 已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
状态机
int listen (int socketfd, int backlog)
- 参数一 socketfd 为 socketfd 文件描述符
- 参数二 backlog,这参数在历史版本有一定的变化
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。
状态统计
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
主要配置(/etc/sysctl.conf)
#对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
##减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
##优化网络设备接收队列
net.core.netdev_max_backlog=3000
## 关闭延迟ACK
## 这样的问题就是每个TCP数据包都会有一个ACK包,增加了网络的包量
root权限下把/proc/sys/net/ipv4/tcp_no_delay_ack文件的值修改成1即可。
-
修改完之后执行/sbin/sysctl -p让参数生效。
-
回收TIME_WAIT
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。 net.ipv4.tcp_fin_timeout这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。 net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测连接存活的相关配置。