协议特性概览
协议 | 连接性 | 双工性 | 可靠性 | 有序性 | 有界性 | 拥塞控制 | 传输速度 | 量级 | 头部大小 | |
---|---|---|---|---|---|---|---|---|---|---|
UDP | 无连接(Connection less) | n:m | 不可靠(丢包后数据丢失) | 无序 | 有消息边界, 无粘包 | 无 | 快 | 高 | 8 字节 | |
TCP | 面向连接(Connection oriented) | 全双工(1:1) | 可靠(重传机制) | 有序 | (通过 SYN 排序) | 无, 有粘包情况 | 有 | 慢 | 低 | 20~60 字节 |
UDP
面向报文
UDP 是一个面向报文的协议. 不会对报文进行任何拆分和拼接操作.
具体来说:
- 在发送端, 应用层将数据传递给传输层的 UDP 协议, UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议, 然后就传递给网络层了
- 在接受端, 网络层将数据传递给传输层, UDP 只去除 IP 报文头就传递给应用层, 不会进行任何拼接
不可靠性
- UDP 是无连接的, 通信不需要建立和断开连接
- UDP 是不可靠的, 不关心备份数据和不保证数据可达
- UDP 没有拥塞控制, 一直会以恒定的速度发送数据, 在网络条件不佳的情况下可能会导致丢包. 在实时性要求比较高的场景则更有优势
高效
UDP 没有 TCP 复杂, 头部只有 8 字节.
主要包含数据:
- 两个十六位的端口号, 分别为源端口(可选)和目标端口
- 整个数据报文的长度
- 整个数据报文的校验和(IPV4 可选), 该字段用于发现头部信息和数据中的错误.
传输方式
UDP 不止支持一对一的传输, 也支持一对多, 多对多, 多对一. 简单的说, 提供了单播,多播和广播的能力
TCP
头部
对于 TCP 头部来说, 以下几个字段是比较重要的:
- Sequence number: 这个序列号保证了 TCP 传输的报文都是有序的, 接收端可以通过序号顺序拼接报文
- Acknowledgement Number: 这个序号表示数据接收端接受期望的下一个字节的编号是多少, 同时也表示上一个序号的数据已经收到
- Window Size: 窗口大小, 表示还能接受多少字节的数据, 用于流量控制
- 标识符:
- URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
- ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
- PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
- RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
- SYN=1:当 SYN=1,ACK=0 时,表示当前报文段是一个连接请求报文。当 SYN=1,ACK=1 时,表示当前报文段是一个同意建立连接的应答报文。
- FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。
状态机
HTTP 是无连接的, 所以作为下层的 TCP 协议也是无连接的, 虽然看似 TCP 将两端连接了, 但是其实只是两端共同维护了一个状态:
TCP 的状态机比较复杂, 并且与建立断开连接时的握手息息相关, 接下来就介绍两次握手.
此外, 指标 RTT 表示发送端发送数据到接收到对端数据所需的往返时间.
三次握手
在 TCP 协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 也是一个全双工的协议。
起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。
三次握手的目的在于让双方互相确认对方具有发送和接受消息的能力
第一次握手
客户端发起
客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。
第二次握手
服务端确认了客户端发送的能力
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。
第三次握手
客户端确认了服务端的接受的能力和发送的能力
服务端收到消息确认了客户端接收的能力
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
PS:第三次握手可以包含数据,通过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,都可以使用类似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减少 RTT 的目的。
四次挥手
TCP 是全双工的, 在断开连接时两端都需要发送 FIN 和 ACK.
四次挥手的目的在
第一次挥手
客户端: 我消息发完了
若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
第二次挥手
服务器: 好的, 但是等一下, 我把剩下没发完的处理一下
B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。
第三次挥手
服务器: 好的, 现在消息发完了
B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。
第四次挥手
客户端: 消息发完了是吗, 那我们断开吧
服务端: 收到
A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。
为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?
为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭。
为什么第二次和第三次不能合并, 第二次和第三次之间的等待是什么?
当服务器执行第二次挥手之后, 此时证明客户端端不会再向服务端请求任何数据, 但是服务端可能还在正给客户端发送数据, 此时服务端会等待把之前未传输完的数据传输完毕之后再发送关闭请求.
保活计时器
除时间等待计时器外, TCP还有一个保活计时器(keepalive timer). 设想这样的场景: 客户以主动与服务器建立了TCP连接. 但后来客户端的主机突然发生故障. 显然, 服务器以后就不能再收到客户端发送的数据. 因此, 应当有措施使服务器不要再白白等待. 这就是保活计时器起作用的地方了.
服务器每收到一次客户的数据, 就重新设计保活计时器. 时间的设置通常是两个小时. 如果两个小时都没有收到客户端的数据, 服务端就发送一个探测报文段, 每个75秒发送一次. 若连续发送10个探测报文段后仍然无客户端的响应, 服务端就认为客户端失效了, 随后关闭连接
TCP 如何保证传输过程的可靠
- 校验和: 发送方在发送数据之前计算校验和, 接收方收到数据后同样计算, 如果不一致, 那么传输有误.
- 确认应答, 序列号: TCP进行书传输的时候数据都进行了编号, 每次接收方返回ACK都有确认序列号
- 超时重传: 参见下面的ARQ协议介绍, 简单的说就是如果发送方发送数据一段时间后没有收到ACK, 那么就重发数据
- 连接管理: 三次握手和四次挥手, 参见上文内容
- 流量控制: 参见下面的
滑动窗口
- 拥塞控制: 参见下面的
拥塞控制
ARQ 协议
ARQ 就是所谓的超时重传机制, 通过确认和超时机制保证了数据的正确送达, ARQ 协议包含停止等待的ARQ
和连续ARQ
.
停止等待的 ARQ
-
正常传输过程: 只要 A 向 B 发送一短报文, 都要停止发送并启动一个定时器, 等待对端回应, 在定时器时间内接收到对端应答就取消定时器并发送下一段报文
-
报文丢失或出错: 在报文传输的过程中可能会出现丢包。这时候超过定时器设定的时间就会再次发送丢包的数据直到对端响应,所以需要每次都备份发送的数据。即使报文正常的传输到对端,也可能出现在传输过程中报文出错的问题。这时候对端会抛弃该报文并等待 A 端重传。PS:一般定时器设定的时间都会大于一个
RTT
(Round-Trip Time, 往返时间)的平均时间。 -
ACK超时或丢失: 对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。
这个协议的缺点就是传输效率低,在良好的网络环境下每次发送报文都得等待对端的ACK 。
连续 ARQ
在连续 ARQ 中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待 ARQ 协议来说减少了等待时间,提高了效率。
累计确认
连续 ARQ 中,接收端会持续不断收到报文。如果和停止等待 ARQ 中接收一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文以后统一回复一个应答报文。报文中的 ACK 可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请发送这个序号 + 1 的数据。
但是累计确认也有一个弊端。在连续接收报文时,可能会遇到接收到序号 5 的报文后,并未接到序号 6 的报文,然而序号 7 以后的报文已经接收。遇到这种情况时,ACK 只能回复 6,这样会造成发送端重复发送数据,这种情况下可以通过 Sack 来解决,这个会在下文说到。
滑动窗口
在上小节中的发送窗口, 在两端都有这样一个窗口, 分别为发送端窗口和接受端窗口.
发送端窗口包含已发送单位收到应答的数据和可以发送但是未发送的数据:
发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。
当发送端接收到应答报文后,会随之将窗口进行滑动
滑动窗口实现了流量控制。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据。
Zero 窗口
在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,并启动persistent timer
。该定时器会定时发送请求给对端,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。
拥塞控制
拥塞处理和流量控制不同, 后者作用于接收方, 而拥塞空时是保证接收方来得及接受数据. 前者是作用于网络, 防止过多的数据拥塞网络, 避免出现网络负载过大的情况.
拥塞处理包括四个算法, 分别为: 慢开始, 拥塞避免, 快速重传, 快速恢复
慢开始算法
慢开始算法, 顾名思义, 就是在传输开始时将发送窗口慢慢指数级扩大, 从而避免一开始就传输大量数据导致网络拥塞
慢开始算法具体步骤如下:
- 连接初始设置拥塞窗口(Congestion Window)为 1MSS(一个分段的最大数据量)
- 每过一个 RTT 就将窗口大小 x2
- 指数级增长到一定的阈值就回启动
拥塞避免算法
拥塞避免算法
拥塞避免算法相较来说比较简单, 每过一个RTT
窗口大小只加1. 这样能够避免指数级增长导致网络拥塞, 慢慢将大小调整到最佳值.
在传输过程中可能定时器超时的情况, 这时候 TCP 会认为网络拥塞了, 会马上进行:
- 将阈值设置为当前拥塞窗口的一半
- 将拥塞窗口设为
1MSS
- 启动拥塞避免算法
快速重传
快速重传一般和快速恢复一起出现. 一旦接收端收到的报文出现失序的情况, 接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的情况下). 如果收到三个重复的 ACK, 无需等待定时器超时再重发而是启动快速重传. 具体算法分为两种:
TCP Tano :
- 将阈值设为当前拥塞窗口的一半
- 将拥塞窗口设为 1MSS
- 重新开始慢开始算法
TCP Reno:
- 拥塞窗口减半
- 将阈值设为当前拥塞窗口
- 进入快速恢复阶段(重发对端需要的包, 一般收到一个新的 ACK 答复就退出该阶段)
- 使用拥塞避免算法
TCP New Reno改进后的快速恢复:
TCP New Reno
算法改进了之前 TCP Reno 算法的缺陷. 在之前, 快速恢复中只要收到一个新的 ACK 包, 就会退出快回复. 在 TCP New Reno 中,TCP发送方先记下三个重复ACK的分段的最大序号.
TCP沾包
如果客户端连续不断的向服务端发送数据包的时候, 服务端接受的数据可能会出现两个数据包粘在一起的情况.
理由大致如下:
- TCP是基于字节流, 虽然应用层的TCP传输层之间的数据交互式大小不等的数据块, 但是TCP把这些数据块仅仅看成一连串无结构的字节流, 没有边界.
- 从TCP的帧结构出发, 在TCP的首部是没有表示数据长度的字段的
基于上面两点, 在使用TCP传输数据时, 才有粘包或者拆包的现象发生.
接收端收到了两个数据包, 但是这两个数据可能会多也可能不完整.
粘包的产生
- 发送方产生粘包
采用TCP协议传输数据的客户端与服务端经常保持一个长连接的状态, 双方在连接不断开的情况下, 可以一直传输数据. 但是当发送的数据包过小的时候, TCP会启用默认的Nagle算法, 将这些较小的数据包进行合并发送; 这个合并的过程是在发送缓冲区中进行的.
- 接收方产生粘包
接收方采用TCP协议接受数据时的过程是这样的:
- 数据到接收方, 从网络模型的下方传输到传输层
- 传输层的TCP协议处理是将其放置接受缓冲区, 然后由应用层来主动获取
这时会出现一个问题, 就是我们在程序中调用的读取数据函数不能及时的把缓冲区中的数据拿出来, 而下一个数据而到来并有一部分放入的缓冲区末尾, 等读取数据的时候就一个粘包. (放数据的速度 > 应用层拿数据速度)
解决粘包和拆包
- 特殊字符控制
- 在包头首部添加数据包的长度