传输层概述
1. 传输层服务概述
传输层协议为运行在不同Host上的进程提供了一种逻辑通信机制。
端系统运行传输层协议:
- 发送方:将应用递交的消息分成一个或多个的segment,并向下传给网络层
- 接收方:将接受到的segment组装成消息,并上交给网络层
网络层 VS 传输层:
-
网络层:提供主机之间的逻辑通信机制
-
传输层:提供应用程序之间的逻辑通信机制
+++
2. 多路复用/解复用
如果某层的一个协议直接对应上层的多个协议/实体,则需要复用/分用。
**多路分用:**传输层根据头部信息将收到segment交给正确的socket,即不同的进程
**多路复用:**从多个Socket接收数据,为每块数据封装上头部信息,生成segment,交给网络层。
2.1 多路分用的工作原理
主机收到IP数据报:
- 每个数据报携带源IP地址、目的IP地址
- 每个数据报携带一个 传输层的段(segment)
- 每个段携带源端口号和目的端口号(以此标识)
无连接分用:
- 利用端口号创建Socket
- UDP的Socket用二元组标识(目的IP地址、目的端口号)
- 主机收到UDP段后,检查段中目的端口号,将该UDP段绑定在该端口号Socket
面向连接的分用:
- TCP的Socket用四元组标识(源IP地址、源端口号、目的IP地址、目的端口号)
- 服务器可能支持多个TCP Socket,每个Socket用自己的四元组标识,web服务器为每个客户端开不同的Socket
+++
3. UDP协议
3.1 UDP概述
UDP(user datagram protocol)协议基于Internet IP协议,采用了
- 复用/分用
- 简单的错误校验?为什么在存在链路层错误检测的前提下,还要做错误校验
- 并不是所有链路层协议都存在错误检测机制
- 在路由器存储转发的过程中,也可能出错
UDP提供的“尽力而为”的服务,UDP端可能丢失,也可能乱序到达
UDP是无连接的,发送方/接收方之间不需要握手,每个UDP段的处理独立于其他段
UDP常用于流媒体应用,还用于DNS/SNMP
UDP为什么存在?
- 无需建立连接(减少延迟)
- 实现简单:无需维护连接状态
- 头部开销少
- 没有拥塞控制,应用可以更好的控制发送时间的效率
3.2 UDP报文格式
- 源端口/目的端口
- 长度:整个UDP报文的长度
- 检验和:在进行检验和计算时,会添加一个伪首部一起进行运算。
- 伪首部:伪首部(占用12个字节)为:4个字节的源IP地址、4个字节的目的IP地址、1个字节的0、一个字节的数字17、以及占用2个字节UDP长度。引入只是为了计算校验和
3.3 RUDP
3.3.1 描述
**可靠用户数据报协议(RUDP)**是一种基于可靠数据协议(RDP: RFC908 和 1151 (第二版))的简单分组传输协议。作为一个可靠传输协议,RUDP 用于传输 IP 网络间的电话信号。它允许独立配置每个连接属性,这样在不同的平台可以同时实施不同传输需求下的协议。
在实时通信过程中,不同的需求场景对可靠的需求是不一样的,我们在这里总体归纳为三类定义:
- 尽力可靠:通信的接收方要求发送方的数据尽量完整到达,但业务本身的数据是可以允许缺失的。例如:音视频数据、幂等性状态数据。;
- 无序可靠:通信的接收方要求发送方的数据必须完整到达,但可以不管到达先后顺序。例如:文件传输、白板书写、图形实时绘制数据、日志型追加数据等;
- 有序可靠:通信接收方要求发送方的数据必须按顺序完整到达。
RUDP 是根据这三类需求和上节图中的三角制约关系来确定自己的通信模型和机制的,也就是找通信的平衡点。
3.3.2 重传模式
IP 协议在设计的时候就不是为了数据可靠到达而设计的,所以 UDP 要保证可靠,就依赖于重传,这也就是我们通常意义上的 RUDP 行为。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(im***4AbwUz-1631114078440)(…/…/images/计网传输层RUDP架构.PNG)]
RUDP 分为发送端和接收端,每一种 RUDP 在设计的时候会做不一样的选择和精简,概括起来就是图中的单元。RUDP 的重传是发送端通过接收端 ACK 的丢包信息反馈来进行数据重传,发送端会根据场景来设计自己的重传方式,重传方式分为三类:定时重传、请求重传和 FEC 选择重传。
定时重传
定时重传很好理解,就是发送端如果在发出数据包(T1)时刻一个 RTO 之后还未收到这个数据包的 ACK 消息,那么发送端就重传这个数据包。这种方式依赖于接收端的 ACK 和 RTO,容易产生误判,主要有两种情况:
- 1)对方收到了数据包,但是 ACK 发送途中丢失;
- 2)ACK 在途中,但是发送端的时间已经超过了一个 RTO。
所以超时重传的方式主要集中在 RTO 的计算上,如果你的场景是一个对延迟敏感但对流量成本要求不高的场景,就可以将 RTO 的计算设计得比较小,这样能尽最大可能保证你的延时足够小。
例如:实时操作类网游、教育领域的书写同步,是典型的用 expense 换 latency 和 quality 的场景,适合用于小带宽低延迟传输。如果是大带宽实时传输,定时重传对带宽的消耗是很大的,极端情况会有 20% 的重传率,所以在大带宽模式下一般会采用请求重传模式。
请求重传
请求重传就是接收端在发送 ACK 的时候携带自己丢失报文的信息反馈,发送端接收到 ACK 信息时根据丢包反馈进行报文重传。
整个请求重传机制依赖于 jitter time 和 RTO 这个两个时间参数,评估和调整这两个参数和对应的传输场景也息息相关。请求重传这种方式比定时重传方式的延迟会大,一般适合于带宽较大的传输场景,例如:视频、文件传输、数据同步等。
FEC选择重传
除了定时重传和请求重传模式以外,还有一种方式就是以 FEC 分组方式选择重传,FEC(Forward Error Correction)是一种前向纠错技术,一般通过 XOR 类似的算法来实现,也有多层的 EC 算法和 raptor 涌泉码技术,其实是一个解方程的过程。
4. 可靠数据传输(网络top-10问题)
4.3 Rdt2.0:产生错误的信道
Rdt2.0中引入的新机制:差错检测、接收方反馈控制消息(ACK/NAK)、重传
底层信道可能翻转分组中的位:
- 利用校验和检测位错误
如何从错误中恢复:
- 确认机制(Acknowledgements,ACK):接收方显示地告知发送方分组已正确接收
- NAK:接收方显示地告知发送方分组有错误
- 发送方收到NAK后,重传分组
基于上述重传机制的rdt协议称为ARQ协议
4.4 Rdt2.1和Rdt2.2:确认信号出现错误
Rdt2.1:
为了应对Rdt2.0的ACK/NAK破坏情况,rdt2.1引入了序列号机制,用于去除重复分组。
发送方为每个分组增加了序列号,因为使用的是停等机制,所以序列号只需0/1即可,另外还为NAK/ACK添加了校验和。
接收方需要判断分组是否重复,重复则丢弃。
Rdt2.2:
与rdt2.1功能相同,但只使用ack(接收方通过ack告知最后一个被正确接收的分组)
4.5 Rdt3.0:产生错误或丢失的信号
解决方案:发送方等待“合理”时间
- 如果没收到ACK(可能数据报丢失,也可能ACK丢失),重传
- 如果出现延迟,则会产生重复分组,但可以通过序列号解决
- 需要计时器
Rdt3.0性能分析:
因为Rdt3.0采用停等机制,存在端到端的传播延迟,因此性能很差
4.6 流水线机制与滑动窗口协议
流水线机制允许发送方在收到ACK之前连续发送多个分组
- 更大的序列号范围
- 发送方/接收方需要更大的存储空间以缓存分组
因此引入了滑动窗口协议:窗口大小为允许使用的序列号范围,最多有n个等待确认的消息,随着协议的运行,窗口在序列号空间内向前滑动。
4.7 滑动窗口协议——GBN协议
发送方:
-
发送方收到ACK(n),表示接收方确认序列号n(包含n)的分组均已被正确接收
-
超时TimeOut(n)事件:如果发送方未收到ACK(n),则重传序列号大于等于n的所有分组。可能造成资源浪费
接收方:
- 只需要记住唯一的expectedseqnum,如果期望的分组5,但是收到了分组7,直接丢弃分组7并发送ACK(4)
4.8 滑动窗口协议——SR协议
接收方对每一个分组单独进行确认:
- 设置缓存机制,缓存乱序到达的分组
- 设置接收方滑动窗口,缓存收到的乱序分组
发送方只重传没收到ack的分组:
- 为每个分组设置特有定时器
滑动窗口大小与序列号大小带来的困境:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z70yAiHD-1631114078446)(…/…/images/计算机网络可靠数据传输窗口协议SR困境.PNG)]
5. TCP协议
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
为什么要面向连接
因为TCP是一个可靠的传输协议,而建立连接的意义在于让通信两端确定信息是可以传输的,如果没有建立连接,那么可能有一方就不会接收或响应,另一方则会进行多次超时重传,极大的浪费了客户端的资源。因此想要实现可靠传输就必须建立连接。
为什么要基于字节流
我们只要,TCP是面向连接的,也就是说,在连接持续的过程中,socket中收到的数据都是由同一台主机发出的(劫持什么的不考虑),因此只要保证数据有序到达就可以了(通过序列号实现),至于每次发送多少,则视网络情况而定,数据报太大则要拆分,太小则可以合并。
TCP协议的主要功能是确保两个端口间数据的可靠传输,通过校验和、序列号、确认应答、重发控制、连接管理、拥塞控制等机制实现。TCP是全双工的。
5.1 TCP报文格式
- 端口号:
- 源端口:源端口和IP地址的作用是标识报文的返回地址。
- 目的端口:端口指明接收方计算机上的应用程序接口。
- 序列号:指本报文段发送的数据组的第一个字节的序号
- 确认号(acks):指明下一个期待收到的字节序号
- 数据偏移/首部长度:指明首部的长度,因为除了20字节的固定首部外,还有可选首部选项
- 控制位:控制首部中某些位置是否有效等
- 窗口大小:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制
- 校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得
- 紧急指针:TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式
- 可选首部:
5.2 TCP连接管理
TCP是面向连接的协议,因此在进行数据传输时需要进行三次握手,而断开连接则需要四次挥手。为了加深记忆,下面将以linux内核的角度来理解连接管理。
5.2.1 三次握手
描述
-
前提:前面提到了TCP是可靠的、基于字节流的。现在我们举个例子,我和面试官您是TCP通信的双方,我是客户端,您是服务端,我要和你聊天,聊天的前提是你要在听,在linux服务器层面就是为此端口要维护两个队列,半连接队列和全连接队列。此时服务器才能能够处理客户端的请求,处于Listen状态
-
第一次握手:现在我对您说话,为了避免您没有听到,我就得设置一个计时器,并且对话是基于字节流的,因此我需要告诉你我说的是第几句,这样你才好接收,过了多长时间您没有回复我就重说一下。这就是客户端的报文超时计时器,客户端进入SYN_SENT状态
-
第二次握手: 现在您听到了我说话,但是有以下几种可能:您正在和别人聊天没空、您有空。
- 您没空,就直接不理我。在服务器端就是查询半连接队列和syn_cookies,
- 如果队列满且未开启cookies,则直接不理我,
- 如果队列满但开启cookies,则判断全连接是否满,
- 如果满,则直接丢弃
- 如果不满,则回复我
- 您有空,则直接回复我
现在你回复我,说我听到了你上一句说的啥,你继续说。就是ack = seq + 1。
- 您没空,就直接不理我。在服务器端就是查询半连接队列和syn_cookies,
-
第三次握手: 现在您回复我了,我收到了,我也要回复您,如果不回复,则会出现很严重的后果。
- 我没听到您说话或者我说话您没听到,但是您认为连接已经建立了,还一直在等我说话,浪费资源
- 由于信号不好,我发起了好几次建立连接请求,有一次在关闭连接之后终于到达了您,您就等我说话我却没有说,也是浪费资源
现在我要回复您,自然也就是说我听到您上一句说的啥,我接着我之前说过的说啊,balaba。
总结
A、B关闭状态CLOSED——B收听状态LISTEN——A同步已发送状态SYN-SENT——B同步收到状态SYN-RCVD——A、B连接已建立状态ESTABLISHED
服务器Listen()
在服务器 listen 的时候,主要是进行了全/半连接队列的长度限制计算,以及相关的内存申请和初始化。全/连接队列初始化了以后才可以相应来自客户端的握手请求。否则没有内存或者资源来处理连接请求。
客户端connect()
现在客户端发起连接请求,客户端在 connect 的时候,把本地 socket 状态设置成了 TCP_SYN_SENT,选了一个可用的端口,接着发出 SYN 握手请求并启动重传定时器。
服务器响应ack
连接请求到达服务器后,经过网卡、软中断进入对应的socket。在这里首先判断半连接队列是否满了,如果满了的话进入 tcp_syn_flood_action 去判断是否开启了 tcp_syncookies 内核参数。如果队列满,且未开启 tcp_syncookies(一种防范syn攻击的机制),那么该握手包将直接被丢弃!!
如果半连接未满或开启了syncookies,接着还要判断全连接队列是否满。因为全连接队列满也会导致握手异常的,那干脆就在第一次握手的时候也判断了。如果全连接队列满了,且有 young_ack 的话,那么同样也是直接丢弃。
接下来是构造 syn_ack 包,然后通过 ip_build_and_send_pkt 把它发送出去。
**最后把当前握手信息添加到半连接队列,并开启计时器。**计时器的作用是如果某个时间之内还收不到客户端的第三次握手的话,服务器会重传 synack 包。
客户端响应synack
客户端收到服务器的ack后,也会构造ack包并发送出去,客户端响应来自服务器端的 synack 时清除了 connect 时设置的重传定时器,把当前 socket 状态设置为 ESTABLISHED,开启保活计时器后发出第三次握手的 ack 确认。
服务端响应ack
服务器响应第三次握手 ack 所做的工作是把当前半连接对象删除,创建了新的 sock 后加入到全连接队列中,最后将新连接状态设置为 ESTABLISHED。
5.3.2 四次挥手
- 数据传输结束后,通信的双方都可释放连接,A和B都处于ESTABLISHED状态。此时需要断开连接。
描述
- 第一次挥手:现在我还有事,不想聊了,我就和面试官您说。客户端发送FIN报文,进入FIN_WAIT_1状态
- 第二次挥手:面试官您听到了,回复我:我听到了,但是我还没说完,你等我说完。服务端进入CLOSE_WAIT状态。我听到了,我就等你说完。客户端进入FIN_WAIT_2状态
- 第三次挥手:面试官您终于说完了,于是对我说:我说完了。服务端发送FIN报文,进入LAST_ACK状态
- 第四次挥手:我听到您说说完了,我就回复好的。A进入TIME_WAIT状态
- 您听到了我的回复,就忙别的去了。服务端进入CLOSE状态
- 我为了确认您收到了我的回复,于是我等了2MSL(最大报文存活时间)才进入CLOSE状态,防止您没收到再次问我时我能及时回复您,避免浪费您的时间。
总结
A、B打开状态ESTABLISHED——A终止等待FIN_WAIT+_1——B关闭等待Close_WAIT——A终止等待FIN_WAIT_2——B最后确认LAST_ACK——A时间等待TIME_WAIT——B关闭——A关闭
5.3.3 常见问题
为什么需要三次握手,而不是两次?
存在无法建立连接(死锁)问题:如果B请求连接A,A答应连接并发送确认应答分组,此时A默认连接建立了。假如此时B没有收到确认应答分组,B认为没有建立连接,将不会收到A发来的任何分组除了连接确认应答分组,而A继续发送数据分组,因此无法正确连接。
因为TCP连接是全双工通信,我在发送数据的同时也要接收数据,因此必须知道tcp包的起始序号。TCP 需要 seq 序列号来做可靠重传或接收,而避免连接复用时无法分辨出 seq 是延迟或者是旧链接的 seq,因此需要三次握手来约定确定双方的 ISN(初始 seq 序列号)。
为什么需要四次挥手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET(可能此时还有文件要传输),所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
但是在某种情况下,四次挥手可能变成三次挥手。
TCP是全双工通信,Cliet在自己已经不会在有新的数据要发送给Server后,可以发送FIN信号告知Server,这边已经终止Client到对端Server那边的数据传输。但是,这个时候对端Server可以继续往Client这边发送数据包。于是,两端数据传输的终止在时序上是独立并且可能会相隔比较长的时间,这个时候就必须最少需要2+2 = 4 次挥手来完全终止这个连接。
但是,如果Server在收到Client的FIN包后,在也没数据需要发送给Client了,那么对Client的ACK包和Server自己的FIN包就可以合并成为一个包发送过去,这样四次挥手就可以变成三次了(似乎linux协议栈就是这样实现的)。
为什么TIME_WAIT状态需要经过2MSL才达到CLOSE状态?
-
可靠地实现TCP全双工连接的终止
- 在进行关闭连接四次挥手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,服务器将重发最终的FIN,因此客户端必须维护状态信息允许它重发最终的ACK。
-
允许老的重复分节在网络中消逝
- 当出现关闭一个连接后,又马上建立另一个连接,此时可能前一个连接的迷途重复分组到达了,被当前连接误解成当前连接的化身,导致连接被关闭。因此TCP的Time_wait持续2ml,就可以保证建立一个TCP连接时前面分组已经在网络中消失。
TCP连接的初始序列号(ISN)能否固定
不能,
- 假设ISN固定是1,Client和Server建立好一条TCP连接后,Client连续给Server发了10个包,这10个包不知怎么被链路上的路由器缓存了(路由器会毫无先兆地缓存或者丢弃任何的数据包),这个时候碰巧Client挂掉了;
- 然后Client用同样的端口号重新连上Server,Client又连续给Server发了几个包,假设这个时候Client的序列号变成了5;
- 接着,之前被路由器缓存的10个数据包全部被路由到Server端了,Server给Client回复确认号10,这个时候,Client整个都不好了,这是什么情况?我的序列号才到5,你怎么给我的确认号是10了,整个都乱了
建议ISN和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始,**这需要4小时才会产生ISN的回绕问题,这几乎可以保证每个新连接的ISN不会和旧的连接的ISN产生冲突。**这种递增方式的ISN,很容易让攻击者猜测到TCP连接的ISN,现在的实现大多是在一个基准值的基础上进行随机的。
初始化连接的SYN超时问题
也就是半连接问题,服务端接收到客户端的连接建立请求,回复ACK给客户端后,客户端未响应。目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了.所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会把断开这个连接。
因此,给攻击者留下一个攻击机会,对于SYN过多的问题,linux提供了几个TCP参数来调整应对。
time_wait过多的问题
在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接。这个场景下会出现大量socket处于TIME_WAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。
**快速回收:**Linux实现了一个TIME_WAIT状态快速回收的机制,即无需等待两倍的MSL这么久的时间,而是等待一个Retrans时间即释放,也就是等待一个重传时间(一般超级短,以至于你都来不及能在netstat -ant中看到TIME_WAIT状态)随即释放。释放了之后,一个连接的tuple元素信息就都没有了
**重用:**如果说TIME_WAIT回收只是一种特定系统的优化实现的话,那么TIME_WAIT重用则有相关的规范,即:如果能保证以下任意一点,一个TIME_WAIT状态的四元组(即一个socket连接)可以重新被新到来的SYN连接使用:
-
1.初始序列号比TIME_WAIT老连接的末序列号大
-
2.如果使能了时间戳,那么新到来的连接的时间戳比老连接的时间戳大
-
上述重用仅仅当处于同一端口号的才能重用
5.2 TCP可靠传输
为了确保数据包的可靠传输,TCP在IP层提供的不可靠服务基础上实现可靠数据传输服务。由于TCP数据包在传输过程中可能出现丢包、粘包、乱序到达,TCP引入了序列号和确认号标记数据包在原数据中的位置、引入了计时器来实现超时重传、引入了流水线机制来提高传输效率。
-
SEQ(序列号):序列号指segment中第一个字节的编号,而不是segment的编号
-
ACKS(确认号):希望接收到的下一个字节的序列号
-
累计确认:TCP在每次响应请求时,都会告诉其想要的下一个报文段
-
TCP使用单一重传定时器(设置超时时间:使用平均RTT,并采用指数加权平均移动 E R T T = ( 1 − a ) ∗ 历 史 E R T T + a ∗ 平 均 R T T ERTT = (1 - a) *历史 ERTT + a * 平均RTT ERTT=(1−a)∗历史ERTT+a∗平均RTT)
TCP数据传输过程
- 发送方:发送一个数据报,里面记录着当前报文的序列号seq、发送方需要的报文序列号ack、发送方滑动窗口的大小等,并设计计时器
- 接收方:根据发送方的ack发送ack+1,然后请求发送方的seq+1,报文长度不能大于发送方滑动窗口大小
- 发送方:如果发生超时(接口方长时间未回复)或通过重复ACK检测到分组丢失触发快速重传机制。
TCP怎么保证可靠传输
首先,应用数据被TCP分割为最合适发送的数据块,并对数据块(包)进行编号,然后接收端依据编号进行组装,为了确保数据不被篡改,TCP引入了校验和机制,并且为了平衡两端的数据处理能力引入了流量控制,为了避免网络拥塞使用了拥塞控制,此外,引入了确认应答标志和计时器,当TCP报文段的计时器到时或收到要求ACK,则立即重发此报文,这就是超时重传(快重传)机制。
5.3 TCP流量控制
我们知道,TCP收到数据并不是直接交给上层协议,而是先存储在缓存中,如果在某时刻到来大量数据而缓存又无法存在时,必定会发生数据丢失,因此TCP维护了一个滑动窗口来实时告诉发送方。
缓存buffer可用空间 R c v W i n d o w = R c v B u f f e r − ( L a s t B y t e R c v d − L a s t B y t e R e a d ) RcvWindow = RcvBuffer - (LastByteRcvd - LastByteRead) RcvWindow=RcvBuffer−(LastByteRcvd−LastByteRead)
-
接收方在segment头部字段将 R c v W i n d o w RcvWindow RcvWindow告诉发送方
-
发送方限制自己已发送但还未收到ACK的数据不超过 R c v W i n d o w RcvWindow RcvWindow
-
如果 R c v W i n d o w RcvWindow RcvWindow为0,发送方仍能发送一个很小的报文段,再收到新的 R c v W i n d o w RcvWindow RcvWindow
5.3.1 滑动窗口
这个窗口是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
发送窗口
**发送方要知道那些可以发,哪些不可以发,一个简明的方案就是按照接收方的窗口通告,发送方维护一个一样大小的发送窗口就可以了,**在窗口内的可以发,窗口外的不可以发,窗口在发送序列上不断后移,这就是TCP中的滑动窗口。
但是,如果接收方没有能力处理数据了,发送端的发送窗口也就变成了0,也就是发送端不能发数了。如果发送端一直等待,直到接收端通知一个非零窗口在发数据的话,这似乎太受限于接收端,如果接收端一直不通知新的窗口呢?
显然发送端不能干等,起码有一个主动探测的机制。**为解决0窗口的问题,TCP使用了Zero Window Probe技术,缩写为ZWP。**发送端在窗口变成0后,会发ZWP的包给接收方,来探测目前接收端的窗口大小,一般这个值会设置成3次,每次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST报文来关掉这个连接。正如有人的地方就会有商机,那么有等待的地方就很有可能出现DDoS攻击点。攻击者可以在和Server建立好连接后,就向Server通告一个0窗口,然后Server端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把Server端的资源耗尽。
5.4 TCP拥塞控制
如果当前太多主机发送了太多数据或发送数据太快,以至于网络无法处理。就会出现分组丢失、分组延迟过大等现象,因此我们在使用网络进行通信时必须避免网络出现拥塞,通常情况下是减少发送的数据量等。
5.4.1 TCP Tahoe
Tahoe采用了慢启动、拥塞避免、快速恢复的算法使得拥塞窗口在发生拥塞时迅速减为1,慢启动门限降为一半,虽然避免了网络拥塞,但也使得性能大大的降低了。
5.4.2 TCP Reno
Reno算法包含4个部分: 慢热启动、拥塞避免、快速重传、快速恢复。
慢热启动
慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络情况,然后在慢慢提速。不要一上来就拼命发包,这样很容易造成链路的拥堵,出现拥堵了在想到要降速来缓解拥堵这就有点成本高了,毕竟无数的先例告诫我们先污染后治理的成本是很高的。
-
连接建好的开始先初始化cwnd = N,表明可以传N个MSS大小的数据。
-
每当收到一个ACK,++cwnd; 呈线性上升
-
每当过了一个RTT,cwnd = cwnd * 2; 呈指数让升
-
还有一个慢启动门限ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入"拥塞避免算法 - Congestion Avoidance"。
拥塞避免
慢启动的时候说过,cwnd是指数快速增长的,但是增长是有个门限ssthresh(一般来说大多数的实现ssthresh的值是65535字节)的,到达门限后进入拥塞避免阶段。
- 每收到一个ACK,调整cwnd 为 (cwnd + 1/cwnd) * MSS个字节;
- 每经过一个RTT的时长,cwnd增加1个MSS大小。
快速重传
快重传算法首先要求接收方每收到一个失序的报文段就立即发出重复确认(为的是使发送方及早的知道有报文段没有到达对方)而不要等到自己发送数据时才捎带确认。
快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待为其设置的重传计时器到期。
当传输速率达到一个峰值时,网络开始拥塞,此时就会出现一些症状,比如报文定时器超时、或者重复ack等
- 出现RTO超时,重传数据包。这种情况下,TCP就认为出现拥塞的可能性就很大,于是它反应非常’强烈’:
- 调整门限ssthresh的值为当前cwnd值的1/2;
- reset自己的cwnd值为1;
- 然后重新进入慢启动过程。
- 在RTO超时前,收到3个duplicate ACK进行重传数据包。此时认为数据包在网络中丢失,但网络并不是十分拥塞,此时快速重传此报文,并触发快恢复机制。
快恢复
由于发送方现在认为网络很可能没有发生拥塞(如果网络发生了严重拥塞,就不会一连有好几个报文段连续到达接收方,也就不会导致接收方连续发送重复确认)。因此与慢开始不同之处就是现在不执行慢开始算法(即拥塞窗口现在不设置为1),而是把拥塞窗口的值设置为慢开始门限减半后的值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
快速恢复做的事情有:
- 调整门限ssthresh的值为当前cwnd值的1/2;
- 将cwnd值设置为新的ssthresh的值;
- 重新进入拥塞避免阶段。
5.4.3 New Reno算法
TCP Reno 提出的快速恢复算法提高了丢失报文后的吞吐量和顽健性,但是:**Reno仅考虑了每次拥塞发生时只丢失一个报文的情形。**实际网络中,一旦发生拥塞,路由器会丢弃大量的报文,即一次拥塞中丢失多个报文的情形很普遍。
此时若采用Reno算法,则会多次将拥塞窗口(cwnd)和慢启动阈值(ssthresh)减半,造成TCP的发送速率呈指数降低系统吞吐量急剧下降,(当发送窗口小于3时)无足够的重复ACK可以触发快速恢复,只能等待超时重传。TCP Reno 终端会陷入仅通过传输超时来发现报文丢失的困境中。
NewReno TCP在Reno TCP的基础上对快速恢复算法进行修改,只有一个数据包丢失的情况下,其机制和Reno是一样的;当同时有多个包丢失时就显示出了它的优势。
- Reno快速恢复算法中发送方收到一个新的ACK就退出快速恢复状态
- New Reno算法中只有当所有报文都被应答后才退出快速恢复状态。
因此New Reno能够避免一次拥塞中多次指数降低滑动窗口大小,可以较快地从拥塞中恢复。
虽然NewReno可以解决大量数据包遗失的问题,但是NewReno在每个RTT时间只能一个数据包遗失的错误。为了更有效地处理大量数据包遗失的问题,另一个解决方法就是让传送端知道哪些已经被接收端收到,但用此方法必须同时修改传送端和接收端的传送机制。
5.4.4 TCP SACK算法
前面提到,NewReno在每个RTT时间只能一个数据包遗失的错误,而如果发生大量数据包丢失的问题,Reno算法只能在每个RTT时间内至多重传一个丢弃的包(以为可能有的包已经在接收端,避免重复传送),而TCP SACK在TCP Reno基础上增加了:选择确认和选择重传。
当一个窗口内有多个数据包丢失时:
- 接收端:在ACK中报告其接收到的不连续的报文,使发送方准确地知道哪些数据包被接收方正确接收。
- 发送端:使用选择重传机制,可以在一个窗口中一次重传所有从一个窗口中丢失的数据包。
减少了时延,提高了网络吞吐量,使更快地从拥塞状态恢复。
SACK中加入了一个SACK选项(TCP option field),允许接收端在返回Duplicate ACK时,将已经收到的数据区段(连续收到的数据范围)返回给发送端,数据区段与数据区段之间的间隔就是接收端没有收到的数据。发送端就知道哪些数据包已经收到,哪些该重传,因此SACK的发送端可以在一个RTT时间内重传多个数据包。
SACK TCP恢复速度更快,对拥塞的处理更合理。
5.5 TCP与UDP的区别
从功能上说,TCP保证数据准确交付,UDP保证数据快速到达。因此,两者的实现都是基于实现功能来区别的。
- TCP是面向连接的,UDP是无连接的;
- TCP是可靠的,UDP是不可靠的;
- TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多的通信模式;
- TCP是面向字节流的,UDP是面向报文的;
- TCP有拥塞控制机制, UDP没有拥塞控制,适合媒体通信;
- TCP首部开销(20个字节)比UDP的首部开销(8个字节)要大;
5.6 TCP的保活机制
我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
而对于这个链接,根据什么时候关闭来区分长链接和短连接。
5.6.1 TCP的长短连接
- 短连接:C/S两端在数据发送完成后,则断开此TCP连接,下一次通信需要重新建立连接。
- 长连接:指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接(心跳机制)。
5.6.2 TCP的保活机制
在需要长连接的网络通信程序中,经常需要心跳检测机制,来实现检测对方是否在线或者维持网络连接的需要。这一机制是在应用层实现的,对应的,在TCP协议中,也有类似的机制,就是TCP保活机制,不同这是传输层在内核完成的。
如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:
-
客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务将保活定时器复位。
-
客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
-
客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
-
客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。
5.6.3 TCP保活机制与HTTP长链接的区别
HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。
TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。
##5.7 TCP粘包和拆包
5.7.1 概念
产生粘包和拆包问题的主要原因是,操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,
- 粘包:如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;
- 拆包:如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。
5.7.2 常见解决方案
-
固定包大小:客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度;
-
客户端在每个包的末尾使用固定的分隔符,例如\r\n,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,
-
将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
-
通过自定义协议进行粘包和拆包的处理。
6. 其他协议
6.1 KCP协议
TCP保证数据准确交付,UDP保证数据快速到达,KCP则是两种协议的一个折中。KCP的设计目标是为了解决在网络拥堵的情况下TCP传输速度慢的问题。
KCP没有规定下层传输协议,但通常使用UDP来实现,至于原因,非常有必要说明,如果不清楚,就不能够真正地了解KCP。
6.1.1 KCP报文的格式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dspsNArI-1631114078450)(…/…/images/计网传输层KCP报文的格式.PNG)]
6.1.2 KCP协议的特点
-
RTO不翻倍:RTO(Retransmission-TimeOut)即重传超时时间,TCP是基于ARQ协议实现的可靠性,KCP也是基于ARQ协议实现的可靠性,但TCP的超时计算是
RTO*2
,而KCP的超时计算是RTO*1.5
,也就是说假如连续丢包3次,TCP是RTO*8
,而KCP则是RTO* 3.375
,意味着可以更快地重新传输数据。 -
选择性重传:TCP中实现的是连续ARQ协议,再配合累计确认重传数据,只不过TCP重传时需要将最小序号丢失的以后所有的数据都要重传,而KCP则只重传真正丢失的数据。
-
快速重传: 与TCP相同,都是通过累计确认实现的,发送端发送了1,2,3,4,5几个包,然后收到远端的ACK:1,3,4,5,当收到ACK = 3时,KCP知道2被跳过1次,收到ACK = 4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。
-
非延迟ACK: TCP在连续ARQ协议中,不会将一连串的每个数据都响应一次,而是延迟发送ACK,即上文所说的UNA模式,目的是为了充分利用带宽,但是这样会计算出较大的RTT时间,延长了丢包时的判断过程
-
底层基于UDP实现,第一个原因是UDP的头部相对于TCP的头部来说更小,第二个原因是KCP相对于TCP来说有太多的冗余功能,这样白白导致了很多不必要的开销。
KCP协议在视频加速、直播推流、MOBA
游戏(多人在线战术竞技游戏)等场景中更是大放异彩。