一、网络游戏架构的前世今生(2)

上文: 网络游戏架构的前世今生(1)

2.2 网络连接方案

相比于网络同步方案,游戏在网络连接的方案上和其他应用上并没有太大差异。有些轻量级休闲游戏,会选择 HTTP/HTTPS 等短连接方案,少量利用 websocket 全双工做一些主动消息推送业务。这种方案的好处在于,有大量成熟的三方库和参考案例,并且不局限于游戏领域之内,门槛低、方便实现并易于更新迭代。
HTTPS短连接

虽然 HTTP 可以用于大流量的通信场景,但对低延迟通信来说并不是最好的选择,相比之下,主流的游戏网络连接方案还是 TCP 长连接,这是因为短连接会不断的创建和释放连接,既消耗服务器性能又增加了平均延迟。

即使当今的 http 库通常都自带连接池,短连接的建立和释放并不是一一对应 TCP
的握手和挥手,但应用层不必要的建立和释放过程也会影响程序运行性能,在玩家数量高时尤为明显。

MUD、MUX等游戏会选择 Telnet 等普遍使用的 TCP 长连接协议,是由于这些游戏往往在客户端渲染表现上投入较少,在游戏世界的设计和玩法内容设计上投入较多,所以会使用较为常规的长连接协议减轻客户端工作量。另一个方面原因,这些游戏往往可以通过统一的客户端进行登录游玩,保持协议的统一也是游戏间的互相促进和成就。

客户端界面更用户友好的游戏,还是会以自定义 TCP 长连接协议居多。对于网络安全要求较高的公司和游戏产品,还需要在协议上进行加密。我所服务过的好几家大型游戏公司,内部都有专门的团队在做网络协议、网络加密的更新迭代,这也是游戏背后的网络攻防战(网络安全不属于本栏目的话题,在这里仅作基本介绍)。游戏中的自定义网络协议并不像某些更专业的领域那么严格(如 IOT等,学习看懂都是一件很复杂的事),通常需要考虑以下几个点:

  1. 粘包拆包问题的处理
  2. 包体的序列化与反序列化
  3. 包头包尾是否需要特定标识
// 伪代码展示网络连接的处理逻辑,语法使用 golang
for {
  ... // 预处理判断网络连接是否正常,设置读取 deadline 等
  bytesLength, err := tcpConnection.Read(b) // 从网络连接中读取数据
  if err != nil {
    // 错误异常处理
  }
  
  if bytesLength > 0 {
    ringbuffer.PushPacket(b[:n]) // 将当前收到的数据包塞入 ringbuffer
    // 一次获取到的网络数据包中,可能是包含数个网络包粘包的结果,循环处理
    for {
      // 通过 codec 解码 buffer 中的包体
      outBytes, err := codec.Decode(ringbuffer)
      if err != nil || outBytes == nil {
        // 暂未收到完整包,或者有错误异常
      }      
      ... // 自定义一些特殊包的处理,例如心跳包等
      agent.OnMessage(outBytes) // 向业务层 agent 通知收到消息事件
    }
  }
}

一般来说,上述三个点中只有前两个点是相对必要的。粘包拆包问题并不是游戏行业的特有问题,很多成熟的网络库提供好了现成的方案,如果自己编写的话也不难,只需要选择适合的 codec 即可。例如在包头中带包体长度,或是发送定长包体等。
带包体长度

// 以带包体长度(LengthFieldBased)为例,b 为序列化后的输出结果
func (codec *Codec) Encode(b []byte) (out []byte, err error) {
  length := len(b) // 获取长度
  out = getLengthBytesByBigEndian(out, length) // 获取大典序的长度 bytes
  out = append(out, b...) // 拼接加码结果
  return
}

func (codec *Codec) Decode(buffer *RingBuffer) (out []byte, err error) {
  // buffer读指针不移位的情况下读取4位,获取包体长度
  lengthBuffer, err := buffer.LazyReadN(4)
  if err != nil {
    return // 没有4位可以读,包不完整
  } else {
    frameLength = getFrameLength(lengthBuffer) // 获取包体长度的数字
  }
  // buffer读指针不移位的情况下读取{包体实际长度位}4位,获取实际包体内容
  body, err := buffer.LazyReadN(frameLength)
  if err != nil {
    return
  }
  buffer.ShiftN(frameLength + 4) // buffer 读指针移动到正确位置
  // 注:这里千万要注意 LazyReadN 的返回值是值拷贝还是引用拷贝,需要复制出一份新的内存数据出来
  out = make([]byte, frameLength)
  copy(out, body)
  return
}

