在计算机网络中的传输层TCP/UDP协议,都是操作系统网络协议栈提供好的接口供开发者使用,但是大家都会听一些有经验的程序员说TCP协议比UDP协议可靠,这是为什么呢?都说TCP协议提供可靠传送,面向连接性的,基于字节流的,数据包不丢包这是怎么做到的?本文将讲解一些关于TCP协议内部实现一些细节。

计算机能实现网络通讯全部依靠着是互联网,互联网是有多个广域网组成的,通过世界各地网络运营商组成的通讯网络。但是两个计算机依靠着这些网络上的硬件设备进行通讯,这些硬件设备都不能保证网络数据能正常传到对方计算机,这是做不到的。网络环境是一个很复杂的环境,可能两台通讯的计算机一台东半球,另外一台在西半球,而网络数据包则需要通过遍布在地球表面上的物理网络设备来传输(当然这里也可以通过卫星上网,例如STARLINK,当然在某些所在的地区不提供服务...),路由器、集线器、网络运营商的网络传输节点都不能百分之百保证网络环境是正常的,所以在传输层上设计TCP协议,能解决这些问题。


连接的状态

对应开发者而言我们只需要使用各种编程语言提供好的Socket接口传入目标IP和端口即可得到相应的网络描述符,这是作为一个开发者而说只需要使用API即可完成数据收发工作,但是具体协议内部是如何工作的呢?这里我要剖析TCP协议工作原理,在TCP协议中通讯时分为3部分,不要看就3个状态而已,内部实现其实挺复杂:

  1. 建立连接
  2. 数据传送
  3. 连接关闭

相信很多开发者都听说过TCP的3次握手🤝4次挥手👋🏻这些概念,但是大家有没有想过为什么要这么设计呢?而且这两个概念在TCP协议那个环节做的呢?为什么要这样做?

通常建立连接通讯的时候,相应的计算机会在某进程中监听着某个端口,以便有其他客户端发送连接的请求,当有请求过来的时候会触发TCP建立连接的规则,这里的规则就是三次握手。在说连接建立之前的得先说明一下每个TCP数据包都要包含的TCP数据包头,TCP包头尺寸除非存在选项,否则其正常大小为20字节,如下图:

我整理一个表格分别记录其对应的作用或者用途如下:

字段名称作用说明
源端口用于记录数据包发送者的端口
目的端口用于记录数据包接收者的端口
序列号TCP为TCP段中包含的每个数据字节分配一个唯一的序列号
确认号下需要从对应值位置开始的序列号,它总是最后一个接收到的数据字节的序列号加1
包头大小它包含TCP包头的长度,数据从哪里开始读
数据窗口它通告发送者无需确认即可接收多少数据
校验和发送方在发送数据之前将CRC校验和添加到校验和字段
紧急指针添加到序列号的紧急指针指示紧急数据字节的结尾
时间戳根据时间戳,接收方可以识别正确的段
窗口尺寸扩展如果接收方希望接收更多数据,则可以使用此字段来通告其更大的窗口大小

以上为部分TCP包头一些字段,还有一部分不是很重要的我没有列举出来,至于现在你只需要大致了解其作用,这篇文章后面部分我将在网络可靠性上引用这些标记位和要解决的问题,下面为最重要的部分标记位📌这部分主要做一些数据的特殊处理,表格如下:

标记位作用说明
URG位它指示接收器当前段中的一定数量的数据很紧急
ACK位当ACK位设置为1时,表示TCP头中包含的确认号有效
PSH位PSH位用于将整个缓冲区立即推送到接收应用程序
RST位RST位用于重置TCP连接
SYN位SYN位用于同步序列号
FIN位FIN位用于终止TCP连接

这些标志位在数据包通讯过程中起到很重要的作用,这些消息会数据包头部存储,客户端会服务器通讯的时候会携带着,以上的数据包头部为TCP控制信息,会在连接建立到结束的每次数据通讯都会有。


连接的建立

TCP在初始化连接建立时其实就为了交换两台计算机的控制信息,这里的控制信息一部分上面我已经讲过了。控制信息一般都是存储在操作系统提供的网络描述符中的,但是在首次建立连接的时候这些信息起到关键的作用,首先我们要了解一下一个标准的TCP数据包是什么样子的?

