前言
说起TCP协议相信大家都不陌生,作为互联网的主要的传输协议,其重要性自不必说。
计算机网络体系十分庞大而复杂,TCP协议也不例外。TCP协议从发布至今也经历过无数次的修订和迭代,所以本篇也只是对其中的核心点分析一二。
若有谬误之处,还请指正。
TCP报文结构分析
传输控制协议(Transmission Control Protocol),简称TCP。
提供一种面向连接的、可靠的、基于字节流的服务。
位于OSI网络模型的传输层。
全双工通信。
官方文档:https://tools.ietf.org/html/rfc793
OSI网络模型
TCP报文结构如下图所示:
TCP报文字段含义说明:
源端口:占2字节;用于识别连接的发送端口。
目的端口:占2字节;用于识别连接的接收端口。
序列号:seq,占4字节; 如果SYN标记有效,则此为最初的序列号;如果SYN标记无效,则此为当次发送的数据部分的第一个字节编号。
确认号:ack,占4字节;期望对方下一次传过来的TCP数据部分的第一个字节的编号。
数据偏移:占4位;取值范围0x0101~0x1111;乘以4之后的值表示TCP首部总长度。
保留:占6位;目前的值全为0。
窗口:占2字节;进行流量控制,告知对方下一次允许发送的数据大小。(字节为单位)
检验和:占2字节;检验和的计算内容:伪首部 + 首部 + 数据。
选项字段:最多40字节,用于扩展TCP功能。
标志位
1
2
3
4
5
6URG(Urgent):当URG=1时,紧急指针字段才有效。表明当前报文段中有紧急数据,应优先尽快传送。
ACK(Acknowledgment):当ACK=1时,确认号字段才有效。
PSH(Push):当PSH=1时,接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。
RST(Reset):当RST=1时,表明连接中出现严重差错,必须释放连接,然后再重新建立连接。也可以用于拒绝非法数据和拒绝连接请求。
SYN(Synchronization):当SYN=1、ACK=0时,表明这是一个建立连接的请求。
FIN(Finish):当FIN=1时,表明数据已经发送完毕,要求释放连接。
TCP的运行方式及特性
TCP通信的数据封装解封过程,如下图所示:
- 按照OSI网络模型的分层设计,数据封装时从应用层到数据链路层每一层接收上层传递过来的数据,并加上当前层的头部,直至传递到物理层;数据经物理层负责运转,之后再反过来逐层解封,交给应用层处理。
TCP运行的三个主要阶段
- TCP协议的运行主要划分为三个阶段:创建连接、传送数据、终止连接。
我们可以看到,TCP通信需要通过连接完成,那么什么是连接?
连接的定义
- 在官方文档中,对连接有着明确的规定:
- 总结来说,连接就是用于保证可靠性和流控制机制而维护的数据流状态信息,由Socket、序列号以及窗口大小组成的组合。
创建连接之三次握手
三次握手协议的过程详解:
一对终端同时初始化一个它们之间的连接是可能的。但通常是由一端(服务器端)打开一个套接字(socket)然后监听来自另一方(客户端)的连接,这就是通常所指的被动打开(passive open)。服务器端被被动打开以后,客户端就能开始创建主动打开(active open)。
服务器端执行了listen函数后,就在服务器上创建起两个队列:
1 | SYN队列:存放完成了二次握手的结果。 队列长度由listen函数的参数backlog指定。 |
客户端(通过执行connect函数)向服务器端发送一个SYN包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数x作为消息序列号。
服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为x+1,SYN/ACK包本身携带一个随机产生的序号y。
客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为x+1,而ACK的确认码则为y+1。然后客户端的connect函数成功返回。当服务器端收到这个ACK包的时候,把请求从SYN队列中移出,放至ACCEPT队列中;这时accept函数如果处于阻塞状态,可以被唤醒,从ACCEPT队列中取出ACK包,重新创建一个新的用于双向通信的sockfd(本次socket连接的文件句柄),并返回。
为什么需要三次握手?
TCP官方文档中明确指出了 TCP 连接使用三次握手的首要原因;
- 即为了阻止历史的重复连接初始化造成的混乱问题,防止使用 TCP 协议通信的双方建立了错误的连接。
1 | 试想一下,如果通信双方的通信次数只有两次,那么发送方一旦发出建立连接的请求之后它就没有办法撤回这一次请求, |
传送数据
可靠传输
流量控制
拥塞控制
可靠传输
可靠传输 - 停止等待ARQ协议
ARQ(Automatic Repeat–reQuest),自动重传请求。
所谓的停止等待,也就是在发生数据传输错误时(上面所示的几种情况下),TCP启动超时重传机制,但是这会出现严重的效率问题,当上一个数据传送成功之后,才能开始传送下一个数据,可以类比于串行队列。
为了解决这个问题,也就发明出了连续ARQ协议与滑动窗口协议。
可靠传输 - 连续ARQ协议+滑动窗口协议
在连续ARQ协议+滑动窗口协议的共同协作下,使TCP一个数据段内同时发送多条数据成为可能。
发送字节编号连续的一个数据段,接收方以“最后一个字节编号+1”作为确认号,不需要每个字节都确认一遍,提升了传输效率和性能。
滑动窗口协议:发送方有发送窗口,接收方有接收窗口,在数据发送过程中,接收方在报文中携带窗口大小,在网络情况较差或者恢复的情况下,动态的调整发送方窗口大小,避免因网络因素引起的传输瘫痪。
可靠传输 - SACK(选择性确认)
问题场景
在TCP通信过程中,如果发送序列中间某个数据包丢失(比如1、2、3、4、5中的3丢失了)。
TCP会通过重传最后确认的分组后续的分组(最后确认的是2,会重传3、4、5)。
这样原先已经正确传输的分组也可能重复发送(比如4、5),降低了TCP性能。解决方案
为改善上述情况,发展出了SACK(Selective acknowledgment,选择性确认)技术。
告诉发送方哪些数据丢失,哪些数据已经提前收到。
使TCP只重新发送丢失的包(比如3),不用发送后续所有的分组(比如4、5)。
流量控制
问题场景
如果接收方的缓存区满了,发送方还在疯狂着发送数据。接收方只能把收到的数据包丢掉,大量的丢包会极大着浪费网络资源,所以要进行流量控制。什么是流量控制?
让发送方的发送速率不要太快,让接收方来得及接收处理。原理
通过确认报文中窗口字段来控制发送方的发送速率。
发送方的发送窗口大小不能超过接收方给出窗口大小。
当发送方收到接收窗口的大小为0时,发送方就会停止发送数据。
流量控制 - 特殊情况
- 有一种特殊情况
一开始,接收方给发送方发送了0窗口的报文段。
后面,接收方又有了一些存储空间,给发送方发送的非0窗口的报文段丢失了。
发送方的发送窗口一直为零,双方陷入僵局。
- 解决方案
当发送方收到0窗口通知时,这时发送方停止发送报文。
并且同时开启一个定时器,隔一段时间就发个测试报文去询问接收方最新的窗口大小。
如果接收的窗口大小还是为0,则发送方再次刷新启动定时器。
拥塞控制
防止过多的数据注入到网络中
避免网络中的路由器或链路过载。拥塞控制是一个全局性的过程
涉及到所有的主机、路由器等。
以及与降低网络传输性能有关的所有因素。相比而言,流量控制是点对点通信的控制。
网络拥塞示意图:
- 在理想情况下,输入的负载与链路吞吐量成正相关,但是实际值与理论值存在一定的偏差,当总带宽1000M/s,实际负载到达700M/s附近可能就发生了轻度拥塞,之后增加负载,拥塞就更加严重,最后造成整个网络瘫痪,这时也就彰显了拥塞控制的必要性。
拥塞控制 – 方法
TCP的拥塞控制具有一定的方法,具体如下:
慢开始或称慢启动(slow start)
拥塞避免(congestion avoidance)
快速重传(fast retransmit)
快速恢复(fast recovery)
先来了解几个基础概念
1
2
3
4
5MSS(Maximum Segment Size):每个段最大的数据部分大小;在建立连接时确定。
cwnd(congestion window):拥塞窗口
rwnd(receive window):接收窗口
swnd(send window):发送窗口
swnd = min(cwnd, rwnd)
拥塞控制 – 方法 – 慢开始
- 当MSS=100,且rwnd=3000时,发送方的发送窗口以2倍的速度逐级试探性增长,知道达到接收窗口的阈值为止。
如下图所示:
拥塞控制 – 方法 – 拥塞避免
ssthresh(slow start threshold):慢开始阈值,cwnd达到阈值后,以线性方式增加。
拥塞避免(加法增大):拥塞窗口缓慢增大,以防止网络过早出现拥塞。
乘法减小:只要网络出现拥塞,把ssthresh减为拥塞峰值的一半,同时执行慢开始算法(cwnd又恢复到初始值)。
当网络出现频繁拥塞时,ssthresh值就下降的很快。
如下图所示:
拥塞控制 – 方法 – 快重传
接收方
每收到一个失序的分组后就立即发出重复确认。
使发送方及时知道有分组没有到达。
而不要等待自己发送数据时才进行确认。发送方
只要连续收到三个重复确认(总共4个相同的确认),就应当立即重传对方尚未收到的报文段。
而不必继续等待重传计时器到期后再重传。
如下图所示:
拥塞控制 – 方法 – 快恢复
当发送方连续收到三个重复确认,说明网络出现拥塞。
就执行“乘法减小”算法,把ssthresh减为拥塞峰值的一半。
与慢开始不同之处是现在不执行慢开始算法,即cwnd现在不恢复到初始值。
而是把cwnd值设置为新的ssthresh值(减小后的值)。然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
如下图所示:
终止连接之四次挥手
四次挥手过程分析
四次挥手过程详解:
客户端发送断开连接请求,FIN = 1,seq = u,客户端进入FIN-WAIT-1状态。
服务端收到断开请求, 发送确认 ACK=1,ack=u+1,,同时发送一个验证包 seq = v,进入 CLOSE-WAIT状态,客户端收到请求确认,进入 FIN-WAIT-2 状态,等待服务器发送断开请求。
服务器传输完成,发送断开请求 FIN = 1,ACK = 1,seq = w,ack = u + 1,服务端进入 LAST-ACK 状态。
客户端收到断开请求,向服务端发出确认 ACK = 1,seq = u + 1,ack = w + 1,客户端进入TIME-WAIT状态, 服务端立即进入CLOSED状态,客户端在 TIME-WAIT 状态结束后(2MSL),进入 CLOSED 状态。
为什么释放连接的时候,要进行4次挥手?
TCP是全双工通信模式。
第1次挥手:当主机1发出FIN报文段时
表示主机1告诉主机2,主机1已经没有数据要发送了,但是,此时主机1还是可以接受来自主机2的数据。第2次挥手:当主机2返回ACK报文段时。
表示主机2已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的。第3次挥手:当主机2也发送了FIN报文段时
表示主机2告诉主机1,主机2已经没有数据要发送了。第4次挥手:当主机1返回ACK报文段时
表示主机1已经知道主机2没有数据发送了。随后正式断开整个TCP连接。
TCP性能问题
TCP协议可以说是互联网的基石,作为可靠传输协议,在当今几乎所有的数据都会通过TCP协议传输,但是也会存在一些严重的性能问题。
自从RFC 793发布至今已经走过了40年时间。
但在TCP设计之初没有考虑到现今复杂的网络环境,当我们弱网情况下被断断续续的网络折磨时,我们可能都不知道这一切可能都是TCP协议造成的。
造成性能问题的原因
TCP设计时还处于有线网时代,丢包的概率相对较小,所以TCP认为丢包就发生了拥塞,但是随着网络发展,丢包意味着网络拥塞这一假设在很多时候已经不成立了,这也是问题的根源。
TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差。
TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟。
TCP默认由操作系统内核实现,升级迭代很麻烦,发展缓慢。
扩展
QUIC
- QUIC(读作“quick”)是一个通用的传输层网络协议,最初由Google的Jim Roskind设计,2012年实现并部署,目前还处于草稿阶段。
- QUIC旨在提供几乎等同于TCP连接的可靠性,但延迟大大减少。
QUIC的特点
协议变更,基于UDP,减少了TCP三次握手及TLS握手时间。
拥塞控制,QUIC将拥塞控制策略从操作系统内核转交给应用层来控制,更新拥塞控制算法不需要停机升级,使得在某些场景下可更有效地改变拥塞策略,达到更优的效果。
连接管理,TCP 使用四元组 (源 IP,源端口,目的 IP,目的端口) 标识连接,网络切换时会导致重连,而 QUIC 使用自定义的 Connection ID 作为链接标识,只要客户端使用的 Connection ID 不变,即使网络切换也能保证链接不中断。
多路复用。
总结
到此为止,我们分析了TCP的一些比较核心的特性,但是TCP的复杂性远超我们本文所说,所以有兴趣的同学可以自行仔细研究一番。
参考资料
《TCP/IP详解卷1:协议》