一、网络游戏架构的前世今生(2)_王元恺David的博客-程序员宅基地_网络游戏架构

技术标签: 架构  游戏程序  游戏微服务架构实践  网络  服务器  网络协议  游戏后端  

上文: 网络游戏架构的前世今生(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 版本即可。下图为自定义协议比对版本号的示例(未兼容多版本):
    检查协议版本
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/finishy/article/details/125578504

智能推荐

sql server 链接服务器的问题?_ArvinStudy的博客-程序员宅基地

建立了链接服务器,但是访问失败, 错误提示: 【2012-04-06 14:24:02】:TCP 提供程序: 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。链接服务器"SQL181"的 OLE DB 访问接口 "SQLNCLI" 返回了消息 "登录超时已过期"。链接服务器"SQL181"的 OLE DB 访问接口 "SQLNCLI" 返回了消息 "建立到服务

教师专业发展规划计算机教师,初中信息技术教师个人发展计划_zljchris的博客-程序员宅基地

个人专业发展计划一、指导思想新的教育发展形势对信息技术教师提出了更高的要求,教师不仅要具有深厚的专业知识,还要具有扎实的专业技能,要紧跟改革步伐,走在信息时代的前列。作为一名初中信息技术教师那就是要用先进的教育教学理论,反思自己的教育教学实践,在总结和反思中提升自己,使自己走向成熟,为农村基层教育事业的发展作贡献。为此,我就本人实际情况制定了如下个人专业发展计划。二、个人现状分析自毕业后,走上工作...

一元多项式计算器(程序对于多项式运算非常通用、细节很多详见描述与代码)_Feyl的博客-程序员宅基地_多项式展开计算器

程序支持除题目要求外的所有“任意多个”一元多项式加减运算输入:测试用例:-(2x^3+5x^4)+2x^5+(2x+5x^8-3.1x^11)+4x^6+2x^2+(7-5x^8+11x^9)+(x+x^2-x^3)+10=-2x^5+(2x+5x^8-3.1x^11)+4x^6+2x^9+(7-5x^8+11x^9)+(x+x^2+x^3)+2x^7=(2x+5x^8-3.1x^11)+(7-5x^8+11x^9)=(6x^-3-x+4.4x^2-1.2x^9)-(-7x^...

NIO Selector Channel选择器深入理解_你亲爱的裴先生的博客-程序员宅基地

Selector和非阻塞网络编程要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。​ 选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Select...

疯狂不再-《疯狂的蠢贼》观后感_weixin_30430169的博客-程序员宅基地

昨夜在海阔天空上面闲逛,发现一个国产电影,疯狂的蠢贼,资料上介绍得神乎其神的,说是《疯狂的石头》 《疯狂的赛车》的原班人马完成,延续前两部的疯狂, 说实话,《疯狂的石头》 《疯狂的赛车》都是我挺喜欢看的电影,即疯狂搞笑,又贴近生活,反映社会现实与黑暗。即是这样的片子的延续,岂有不看的道理,马上开动迅雷,4M/s的速度往硬盘里拖,没过多久,就稳稳地在我的...

如何包装简历上的项目?_古老的屋檐下的博客-程序员宅基地_java项目经验包装

这篇文章我们来聊一聊,在系统设计和项目经验这两块,应该如何充分的准备,才能拿出有技术含量的项目经验战胜跟你同台竞技的其他工程师,征服你的面试官,收获各种心仪的offer。(1)高级工程师必备:系统设计能力我们一般在招聘高级及以上工程师的时候,一定会严格考察一项能力,系统设计能力。因为如果你仅仅是对各种各样的技术都熟悉,有技术广度,也有一定的技术深度,实际上是不够的。如果你的系统设计能力不到位...

随便推点

CDH6.3.2集成安装flink on yarn服务(编译源码,制作parcel)(更新:flink1.12.2版本测试也可用)_栗子_yangxw的博客-程序员宅基地_cdh flink on yarn

目录一:环境准备二:下载安装包1. Flink-shaded包2. flink1.10.2 源码包3. maven配置文件三:编译flink-shaded版本1. 解压tar包2. 解压文件后目录结构3. 修改pom文件4. 开始编译四:编译Flink1.10.2源码1. 解压tar包2. 执行编译3. 等待编译成功4. 打包编译好的文件五:制作parcel包1. 下载git开源制作parcel包的项目2. 修改配置文件 flink-parcel.properties3. 赋予build.sh文件执行权限4

grep、awk、sed学习笔记_zhangrenfang的博客-程序员宅基地

一.grep1. grep简介grep (global search regular expression_r(RE) and print out theline,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。Unix的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。e

RH124期末检测考试_谁主沉浮lyb的博客-程序员宅基地

RH124期末检测考试1、修改密码及配置主机信息2、为您的系统配置一个默认的软件仓库3、创建用户账号4、配置文件的权限5、创建一个共享目录/home/managers 特性如下6、配置 ntp时间同步服务7、配置一个用户8、归档一个文件9、查找一个字符串10、设定系统定时任务要求如下:1、修改密码及配置主机信息要求如下:• 主机名称:westos_rh124_100.westos.com• Ip 地址:172.25.254.100• 子网掩码:255.255.255.0• 网关:172.25.2

unity dll实现热更新_ma1238906的博客-程序员宅基地

大家都知道一谈起热更新的话首选是Ulua这个插件, 其实Unity可以使用dll热更新的,如果你实在不想用Lua来编写逻辑,0.0请下看Dll+AssetBundle如何实现热更新的.让你看完这个文章之后只是认识DLL热更新的方式和概念,掌握热更新的实战框架还需要你自己=。=我们通常的做法是编译成的DLL打成AssetBundle文件, Unity通过WWW下载AB文件获取里面DLL.通...

一、elasticsearch部署_weixin_30776545的博客-程序员宅基地

Elasticsearch官网: https://www.elastic.co/products/elasticsearch一、Linux单节点部署1. 解压elasticsearch-5.6.1.tar.gz到安装目录下,这里使用的是/opt/module  $ tar -zxvf elasticsearch-5.6.1.tar.gz -C /opt/module/2. 在...

MySQL字符集LATIN1转UTF8_gyqinag的博客-程序员宅基地

转载自: http://www.ttlsa.com/html/79.html导出表结构1. mysqldump -uroot -p --default-character-set=utf8 -d databasename > db.sql2. 修改db.sql内的字符集设置(notepad++编辑)ENGINE=MyISAM DEFAULT CHARSET=latin1;修改为...

推荐文章

热门文章

相关标签