• 服务器正文18:UDP可靠传输的理解和思考(读云凤博客有感)


    一、什么时候用UDP比TCP好

    • 需求
      有人期望基于 UDP 协议通讯来获得更快的响应速度,而又想让通讯流像 TCP 一般可靠

    • 尝试
      1)一条路是寄希望于业务逻辑上允许信息丢失:比如,在同步状态中,如果状态是有实效性的,那么过期的状态信息就是可丢失的。这需要每次或周期性的全量状态信息同步,每个新的全量状态信息都可以取代旧的信息。或者在同步玩家在场景中的位置时可以用这样的策略。不过在实际操作中,我发现一旦允许中间状态丢失,业务层将会特别难写。真正可以全量同步状态的场合也非常少。
      2)不允许信息丢失,但允许包乱序会不会改善? 一旦所有的包都一定能送达,即丢失的包会用某种机制重传,那么事实上你同样也可以保证次序。只需要和 TCP 一样在每个包中加个序号即可。唯一有优势的地方是:即使中间有包晚到了,业务层有可能先拿到后面的包处理
      3)允许消息丢失,包次序无关
      在网络状况不好的时候,我们可以看到有时采用短连接反而能获得比长连接更好的用户体验。不同的短连接互不影响,无所谓哪个回应先到达。如果某个请求超时,可以立刻重新建立一条新的短连接重发请求。这时,丢包重发其实是放在业务层来做了。而一问一答式的小数据量通讯,正是 TCP 的弱项:正常的 TCP 连接建立就需要三次交互,确定通讯完毕还需要四次交互。如果你建立一次通讯只为了传输很少量的一整块数据,那么明显是一种浪费。这也是为什么 google 的 QUIC 对传统的 http over TCP 有改善的空间

    二、一个可靠的 UDP 通讯模块的 API 接口该如何设计

    • 注意要点
      最糟糕的地方是,通讯模块本身和 UDP 绑的太死,也就是这个模块本身负责了 UDP 包的收发。(整个模块应该只提供输入和输出数据包的接口,和网络通讯 api 无关。)

    • 代码示例

    struct rudp_package {
         struct rudp_package *next;
         char *buffer;
         int sz;
    };
    
    struct rudp * rudp_new(int send_delay, int expired_time);
    void rudp_delete(struct rudp *);
    
    // return the size of new package, 0 where no new package
    // -1 corrupt connection
    int rudp_recv(struct rudp *U, char buffer[MAX_PACKAGE]);
    
    // send a new package out
    void rudp_send(struct rudp *U, const char *buffer, int sz);
    
    // should call every frame with the time tick, or a new package is coming.
    // return the package should be send out.
    struct rudp_package * rudp_update(struct rudp *U, const void * buffer, int sz, int tick);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 游戏中的注意点

    一般在网络游戏或其它需要低延迟的应用中,我们都需要定期保持心跳,以检查连接质量。所以必然会周期性的调用维持用的 api ,这和一般网络应该是不同的。

    • 接口模块使用解析
      1)业务模块按时间周期(时间片)调用udp更新信息
      这里提供了一个 rudp_update 的 api 要求业务层按时间周期调用,当然也可以在同一时间片内调用多次,用传入的参数 tick 做区分。如果 tick 为 0 表示是在同一时间片内,不用急着处理数据,当 tick 大于 0 时,才表示时间流逝,这时可以合并上个时间周期内的数据集中处理
      2)rudp_update 的每次调用均可以传入一个实际收到的 UDP 包(可以是一个完整的 UDP 包,也可以是一部分),这个包数据是一个黑盒子,业务层不必了解细节。它的编码依赖对端采用的相同的 rudp 模块。
      3)每次调用都有可能输出一系列需要发送出去的 UDP 包。这些数据包是由过去的 rudp_send 调用压入的数据产生的,同时也包含了最近接收到的数据包中发现的,对端可能需要重传的数据,以及在没有通讯数据时插入的心跳包等。总的来说,rudp_update 内部做了所有的可靠化通讯需要的数据组织工作。使用的人传入从 UDP socket 上收到的数据(不包括数据加密或其它数据组织工作),并从中获取需要发送到 UDP socekt 的数据。

    • 总结
      而业务层的数据收发只需要调用 rudp_send 和 rudp_recv 即可。其中,rudp_recv 保证数据包按次序输出;rudp_send 也并不真正发送这些数据包,而是堆积在 rudp 对象内,等待下一个时间片。(rudp_new 创建 rudp 对象时,有两个参数可配置。send delay 表示数据累积多少个时间周期 tick 数才打包在一起发送。expired time 表示已发送的包至少保留多少个时间周期。和 TCP 不同,我们既然使用 udp 通讯,就是希望高响应速度,所以即使数据抱迟迟没有送达,它们也不必保留太长时间,而只需要通知业务层异常即可)

    • 固定格式
      0 心跳包
      1 连接异常
      2 请求包 (+2 id)
      3 异常包 (+2 id)

    后两种数据需要跟上两字节的序号(采用大端编码)

    三、一个简单的实现

    链接

    四、一些备注

    1)tcp麻烦的是在wifi情况下的rto问题,fps,moba对延时很敏感,所以基本上都用udp,有过球类游戏用过tcp,统计发现比较糟糕。在国内最好udp包大小最好不超过576,我们踩过坑(国内一些设备制造商没有严格遵循rfc标准),576最安全,fps,moba,球,车类的上行包大多数不超过100字节,下行不超过300,所以大多数情形下不会因为分包造成有效payload减少
    2)UDP可靠库:KCP、enet
    3)资料索引

    1.On Latency and Player Actions in Online Games
    2.The effects of loss and latency on user performance in unreal tournament 2003
    3.Matchmaking in multi-player on-line games studying user traces to improve the user experience
    
    • 1
    • 2
    • 3

    4)上述代码的问题
    算法和UNREAL3中的是一致的,也是采用请求机制,无请求默认消息包对方已收到,发送方需要一个发送缓存区,一个缓存的数据包超过心跳时间就会清掉,如果缓存满了,就是网络出了问题,弹框异常了
    5)在相同的网络环境下,特别是跨网(比如说电信与联通),UDP有可能比TCP块120倍。这是几年内严格测试得出的结论。
    6)固定MTU。但是可以很负责的告诉你,很多路由特别是企业路由,仅允许每次通过512字节的UDP。所以QQ是会在登录期间测试这个值。

  • 相关阅读:
    使用docker简单编译k20pro内核
    Java版 招投标系统简介 招投标系统源码 java招投标系统 招投标系统功能设计
    React组件应用于Spring MVC工程
    MyBatis-3.4.2 源码学习
    Vue3 - 响应式工具函数(使用教程)
    stack栈、queue队列、list链表容器
    PPOCR车牌定位模型推理后处理优化研究
    Flume的简介
    2022年前端技术发展趋势
    110.网络安全渗透测试—[权限提升篇8]—[Windows SqlServer xp_cmdshell存储过程提权]
  • 原文地址:https://blog.csdn.net/weixin_43679037/article/details/125494790