概述
本文尝试从开发者角度梳理开发实时联网游戏后台服务过程中可能面临的挑战, 并针对性地提供相应解决思路, 期望帮助开发者依据自身游戏特点做出合理的技术选型. 维基百科关于网络游戏的定义: 通过计算机网络, 将专用服务器和用户的客户端设备 (手机, PC, 游戏主机等) 相连, 让多名玩家同时联机进行游戏的娱乐形式, 由此可知网络游戏涉及三个角色: 客户端, 网络, 服务器, 从网络架构上来讲网络游戏可分为 C/S 架构和 P2P 架构(特指客户端间直连通信), 在实际开发中还有一种 C/S 和 P2P 架构混合: C/M 架构.
P2P 架构不在本文讨论范围, C/M 架构和 C/S 架构类似, 和经典的 LAMP 网站架构类似一般 C/S 架构的游戏后台也可划分为如下三层:(1)网络接入层;(2)游戏逻辑层;(3) 数据存储层.
网络接入, 游戏逻辑, 数据存储层各自所面临的问题域及对应技术栈都大为不同, 做此划分不仅有助于模块解耦, 技术分工, 组件复用, 也可方便服务的运维部署. 本文也着重从这三个方面来阐述游戏服务器的开发.
1, 网络接入层
网络接入层的主要任务是建立客户端和后台服务以及客户端之间的信道, 接收来自客户端大量并发请求, 考核该层的主要性能指标是: 高吞吐, 低延迟. 因而网络接入层开发考验的是开发者高性能网络编程的功底, 即解决 C10K 甚至 C10M 的能力.
1.1 协议选择
根据 OSI 的七层网络参考模型, 我们可将网游网络也做如下 7 层划分:
其中 4 层以下都由操作系统来负责, 开发者无需为此操心, 在实际的开发过程中开发者首要面临的问题便是传输层是采用 TCP 还是 UDP, 下表简要对比了两者的优劣. 综合两者优劣, 简单来说除非对延迟有极致要求 (例如 FPS,MOBA 类游戏) 需采用 UDP 外, TCP 可应对大部分游戏. 在实际游戏开发中不管是采用 TCP 还是 UDP 方式, 都很少直接通过 Socket 编程方式来进行, 一来因为开发工作量大, 质量性能难以保证; 二来平台兼容性不好(比如 H5 并没有提供 socket 编程能力), 而是基于更上层的通讯协议比如基于 TCP 的 HTTP,websocket 协议, GRPC, 以及基于 UDP 实现的 QUIC,WebRTC 协议等.
值得注意的是基于安全性考虑, 浏览器标准未提供 UDP 收发能力, QUIC 协议也只在 chrome 得到了支持, WebRTC 也还不是浏览器事实标准且协议初始目的是用于实现点对点的音视频通信, 协议内容过于庞杂不容易提炼应用于游戏开发中, 因而现阶段 H5 游戏还只能采用 HTTP 或 Websocket 方式通讯.
通讯协议确定后, 随后要考虑的便是游戏对象的序列化, 序列化主要有基于文本, 基于二进制两种, 其优劣如下表所示. 在开发过程中一般会先采用文本序列化方式, 便于前后端开发联调, 在游戏正式上线前切换至二进制序列化方式以减少传输流量, 提升编解码效率.
至于数据安全性问题, 为了保护敏感数据安全开发者可以选择安全的 https 或 WSS 通讯协议, 而对于直接基于 TCP 协议通讯, 可采用先用 RSA 协商加密秘钥, 然后使用对称加密方式将数据加密后发送.
通过以上分析, 对于游戏协议类型的选择我们给出有以下准则:
1, 弱联网类游戏: 诸如休闲, 卡牌类游戏可直接 HTTP 协议, 对安全性有要求的话就使用 HTTPS;
2, 实时性, 交互性要求较高: 这类游戏一般需要保持长连接, 优先选择标准的 ws 协议(同时使用二进制序列化方式), 如考虑安全性可使用 wss 协议. 而对于提供 socket 接口的 native 平台也可使用 TCP 协议, 同时对数据做对称加密增强安全性;
3, 实时性要求极高: 不仅需要和服务器保持长连接, 且延迟和网络抖动都要求极高(如 FPS, 赛车类游戏), 可使用基于 UDP 的实现流传输协议如 QUIC,KCP 等.
1.2 并发模型
为了处理来自客户端的并发请求, 服务端有 4 种常见的并发模型.
1.2.1 进程
进程是最早采用的并发模型, 进程作为操作资源分配, 调度的单位, 拥有独立的运行空间. 进程并发模型中每个请求由独立的进程来处理, 进程一次只能处理一个请求, 该模型最大的优点就是简单. 如果处理请求的进程由于系统调用而阻塞或进程的时间片用完, 抢占式的进程调度器就会暂停旧进程执行, 调度执行新的进程, 这个过程涉及大开销的上下文切换, 进程并发模型的缺点是比较低效. 最典型的采用进程模型的服务有 Apache.
1.2.2 线程
线程并发模型是进程模型的改进, 线程从属于进程, 是系统更小粒度的执行调度单元. 不同请求可由进程内多个并发执行的线程来处理, 这些线程由操作系统内核自动调度. 线程相对进程的主要优势在于, 调度上下文切换开销更小, 但由于多个线程共享地址空间, 需要额外的线程间互斥, 同步机制来保证程序性正确性. 典型的采用线程模型的服务有 Tomcat.
1.2.3 IO 多路复用
利用操作系统提供的 epoll 等 IO 多路复用机制, 能同时监控多个连接上读, 写事件, IO 多路复用也称事件驱动模型, 网络程序执行逻辑可抽象为事件驱动的状态机. IO 多路复用避免了读写阻塞, 减少了上下文切换, 提升了 CPU 利用率和系统吞吐率. 但 IO 多路复用它将原本 "同步", 线性的处理逻辑变成事件驱动的状态机, 处理逻辑分散于大量的事件回调函数. 这种异步, 非线性的模型, 极大地增加了编程难度, 如 nodeJs 的常见的回调地狱问题. 典型的采用 IO 复用模型的服务有 Nginx,netty.
1.2.4 协程
协程也称为轻量级线程, 是一种协同的, 非抢占式的多任务并发模型. 协程运行在用户空间, 当遇到阻塞或特定入口时, 通过显式调用切换方法主动让出 CPU, 由任务调度器选取另一个协程执行.
协程切换只是简单地改变执行函数栈, 不涉及内核态与用户态转化, 也涉及上下文切换, 开销远小于进程 / 线程切换. 协程的概念虽早已提出, 随着近些年年越来越多的语言 (go, Haskell) 内置对协程支持才被开发者所熟知, 协程极大的优化了开发者编程体验, 在同步, 顺序编程风格能快速实现程序逻辑, 还拥有 IO 多路复用异步编程的性能. 典型的采用协程模型的服务有 openresty(Lua), gevent(Python), golang.
以上总结了目前 4 种常用的并发模型, 它们在工作原理, 运行效率, 编程难度等方面有显著区别, 各自有适用场景, 在实际使用时应该根据需求仔细评估. 在实际开发过程中如果没有可复用的现成网络组件或历史包袱我们建议使用协程并发模式开发网络接入层服务.
来源: https://juejin.im/post/5add9b98518825672d33c7e8