传输层(Transport Layer)是ISO OSI协议的第四层协议, 实现端到端的数据传输.
比较常见的传输层协议是UDP
和TCP
.
用户数据报协议(UDP)
UDP是一个简单的传输层协议. 应用进程往UDP套接字写入一个消息, 该消息随后被封装到一个UDP数据报, 该UDP数据报进而又被封装到一个IP数据报, 然后发送到目的地.
UDP不保证UDP数据报会到达其最终目的地, 不保证各个数据报的先后顺序跨网络后保持不变, 也不保证每个数据报只到达一次.
使用UDP进行网络编程所通到的问题是它缺乏可靠性. 如果一个数据报到达了其最终目的地, 但是校验和检测发现有错误, 或者该数据报在网络传输途中被丢弃了, 它就无法被投递给UDP套接字, 也不会被源端自动重传. 如果想要确保一个数据报到达其目的地, 可以往应用程序中添置一大堆的特性: 来自对端的确认, 本端的超时与重传等.
传输控制协议(TCP)
TCP是一个面向连接的协议, 为用户进程提供可靠的全双工字节流.
TCP提供客户与服务器之间的连接. TCP客户端先与某个给定服务器建立一个连接, 再跨该连接与那个服务器交换数据, 然后终止这个连接.
TCP提供了可靠性. 当TCP向另一端发送数据时, 它要求对端返回一个确认. 如果没有收到确认, TCP就自动重传数据并等待更长时间.在数次重传失败后, TCP才放弃.
TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序. 举例来说, 假设一个应用写2048字节到一个TCP套接字, 导致TCP发送2个分节: 第一个分节所会数据的序列号为1一1024, 第二个分节所售数据的序列号为1025一2048.(分节是TCP传递给IP的数据单元) 如果这些分节非顺序到达, 接收端TCP将先根据它们的序列号重新排序, 再把结果数据传递给接收应用. 如果接收端TCP接收到来自对端的重复数据譬如说对端认为一个分节已丢失并因此重传, 而这个分节并没有真正丢失, 只是网络通信过于拥挤, 它可以(根据序列号)判定数据是重复的, 从而丢弃重复数据.
TCP提供流量控制. TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据, 这称为滑动窗口(advertised window). 在任何时刻, 该窗口指出接收组冲区中当前可用的室间量, 从而确保发送端发送的数据不会使接收缓冲区溢出. 该窗口时刻动态变化: 当接收到来自发送端的数据时, 窗口大小就减小, 但是当接收应用从缓冲区中读取数据时, 窗口大小就增大.
TCP满足大部分互联网业务的需求, 是目前最主要应用的传输层协议, 但它存在几个问题:
- 网络迁移需要重新建立TCP连接, 比如从蜂窝网络切换为Wi-Fi, 需要重新建立连接.
- 低优先级数据的丢失干扰高优先级数据的接收, 或同等优先级数据数据流相互干扰, 也就是常说的队头堵塞问题.因为TCP是字节流协议, 必须保证收到的字节数据是完整且有序的, 如果序列号较低的分节在网络传输中丢失了, 即使序列号较高的分节已经被接收了, 应用程序也无法读取到这部分数据.
流控制传输协议(SCTP)
SCTP在客户端和服务器之间提供关联, 并像TCP那样给应用提供可靠性、排序、流量控制以及全双工的数据传送.
SCTP中使用“关联”一词取代“连接”是为了避免这样的假设: 一个连接只涉及两个了了地址之间的通信. 一个关联指代两个系统之间的一次通信, 它可能因为SCTP支持多宿而涉及不止两个地址.
与TCP不同的是, SCTP是面向消息的. 它提供各个记录的按序递送服务. 与UDP一样, 由发送端写入的每条记录的长度随数据一道传递给接收端应用.
SCTP能够在所连接的端点之间提供多个流, 每个流各自可靠地按序递送消息. 一个流上某个消息的丢失不会阻塞同一关联其他流上消息的投递. 这种做法与TCP正好相反, 就TCP而言, 在单一字节流中任何位置的字节丢失都将阻塞该连接上其后所有数据的递送, 直到该丢失被修复为止.
SCTP还提供多宿特性, 使得单个SCTP端点能够支持多个IP地址. 该特性可以增强应对网络故障的健壮性. 一个端点可能有多个元余的网络连接, 每个网络又可能有各自接入因特网基础设施的连接. 当该端点与另一个端点建立一个关联后, 如果它的某个网络或某个跨越因特网的通路发生故障, SCTP就可以通过切换到使用已与该关联相关的另一个地址来规避所发生的故障.
SCTP问题是并不被所有的操作系统默认支持, 如果使用SCTP可能需要给客户机器安装相关的驱动.
可靠UDP(RUDP)
为了解决TCP存在的问题, 应用程序使用UDP实现了面向连接的协议, 称为可靠UDP(RUDP
), 比较出名的实现是QUIC
协议.它有以下优点:
- 使用 UDP 协议, 不需要三次连接进行握手, 而且也会缩短 TLS 建立连接的时间.
- 连接能够平滑迁移.
- 实现动态可插拔, 在应用层实现了拥塞控制算法, 可以随时切换.
- 多路复用, 解决了队头阻塞问题.
QUIC
协议是非常好的协议, 在移动互联网上开始大量使用, 并产生了不错的效果.
但游戏领域有一些独特的业务场景, 比如过时游戏状态的重传问题, 试想一下:
玩家了穿越整个地图去射击玩家A. 她开始的时候在位置x=0, 在随后的5秒钟, 跑向位置x=100. 服务器每秒向玩家A发送5个数据包, 每个数据包包含玩家了最新位置的x坐标.
如果服务器发现这些数据包中的任何一个丢失了, 那么都会重传. 这意味着当玩家B接近她的最终位置x=100时, 服务器可能还在重传过时的玩家B在x=0附近的状态数据. 这导致玩家A看到的玩家B位置是非常过时的, 在收到玩家B靠近的信息之前就已经被射中了.
Unreal传输层
针对这种业务场景, Unreal传输层提供了类似“最终一致性”的传输方案, 主要用于属性复制.
这种基于UDP实现游戏的传输层, 需要支持可靠传输. 可靠性的首要要求是, 有能力知道数据包是否到达目的地.
要做到这一点, 需要创建某种形式的传递通知模块. 该模块的任务是帮助依赖它的模块将数据包发送到远程主机, 然后通知这些模块数据是否到达. 由这些模块仅重传需要重传的数据.
- 当传输时, 必须唯一标识和标记每个发送出去的数据包, 这样可以将传递状态与每个数据包关联, 并将这个状态以一种有意义的方式传递给依赖模块.
- 在接收端, 必须检查传入的数据包, 并针对每个它决定处理的数据包发送一个确认.
- 回到发送端, 必须处理传入的确认, 并通知依赖模块哪个数据包被接收了和哪个数据包被丢弃了.
该模块也要保证了数据包不会被乱序处理. 如果旧的数据包在新数据包之后到达, 会丢弃这个数据包. 这样可以防止了新的数据包中的新数据被包含在旧数据包中的过时数据覆盖.
总结一下, Unreal传输层相较于TCP有着以下优点:
- 应用层建立连接, 断线重连业务无感知.
- 支持多路复用, 不同通道之间无影响.
- 可靠数据和不可靠数据有序执行, 发送端知道哪些数据接收端没有执行
后续将分析一下Unreal传输层是如何实现的, 以及有哪些改进方案.