基于UDP实现可靠传输

UDP要想可靠,就要接收方收到UDP之后回复个确认包,发送方有个机制,收不到确认包就要重新发送,每个包有递增的序号,接收方发现中间丢了包就要发重传请求,当网络太差时候频繁丢包,防止越丢包越重传的恶性循环,要有个发送窗口的限制,发送窗口的大小根据网络传输情况调整,调整算法要有一定自适应性。恭喜你, 你在应用层重新实现了TCP!来自知乎-UDP如何实现可靠传输?姚冬的回答
也就是需要序列号、确认应答、超时重传、以及发送窗口和拥塞窗口,这不就是TCP吗,为什么还要自己实现一遍呢?

为什么需要用UDP实现可靠传输

  1. TCP建立连接慢,需要三次握手,对于大量的短连接更加不划算
  2. 如果你建立一次通讯只为了传输很少量的一整块数据,那么明显是一种浪费。这也是为什么 google 的 QUIC 对传统的 http over TCP 有改善的空间。
  3. TCP主要慢在拥塞控制上,一是慢启动拥塞窗口初始值太小,二是丢包以后拥塞窗口变得太小,但这有一些新的拥塞控制算法bbr等
  4. RTO翻倍vs不翻倍:
    TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度。
  5. 选择性重传 vs 全部重传:
    TCP 的重传机制在数据包丢失时可能会重新传输已经成功接收的数据段,造成带宽的浪费;
    TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。
  6. 快速重传:
    发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 3,当第一次收到ACK3时,KCP知道2被跳过1次,收到再ACK3时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。(和TCP类似,只是从3次变成2次)
  7. 延迟ACK vs 非延迟ACK:
    TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。

Delay Ack
简单的说,Delay Ack就是延时发送ACK,在收到数据包的时候,会检查是否需要发送ACK,如果需要的话,进行快速ACK还是延时ACK,在无法使用快速确认的条件下,就会使用Delay Ack。

TCP在何时发送ACK的时候有如下规定:
1.当有响应数据发送的时候,ACK会随着数据一块发送
2.如果没有响应数据,ACK就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右,如果在40ms内有数据发送,那么ACK会随着数据一块发送,对于这个延迟的需要注意一下,这个延迟并不是指的是收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms就会检查一次,比如定时器在0ms启动,200ms到期,180ms的时候data来到,那么200ms的时候没有响应数据,ACK仍然会被发送,这个时候延迟了20ms.
3.如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!

优点:减少了数据段的个数,提高了发送效率
缺点:过多的delay会拉长RTT

  1. UNA vs ACK+UNA:
    ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。

连续ARQ协议不会响应每个数据段,而是仅仅响应编号最大的这个数据段,表示之前的数据都收到了,这个叫做UNA模式,而停等ARQ协议可以看作是ACK模式。

  1. 非退让流控:
    TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差;
    KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率(相当于忽略拥塞窗口)。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。

  2. 缓存控制:避免缓存积累延迟
    参考kcp-如何避免缓存积累延迟
    当前发送且没有得到 ACK/UNA确认的数据,都会滞留在发送缓存中,一旦滞留数据超过了发送窗口大小限制,则该链接的 tcp send 调用将会 被阻塞,或者返回:EAGAIN / EWOULDBLOCK,这时候说明当前 tcp 信道可用带宽已经赶不上你的发送速度了。
    可用带宽 = min(本地可用发送窗口大小,远端可用接收窗口大小) * (1 - 丢包率) / RTT
    重设窗口大小
    要解决上面的问题首先对你的使用带宽有一个预计,并根据上面的公式重新设置发送窗口和接收窗口大小。你写后端,想追求tcp的性能,也会需要重新设置tcp的 sndbuf, rcvbuf 的大小,KCP 默认发送窗口和接收窗口大小都比较小而已。

  • 不设置的话,如果默认 snd_wnd 太小,网络不是那么顺畅,你越来越多的数据会滞留在 snd_queue里得不到发送,你的延迟会越来越大。
  • 设定了 snd_wnd,远端的 rcv_wnd 也需要相应扩大,并且不小于发送端的 snd_wnd 大小,否则设置没意义。
    其次对于成熟的后端业务,不管用 TCP还是 KCP,你都需要实现相关缓存控制策略:
    缓存控制:传送文件
    你用 tcp传文件的话,当网络没能力了,你的 send调用要不就是阻塞掉,要不就是 EAGAIN,然后需要通过 epoll 检查 EPOLL_OUT事件来决定下次什么时候可以继续发送。
    KCP 也一样,如果 ikcp_waitsnd 超过阈值,比如2倍 snd_wnd,那么停止调用 ikcp_send,ikcp_waitsnd的值降下来,当然期间要保持 ikcp_update 调用。
    同时,如果你能做的更好点,waitsnd 超过阈值了,代表一段时间内网络传输能力下降了,此时你应该动态降低视频质量,减少码率,等网络恢复了你再恢复。
    缓存控制:游戏控制数据
    大部分逻辑严密的 TCP游戏服务器,都是使用无阻塞的 tcp链接配套个 epoll之类的东西,当后端业务向用户发送数据时会追加到用户空间的一块发送缓存,比如 ring buffer 之类,当 epoll 到 EPOLL_OUT 事件时(其实也就是tcp发送缓存有空余了,不会EAGAIN/EWOULDBLOCK的时候),再把 ring buffer 里面暂存的数据使用 send 传递给系统的 SNDBUF,直到再次 EAGAIN。
    那么 TCP SERVER的后端业务持续向客户端发送数据,而客户端又迟迟没能力接收怎么办呢?此时 epoll 会长期不返回 EPOLL_OUT事件,数据会堆积再该用户的 ring buffer 之中,如果堆积越来越多,ring buffer 会自增长的话就会把 server 的内存给耗尽。因此成熟的 tcp 游戏服务器的做法是:当客户端应用层发送缓存(非tcp的sndbuf)中待发送数据超过一定阈值,就断开 TCP链接,因为该用户没有接收能力了,无法持续接收游戏数据。
    使用 KCP 发送游戏数据也一样,当 ikcp_waitsnd 返回值超过一定限度时,你应该断开远端链接,因为他们没有能力接收了。
    但是需要注意的是,KCP的默认窗口都是32,比tcp的默认窗口低很多,实际使用时应提前调大窗口,但是为了公平性也不要无止尽放大(不要超过1024)。
    总结
    缓存积累这个问题,不管是 TCP还是 KCP你都要处理,因为TCP默认窗口比较大,因此可能很多人并没有处理的意识。
    当你碰到缓存延迟时:
  1. 检查 snd_wnd, rcv_wnd 的值是否满足你的要求,根据上面的公式换算,每秒钟要发多少包,当前 snd_wnd满足条件么?
  2. 确认打开了 ikcp_nodelay,让各项加速特性得以运转,并确认 nc参数是否设置,以关闭默认的类 tcp保守流控方式。
  3. 确认 ikcp_update 调用频率是否满足要求(比如10ms一次)。
  4. 当ikcp_waitsnd超过阈值时停止发送,或降低视频码率,或断开连接等,根据不同场景采取不同策略

参考链接

  1. TCP-IP详解:Delay ACK
  2. 为什么 TCP 协议有性能问题
  3. [https://github.com/skywind3000/kcp]
  4. 基于UDP实现的可靠传输协议(比如uTP),与TCP协议相比有什么优缺点? - 韦易笑的回答 - 知乎
posted @ 2022-01-04 14:56  Rogn  阅读(1009)  评论(0编辑  收藏  举报