我们主要关注的蓝色部分的控制信息的转换工作,蓝色部分为TCP数据包的包头信息也就是上面介绍的那些字段集合部分。初始化连接的时候就和我们日常生活中习以为常的打电话联系别人一样,在打电话的时候我们要知道被呼叫的那个人手机号,那我们要怎么做呢?日常生活中我们都线下见面留一个电话号然后保存起来,然后下次需要呼叫时候自己拨打即可。

换到TCP协议建立连接时就是目标机器的IP和端口号信息,然后操作系统协议栈会帮助我们建立连接,这个过程非常复杂的。计算机网络环境非常复杂的,你不知道当前互联网设备是否存在网络延迟或者说某个光缆被施工队挖断掉了导致数据发送失败等等情况......下面为一个标准的TCP建立连接的过程,如下图:

TCP建立连接为什么是3次数据包交换,而不是两次?这个跟互联网网络环境因素有很大关系;注意数据包中对应的标记位是什么意思这里我不过多叙述,不是我要说明的重点,如果读者你不知道这是你自己的问题自己解决!建设一个连接请求建立过程如下:

为什么双方要发出第3个数据包才能确认连接建立呢?不要看就上面几条线而已,其TCP数据包的交互过程受到网络的因素非常大。看上图中的红线部分就为2次握手建立连接的方式,由于第一次发出去的数据包在网络传输的过程中出现了延迟,导致到达服务器端的时候数据包超过了TCP协议默认的超时限制,这个超时机制是什么?这里要说明的是TCP默认对发送出去的请求数据包设置有超时机制,会有一个时间窗口范围来控制数据时间。例如被呼叫端没有返回ACK包,则认为数据包超时并且会重新发送一次数据包,问题就在这由于要这些超时重发机制导致一些很多矛盾的问题要解决。

假设上面的TCP对一个连接请求超时机制设置为150ms,上图中的红线部分,第一个请求发送出去了由于网络线路上出现抖动总耗时为300ms才达到的服务器端;与此同时客户端由于超时机制触发一次重发数据包对应的为蓝线部分,这次由于蓝线数据包没有遇到网络问题很顺利就达到服务器端耗时100ms,并且返回ACK包;那么问题来了在这个时候红线数据包到达了服务器端,服务器端也返回一个SYN包,此时客户端将认为哪个连接是正确的?或者说怎么防止重复建立连接?

上图红线部分的请求报文在由于TCP发送方超时机制的影响已经被视为无效的报文请求了,但是请求报文还会在网络上传输导致服务器最终还是收到了此次请求并且认为建立连接;此时TCP为了解决这个问题在设计的时候,会让客户端在接受到服务器SYN包时还会向对方发送一次ACK包,如果第三个ACK被服务器端收到才会建立连接。这样即使第一次发出去的延迟请求包头到达了服务器端,服务器端再返回的SYN包,此时的客户端上已经有对应的网络描述符了,就不需要重新建立连接了,并且连接双方的网络描述符状态机已经发生了改变,不需要二次更改了。

通过上面实际数据交换过程来证明了为什么TCP建立连接要3次握手的原因,但是这些问题还是TCP协议众多设计问题中的其中之一,下面我将开始讨论TCP协议如何保证数据可靠性传输,数据包存在乱序问题,怎么为何两台计算机的状态一致性问题...


数据分包问题

连接建立之后,两台计算机就开始通过互联网来保证数据交换,但是上面TCP建立连接都存在网络环境问题,那在连接建立成功之后该如何来保证每次数据包不丢失问题呢?并且可能两台计算机传输的数据文件很大,该如何传输这些保证数据文件能精准传输到对方计算机呢?这都是TCP协议要在设计的时候考虑到的问题。

了解TCP数据包传送之前,我们的先了解一下TCP数据包是在整个网络协议栈中处于什么位置和什么样子的,TCP/IP模型中各层数据包结构的关系如下图:

TCP完成3次握手控制信息和状态机转换后连接建立成功,两台计算机就形成一个网络通道,示意图:

为什么不用一个数据包就把数据发生到对方计算机,然后就一次性发送完就结束通讯?首先TCP是面向连接的,也就是两台计算机双方可以主动发出数据包,任何一方都可以在连接没有关闭的情况下收到对方发了的数据包,例如下面两台计算机打开连接通讯,这里设计者就考虑数据是在连接没有关闭之后都是无限制的数据;对应开发者写数据就调用一个write接口写入字节流数据即可,但是底层怎么实现的?

