TCP
属于OSI七层模型中的传输层协议,位于网络边缘,提供端到端的可靠数据传输,其有着承上启下的作用,协议数据单元为报文段(Message Segment)。
TCP需要提供以下功能:
- 分组和复用
- 应用报文的差错检测(包括出错、丢失、重复、失序、超时等)
- 提供端到端的流量控制
- 提供拥塞控制
- 运输连接建立与释放
- 连接控制
- 报文段序号
传输层为应用进程之间提供端到端的逻辑通信,之所以称为逻辑通信,是因为其看似为两端之间的水平传输,实际为垂直方向的封包和拆包过程。此概念上一节已提到。
应用层的多个进程通过传输层的端口号将应用报文向下交给传输层,然后传输层向下复用
网络层服务。网络层的IP地址和传输层的端口号共同标识主机上特定的进程,当网络层数据报到达目的主机后,拆包得到运输层segment,通过其中的端口号信息,将应用层报文交给相应的进程,此为分用
。
这里将通过抓包分析连接建立和释放过程。
第一次握手由客户端(此处为端口号为60782的Chrome进程)主动向服务端发起,设置初始Sequence number
(序列号)为0;Acknowledge number
设为0表示期望接受到的返回包序列号为0;ACK标志
为0,Syn
为1。即SYN=1, ACK标志=0表示一个连接请求报文段(SYN用于建立连接时的同步信号):
服务端在收到连接建立请求后返回包中:设置初始Sequence number
(序列号)为0;Acknowledge number
设为1表示期望下次接受到的请求包序列号为1,同时也通知客户端已经收到0号包;ACK
标志为1,Syn
为1。即SYN=1, ACK标志=1表示同意建立连接(ACK标志为1时表示此包为确认包):
客户端在收到服务端答复后再一次请求:设置Sequence number
(序列号)为0+1=1
;Acknowledge number
设为1表示期望下次接受到的返回包序列号为1,也即告知服务端已经收到服务端发送的0号包;ACK
标志为1,Syn
为0。
至此,连接建立,方可进行应用数据传输。可以看见,为了可靠传输而需建立连接,这就是握手时延,通常为减少握手时延,使用持久连接技术,这方面会在http1.x
相关小节详解。
图中2MSL
的TIME-WAIT
用以保证对方接收到最后的ACK信息。
在传输过程中,有可能出现发送方的速度大于接收方能够接受的速度,就会导致接收方负载过重而导致拥堵,流量控制�就显得很重要。
TCP的流量控制通过接收窗口大小字段实现,该字段给出接收方的接收缓冲区当前可用字节,即接收方进行流量控制,由16位表示,所以最大为65535
字节,但是通过抓包发现有些包的窗口字段大小远远超过此值,原因是RFC 1323
定义的窗口缩放(TCP Window Scaling)
机制。有关TCP窗口缩放配置参考维基百科:https://en.wikipedia.org/wiki/TCP_window_scale_option通过开启窗口缩放,可以更充分利用带宽,提高性能。
在建立连接时即有接收窗口协商,在传输过程中,接收窗口(rwnd)
大小可以随时改变。发送方实际发送字节数还与拥塞控制
有关,拥塞窗口(cwnd)
由发送方根据网络拥塞程度设定,最终的发送窗口为两者较小值:
发送窗口=Min[rwnd,cwnd]
与流量上述的流量控制不同,拥塞控制解决发送方与网络的关系。造成网络拥塞有许多原因,涉及到网络协议的传输层、网络层、数据链路层。
TCP的拥塞控制手段主要有:慢启动、拥塞避免、快重传、快恢复。
即最开始拥塞窗口(cwnd
)为1个MSS,经过一个RTT后,若没有出现拥塞,则cwnd
设置为2,第二个RTT后设置为4,即指数增长,直到达到门限值ssthresh
。
即AIMD
中的AI
(线性增长),当cwnd
大于或等于ssthresh
时,不再以指数增长的形式增加cwnd,而是每个RTT只增加一个MSS大小即加法增
,避免过早出现网络拥塞。
当发送方没有按时收到ACK(超时)或是收到了重复的ACK,即发生了拥塞。乘法减(MD)就是将ssthresh
快速下降到此时cwnd
的一半。然后有两种处理方案:TCP Tahoe
版本和TCP Reno
版本。
当发送方检测到超时的发生时,就采取此种方案。将cwnd
重新设置为1个MSS,再从头开始应用慢启动算法。
快重传
: 当发送方接收到3个重复的ACK时,立即重传,而不用等到超时发生。将cwnd
设置为新的ssthresh
值(之前cwnd
值的一半),然后使用快恢复
算法。
快恢复
:
cnwd
设置为ssthresh + 3 * MSS
,因为收到3个重复ACK,说明有3个包收到了:
if ( dupacks >= 3 ) {
ssthresh = max( 2 , cwnd / 2 ) ;
cwnd = ssthresh + 3 * SMSS ;
}
- 重传重复ACK指定的包
- 此后每收到一个重复的ACK确认时,
cwnd++
- 当收到对新发送数据的ACK确认时,
cwnd = ssthresh
,之后进入拥塞避免算法(线性增)
如图:
但是TCP Reno
存在的问题是,如果同一窗口丢失了多个包时,快重传机制只会重传一个包,剩下的只能等到超时时间到达。现在TCP使用的是TCP New Reno
算法解决此问题。
谈及重传时,需要先介绍几种实现可靠传输协议(停等协议、回退N协议、选择重传协议)中的选择重传协议,TCP使用选择重传协议。
即接收端在收到顺序不对的包时,窗口不滑动,将这些包先放入到缓冲区保存。发送端只重传超时的包。
上图所示的重传是超时重传,但是这可能需要等比较长的时间。比如client
发送了0,1,2,3
包,server
端收到0,1,3
即2号包丢失,返回ACK
为1,2,4
,2号包将一直等到超时才会重传。见上图。这里面还涉及到超时时间(RTO)的计算,此处不详解。
比如client
发送了0,1,2,3,4
包,server
端收到0,1,3,4
即2号包丢失,返回ACK
为1,2,2,2
,2
号包丢失,后面的3,4
都收到了,但ACK
返回2
,client
收到连续三次重复的ACK,就对丢失的2
号包进行重传。
TCP保证各个分组之间的正确顺序,上面的选择重传例子中,如果中间某一个包丢失了,后续到达的包只能暂时存在接受者的缓冲区,直到错误包重传后才能滑动窗口,这就是所谓的队首延迟,后续我们会在HTTP/2中继续讨论此话题。
由于单独的确认信息比较小,许多TCP栈实现了延迟确认
算法,在特定的窗口时间(100-200ms)内将确认消息放到缓冲区,等待有能够捎带它的数据分组,如果没有等到,就单独发送确认消息。这会导致一个性能问题:当没有足够多的回传数据分组时,会引入相当大的时延,在Linux下,可以通过调用setsockopt()
函数,设置TCP_QUICKACK:
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
除了调节新连接的传输速度,TCP 还实现了 SSR
(Slow-Start Restart,慢启动重 启)机制。这种机制会在连接空闲一定时间后重置连接的拥塞窗口。道理很简单, 在连接空闲的同时,网络状况也可能发生了变化,为了避免拥塞,理应将拥塞窗 口重置回“安全的”默认值。
毫无疑问,SSR 对于那些会出现突发空闲的长周期 TCP 连接(比如 HTTP 的 keep-alive 连接)有很大的影响。因此,我们建议在服务器上禁用 SSR。在 Linux 平台,可以通过如下命令来检查和禁用 SSR
:
$> sysctl net.ipv4.tcp_slow_start_after_idle
$> sysctl -w net.ipv4.tcp_slow_start_after_idle=0
Nagle
算法用于避免太多的小数据包传输,减轻网络负载,比如有可能发送端每次只发送一个字节的包。
Nagle
算法试图在发送一个分组前,将大量TCP数据绑定在一起,一并发送,只有当其他分组被确认后,才允许发送非全尺寸的分组,否则先缓存起来,凑齐全尺寸后再发送。
但这也会造成性能问题,小的数据包可能永远无法发送,且存在延迟确认机制。
可以通过设置TCP_NODELAY
来禁用Nagle
算法。
通过TCP协议细节,我们可以看到,为了保证端到端的可靠数据传输,TCP协议在性能方面存在许多方面的瓶颈。连接建立会带来握手时延,如果连接不能复用,每次都建立新的连接则会造成更大延时;窗口大小限制使得吞吐量存在瓶颈;为了实现拥塞控制,慢启动,拥塞避免等算法也会会造成性能瓶颈;除此之外,协议设计时为了提高性能采取的策略在有些时候反而会造成延时,也需要我们针对特定情况进行设置。
针对这些可能存在的性能瓶颈采取的优化措施,我们会在单独的小节中进行总结。