序列化与反序列化的方式也有很多,不过从传输的效率和性能上考虑,选择 protobuf 或其他高压缩率的序列化方案是主流,这一点和其他应用不太一样。其他应用可能会选择 json、yaml 等主流通用的序列化方案,这些方案的三方库很多,解决方案也多。但这些往往需要占用更多的网络带宽。我在之前的项目中,一般都是以公司层级去写自己公司的网络序列化库(即公司旗下所有游戏都是公用同一个网络库),这样既安全又高效;不过这两年 protobuf 用的更多,生态和开发效率上都是更好的选择,尤其在给新入职的程序员做介绍时,自己写的库往往要讲半天,protobuf 更好上手,资料肯定比公司自己写文档要全。
序列化

当然,开发者们对游戏性能的追求是无止境的,这其中也包含网络连接。TCP 作为最主流的传输层协议,在高峰用网期间是会受到一定影响的,近几年来尤其如此;并且由于其设计上的限制,导致在跨国跨洋的场景上往往不尽如人意。这一点无论是对短连接HTTP(S),还是长连接自定义协议都是如此。为了优化玩家的游戏体验,我们自然把目光放到了另一个耳熟能详的传输层协议 —— UDP 上,希望 UDP 能够优化游戏的网络连接。

最最开始,UDP 只是在有限的游戏流程内进行优化,但很快,部分游戏把 UDP 作为“最终杀器”完全取代了 TCP。老一辈即时战略类游戏如魔兽争霸3,使用 UDP 进行信息的广播;部分有区服概念的 RPG,使用 UDP 进行网速检测、玩家区服信息的传输。MOBA等对网络延迟要求很高的游戏,或是在东南亚等网络条件复杂地区发行的游戏,会使用 UDP 去模拟 TCP 做有状态连接,在第七层应用层做自定义 UDP 协议,从而达到有连接并保序的要求。在我经历过的项目中,使用过 Raknet、ZeroMQ(UDP)、KCP 库进行过自定义 UDP 协议的开发。我个人的经验而言,KCP 是我用起来最顺手,也是测试下来最稳定的开源库,在印度到美国的跨洋连接上也有不俗的稳定表现。

可以参考我之前练手的一个开源项目https://github.com/finishy1995/gmould/tree/main/network/ucpnet
请勿直接使用这个库在项目代码中,后续许多改进修复并未上传)
自定义协议

在写过测过一些网络连接方案之后,我发现当网络环境较好的情况下,其实 TCP 要明显比 UDP 更快更好,毕竟 TCP 天生就是用作长连接场景的。所以现代 MOBA 类游戏通常都是智能判断的网络连接,一开始使用 TCP 长连接,在检测到玩家连接状况不稳定时(通常是收集玩家的ping值数据,通过方差标准差数学统计的方法监控网络的抖动情况,当监控值比预先设定的值大时,触发警报),自动切换为 UDP 协议,从而保证玩家流畅的游戏体验。
自动智能切换

从工程开发角度,也有不少可以演进的点。最早写网络库我用 muduo 做底层,C++ 编写;用 Golang 写项目习惯后,我使用 gnet 做底层;gnet 在 Linux 内核系统上的效率名列前茅,且库内对“网络惊群效应”等问题有较好的支持。对中小型的游戏公司而言,选择一个成熟的网络库,在它之上搭建自己的协议是更为经济有效的方案。

未完待续……

来自朋友的一些提问及回答:

  1. Q:为什么没有统一的 TCP 协议支持游戏场景?
    A:统一协议降低攻击门槛,公开辛苦开发的协议是一件吃力不讨好的事情,且游戏场景各有不同,很难大一统。
  2. Q:大公司大多都有专门的团队做网络库网络协议,如何在更新时尽量不影响玩家?
    A:其实有很多还是采取定期停服维护,有少部分是靠支持多版本号的协议来不停服热更。简单来说,更新前网络库是v1.0版本,更新后服务器同时支持v1.0、v1.1 版本即可。下图为自定义协议比对版本号的示例(未兼容多版本):
    检查协议版本
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王元恺David

感谢你的支持~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值