TCP为了解决这些问题会底层,这个又操作系统决定的,在操作系统内部分配一个缓冲区,根据我们每次写入的数据都被放入缓冲区,然后又协议栈来决定什么时候发送出去。但是带来一个问题就是有可能一次写入数据太多了,也可能一次数据太小,那么何时发送这些数据都带来很大的问题...这就你有一个手推车去取行李运到另外一个地方一样,如果一次性拉的太多了就会很重,换到TCP里面就是一次性运输数据量很大导致网络拥堵;如果每次都发送数据很小那又导致需要多次发送,这就要可能如何设计缓冲区发送频率和数据包大小了?

一个数据包的长度取决于当前网络协议栈能容纳的数据长度,这个是怎么意思?这里拿PPPoE来说一个标准的MTU最大传输单元是1500字节,但要去除一些40字节的头部信息占用,剩下就1460字节可以用来存放数据了。回到上面的问题一个大于1460字节的数据就要拆分成多份数据包进行发送了,数据包大小和发送频率问题就出现这这里,如果要数据每次很小不能接近MTU大小就不进行发送就会出现低延迟,如果直接发送每个MTU数据区有得不到充分利用。

上面所讨论这些都是矛盾因素TCP数据包传送要考虑的;如果按照长度优先那么网络的效率会提高,如果时间优先,网络链路上的数据包会变多,导致降低网络传送效率或者阻塞情况...这些问题在TCP滑动窗口算法来解决的问题,但是本文不会介绍这些因为TCP这个协议太复杂了;并且为了解决这些问题Google在2016年曾发布过他们研发的BBR加速算法,在本文末尾处有提供相关链接。


数据可靠传输

TCP协议对每个段字节序数据都会分配一个编号,这和开发中日常使用的数组下标一样一个意思,只不过是一个字节序列;协议栈会为每个已经存储在缓冲区里面的数据分配一个序号,如下图:

发送数据过程其实很简单,这个数据交换过程是可以任意一方的,例如上图服务器发起一个数据请求,这是TCP包头上的那些字段位就派上用场了,可以在请求头序号填写好数据段从哪里开始,如果要数据段大小规定也可以在请求头中设置。如果没有则直接读取1460字节的数据,而接收方只需要减去包头大小就能获取全部数据,然后放到本地缓冲区拼接起来。

TCP如何保障数据包不能丢的呢?发送方每次方式完的数据窗口里的数据都会被继续保存着,指定服务器端回复ACK编号即可,每次的ACK编号都是ACK=seq+size,这里的size就是每次接收到数据包大小,这样客户端也会根据ACK来确认数据完整性。

上图为双方数据确认过程,如果接收方回复ACK=size+seq说明在这序号之前的数据都已经被接受或者确认完成,具体实现细节可以查看有关资料,TCP这个协议实现确实太复杂了,由于接受者和发送者双方出来的数据速度不一样并且还受到网络因素的影响比较大,还需要考虑动态调整发送频率算法,也就是TCP中的滑动窗口算法和納格算法,这些实现细节极其复杂,我不可能做的面面俱到。


TCP状态机

在两台独立计算机在通过TCP协议通道连接下,因为是全双工的通讯所以双方都发起连接关闭请求,这也对应着TCP状态机和状态机转换,那什么是TCP状态机呢?我个人有这么一句话定义TCP的状态机: 可以认为TCP状态机就是随着两台互相通讯的计算机随着连接的变化的过程,状态机的状态可以描述一个连接的变化情况; 下图就为TCP状态机的状态关系图:

  • 图中红实线表示客户端正常的状态变迁
  • 图中蓝实线表示服务端正常的状态变迁
  • 虚线用于不常见的序列,如复位、同时打开、同时关闭等等

客户端特有的状态:SYN_SENT、FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSED;服务端特有的状态:LISTEN、SYN_RCVD、CLOSE_WAIT、LAST_ACK、CLOSED; 共有的状态:CLOSED、ESTABLISHED。

TCP状态机其实就是TCP通信协议的生命周期,看了上面的图片是不是觉得这个状态机极其复杂,其实如果能够完全理解TCP连接过程和断开过程其实也不太复杂,并且状态要搞清楚之间的关系,下面是每个状态的说明:

