TCP
一、TCP 报文协议段
下面是一张比较清晰的TCP协议段格式(来源网络):
二、TCP 原理
从上面的协议格式也能看出来,相比于 UDP,TCP 增加了很多字段,并且这些字段大多都指向:安全和效率。这也正是 TCP 传输协议的核心特性:
1、确认应答机制
对于TCP协议来说,它的一大特点就是可靠传输,这里的可靠性并不是指代100%能够传输过去,而是对于接收方会返回一个应答。确认应答机制是实现TCP传输可靠性的核心机制。
在网络传输中,由于网络环境的瞬息万变,很可能出现一种“后发先至
”的情况。后发先至的情况通常指的是网络中的数据包乱序到达。这种情况下,可能会导致某些后发送的数据包在传输过程中先到达了接收方,而先发送的数据包则被延迟或者还未到达。举个例子:
上面的例子中可以看到,“先发后至”可能导致传输错误,但是我们可以通过对数据进行编号来解决这种错误,在真实的TCP数据传输中,引入了 序号
和 确认序号
。
TCP将每个字节的数据都进行了编号,即序列号:
应答报文和确认号:
确认序号规则:
回到“先发后至”问题上:
2、超时重传机制
在网络传输中由于网络拥堵、介质故障等原因可能导致发送的数据包无法到达,也就是我们常说的丢包。
对于丢包,主要存在以下两种情景:
情景一:如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发。
情景二:进行重发,主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这里就利用前面提到的序列号,就可以很容易做到去重的效果。
超时重传的时间限定
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间:
总结起来就是,对于TCP的超时重传:即能重传就重传,重传不了就关闭连接,尽可能的保证传输!
3、连接管理机制
Tcp建立连接:三次握手
握手(HandShake)是指通信双方进行网络交互,三次握手,相当于 客户端服务器之间进行了三次交互,建立了连接(各自记录对方的信息)关系。
注意:上述过程都是系统内核中自动完成的,应用程序干涉不了,等待连接完成,accept 就把建立好的连接从内核拿到应用程序中。
补充:这里的 SYN 指的是同步报文段,表示一方向另一方申请建立连接,这里的 SYN 其实是 TCP 报文段中的标志位,初始为 0。这里的 ACK+SYN 报文段即将 ACK 和 SYN 保标志位设为1。
三次握手的作用:三次握手本质上是“投石问路
”的过程,验证了客户端和服务器各自的收发能力是否正常。这也是后续进行可靠性传输的基础!
Tcp断开连接:四次挥手
为什么这里的ACK 和 FIN 不能合并?
补充:假设一次客户端服务器断开连接的过程,虽然客户端进程结束,但是 TCP 连接还在(内核维护),直到四次挥手完成,服务器同理。
4、滑动窗口
在上面的“确认应答机制”中,对每一个发送的数据段,都要给一个 ACK 确认应答,即一发一收机制(下图左)。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。因此为了提高效率,可以采用“滑动窗口机制”(下图右),即一次发送多条数据,将多个段的等待时间重叠在一起,相比一发一收就可以显著提高效率。
滑动窗口机制原理
滑动窗口机制下的丢包处理
情景一:数据包到达,ACK 丢失。
不同于“一发一收”情况下ACK丢失,需要进行超时重传。滑动窗口下仅丢失ACK,其实对于可靠性没有影响,因为是批量发送和重叠等待ACK,由于“确认序号”的特性,即表示该序号之前的数据都已收到。如果此时丢失1001ACK 、3001ACK、4001ACK 只要后续收到更大的“确认序号“,例如后续接收到5001ACK 就可以确定5001之前的数据都已收到,所以此时即使丢失部分ACK对整体的可靠性也没有影响,可以通过后续的ACK进行确认。
情景二:数据包丢失。
如果在滑动窗口这种机制下丢失数据包,会触发为 “高速重发控制”(也叫 “快重传”)
5、流量控制
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包
,继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制(Flow Control);
在返回的ACK报文中,会生效一个“窗口大小”字段,这里面的值就是建议发送方发送的窗口大小。(可以将计算窗口大小理解为,返回接收缓冲区的剩余空间)
发送窗口的大小=流量控制+拥塞控制
6、拥塞控制
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果不加以约束,仍然会出现问题。在上述“流量控制”中接收方根据自己的处理能力来反向约束发送速度。其实实际发送中还存在另一个机制“拥塞控制”用来衡量传输路径的传输能力。
拥塞控制,实际上就是摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
7、延时应答
Tcp中决定效率的关键因素就是“窗口大小
”。我们知道对于发送方和接收方,发送方在不停地发送数据,接收方也在不停地从接收缓冲区消费数据,如果此时接收方收到数据后立即返回ACK,此时ACK报文中携带的窗口大小设为N,倘若等待“一小段”时间,让接收方先消费一些数据,然后再返回ACK,此时携带的窗口大小为N+,易知:N+ > N 。
上述就是延时应答,它实现的效果就是通过延时,让接收方程序趁机多消费点数据,此时反馈的窗口大小就会更大一些,使得满足接收方能够处理的前提下,让发送方的发送数据速率也更快一些。
那么所有的包都可以延迟应答么?肯定也不是;
8、捎带应答
很多情况下,客户端服务器在应用层也是 “一发一收
” 的,意味着客户端给服务器发送一个请求,服务器也会返回一个响应。因此在“延时应答”的基础上,ACK 就可以搭顺风车了。ACK 是由内核负责的,一般是收到报文立即返回,而响应则是通过一系列代码执行到才返回,这两个时机本来是不同的,但是通过“延时应答”机制,就很可能使得 ACK 和 响应 合并成一个数据报。
每一次数据报的传输都有封装分用等一系列的复杂过程,两个数据报需要封装分用一次,而两个数据报需要封装分用两次,很明显这种“捎带应答
”机制,可以通过合并的方式减少封装分用的次数,从而提高效率。
正是由于这种“捎带应答”机制,使得“四次挥手”可能三次就完成了。
9、面向字节流(粘包问题)
在Tcp协议中,一个很大的特点就是面向字节流,这样使得 Tcp 的读写更加灵活,但与此同时也暗藏杀机,那就是“粘包问题
”。
在使用Tcp协议的前提下,假设我发送了以下4条数据:
试想站在应用层的角度,在会在接收缓冲区里看到如下信息:
等等,他们为什么要分我们的地瓜?
上面这个例子就展示了一个由于“粘包问题
”闹出的笑话。当A给B连续发送多个应用层数据报之后,这些数据积累到B的接收缓冲区中,数据之间会紧紧地挨在一起,此时B的应用程序在读取数据的时候,就难以区分从哪到哪是一个完整的应用层数据报,可能读出半个包、一个半包等情况。
那么如何避免呢?其实在之前章节《网络编程》中,在使用传输层协议 Tcp 进行通信时,我们当时写了一个 Tcp 的回显客户端-服务器,当时为了区分应用层数据包,我们做了如下约定:
其实以上就是一个简单的自定义应用层协议,通常来说,我们处理粘包问题主要有以下两种方案
:
冷知识:UDP不会出现“粘包问题”。根本原因是 UDP 协议面向数据报,彼此之间有明显的界限。
10、异常情况(机器掉电/网线断开:心跳包)
进程关闭/进程崩溃:进程终止,socket文件也被随之关闭,但是此时操作系统内核还维护有Tcp连接,直到四次挥手完成,因此和正常关闭没有什么区别。
机器关机/机器重启:关机或重启会先杀死所有的用户进程,同样会触发四次挥手,期待的情况是在此过程中四次挥手完成,如果四次挥手还未完成,比如对端发送来FIN,当前机器还没来得及ACK就关机了,此时对端就会重传FIN,重传几次之后,都发现没有ACK,就尝试重置连接,如果还不行就释放连接。
机器掉电/网线断开:连接瞬间关闭,来不及进行任何挥手操作。此时分为两种情况:
更多详情内容请参考 TCP RFC标准文档