状态状态说明
LISTEN等待来自远程TCP应用程序的请求
SYN_SENT发送连接请求后等待来自远程端点的确认,TCP第一次握手后客户端所处的状态
SYN-RECEIVED该端点已经接收到连接请求并发送确认,该端点正在等待最终确认,TCP第二次握手后服务端所处的状态。
ESTABLISHED代表连接已经建立起来了,这是连接数据传输阶段的正常状态
FIN_WAIT_1等待来自远程TCP的终止连接请求或终止请求的确认
FIN_WAIT_2在此端点发送终止连接请求后,等待来自远程TCP的连接终止请求
CLOSE_WAIT该端点已经收到来自远程端点的关闭请求,如果还有数据为发送继续发送,然后回复FIN确认同意关闭来连接
LAST_ACK等待先前发送的FIN到远程TCP的连接终止请求的确认
CLOSING等待来自远程TCP的连接终止请求确认
TIME_WAIT等待足够的时间来确保远程TCP接收到其连接终止请求的确认

连接建立主要会用到前3个状态,剩下的ESTABLISHED状态为正常通信状态,剩下都为连接断开所需要的状态,关于TCP状态转换的视频可以查看:TCP协议状态机转换过程


断开连接

关闭连接的步骤比建立连接的步骤复杂的多,因为也要考虑到网络的因素比较大整个网络通信链路是不可靠的,断开连接就要更改双方的状态机;由于通信的双方是全双通信而工作的,断开连接可以由其中一台计算机发起,所以双方的TCP协议的状态变更都要保持同步,任何一个状态都需要2次确认才能进入下一个状态。

上图是我画的TCP关闭连接的时候的状态图,记住一点:通信的双方的状态机的更新必须是收到对方的回应包才能更新,这里的回应包有ACK包和服务端发来的FIN包,只要没有收到回复的包,其状态一直保持当前状态并且触发数据包重发机制,没有等到回应就重发数据包。

了解到上面我说的规则之后,再来看看为什么要4次挥手关闭连接。上图中的蓝色线部分表示网络链路没有出现阻塞或者上说抖动机制,数据包顺利到达对方;红色线部分表示此数据包可能在传输过程中超时或者某些原因对方不能收到,红色虚线部分表示因为没有收到ACK包,触发了数据包重发机制;绿色虚线部分表示正常的缓冲区数据的还没有传输完成再进行传输,等传输完成之后才能关闭连接。

这里主要关注的是TIEM_WAIT状态: 服务器在发送缓冲区剩下的所有的数据之后会发一个FIN包表示自己也要进入关闭状态,此时客户端收到之后会回复一个ACK包,如此同时服务器的也在等待ACK的到来,只要最后一个ACK到达服务器端,服务器就关闭连接。但是问题就出现在这个ACK包上,如果最后一个ACK发出去了对方没有收到,对方肯定会重新发送一个FIN包表示重新同步状态,这个过程以此类推,这里的TIEM_WAIT作用就要有足够的时间让ACK传输并且看看有没有重试的FIN包,如果有客户端会重新刷新一下TIME_WAIT时间,这里Linux内核中的时间为60秒;也就是日常大家所说的“让子弹多飞一会的”装逼的话的意思差不多,至于子弹能不能命中目标那得看运气,如果没有命中就重新来一次,递归,直到命中为止。


小 结

两台计算机在通信的时候靠着的网络,但是网络是一个非常不可靠的因素,随时都会出现各种问题。TCP协议虽然说是面向连接的,但是在网络链路上哪里有真正的连接呢?都是依靠着软件的机制保障着可靠的连接状态,TCP之所以叫做TCP协议,都是双方约定好的规则来更新两台相互独立的状态机;网络连接的计算机就是一个分布式计算机系统,由于网络链路上各种不可靠因素所以出现了各种分布式系统问题,分布式脑裂、分布式CAP、分布式BASE、分布式Raft一致性算法,这些都是基于网络延伸出来的问题,换到程序身上就是要对可能发生的错误提前编写好错误处理程序,降低程序出错频率,设计一个基于网络同步状态的分布式系统是非常复杂的。


其他资料

便宜 VPS vultr
最后修改:2023 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !