目录

游戏服务端开发面试

专业基础

网络

网络协议

  • TCP 每次发送一个数据包后都要等待接收方发送一个应答信息,这样TCP才可以确认数据包通过因特网完整地送到了接收方。如果在一段时间内TCP没有收到 接收方的应答,他就会停止发送新的数据包,转而去重新发送没有收到应答的数据包,并且持续这种发送状态,直到收到接收方的应答。所以这会造成网络数据传输的延迟,若网络情况不好,发送方会等待相当长一段时间

  • UDP 无连接,不可靠,不保证顺序,快

长连接/短连接

  • 长连接 指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维 : 连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接

  • 短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接,如Http : 连接→数据传输→关闭连接

  • 滑动窗口技术

  • 建立连接的三次握手 与 断开链接的四次挥手

  • TCP/IP协议的传输效率

  • 解释DOS攻击与DRDOS攻击的基本原理

  • 一个100Byte的数据包精简到了50Byte,起传输效率提高了 50%

  • TIMEWAIT状态怎么解释

  • 常用的网络通信模型 Select

  • 常用的网络通信模型 Epoll, 边缘触发与平台触发的区别和应用

  • SelectEpoll的区别和应用

IO模型

  • IO分两个阶段:1. 通知内核准备数据。2.数据从内核缓冲区拷贝到应用缓冲区;根据这2点IO类型可以分成:

  • 同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数

阻塞IO (Blocking I/O Model)

  • 在两个阶段上面都是阻塞的

非阻塞IO (Nonblocking I/O Model)

  • 在第1阶段,程序不断的轮询直到数据准备好,第2阶段还是阻塞的

IO复用 (I/O Multiplexing Model)

  • 在第1阶段,当一个或者多个IO准备就绪时,通知程序,第2阶段还是阻塞的,在第1阶段还是轮询实现的,只是所有的IO都集中在一个地方,这个地方进行轮询

信号驱动IO (Signal-Driven I/O Model)

  • 当数据准备完毕的时候,信号通知程序数据准备完毕,第2阶段阻塞

异步IO (Asynchronous I/O Model)

  • 1,2都不阻塞

Java#Selector

  • 允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据.

Java#NIO2

  • 发出系统调用后,直接返回。通知IO操作完成。

  • 前四种同步IO,最后一种异步IO.二者区别:第二个阶段必须要求进程主动调用recvfrom.而异步io则将io操作全部交给内核完成,完成后发信号通知。此期间,用户不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

行程阻塞的原因

  • Thread.sleep(),线程放弃CPU,睡眠N秒,然后恢复运行

  • 线程要执行一段同步代码,由于无法获得相关的锁,阻塞。获得同步锁后,才可以恢复运行。

  • 线程执行了一个对象的wait方法,进入阻塞状态,只有等到其他线程执行了该对象的notify、nnotifyAll,才能将其唤醒。

  • IO操作,等待相关资源,阻塞线程的共同特点是:放弃CPU,停止运行,只有等到导致阻塞的原因消除,才能恢复运行 。或者被其他线程中断,该线程会退出阻塞状态,并抛出InterruptedException.

阻塞/非阻塞/同步/异步

  • 同步/异步关注的是消息如何通知的机制。而阻塞和非阻塞关注的是处理消息。是两组完全不同的概念。

并发编程模式 Reactor 与 Proactor

  • 用于派发/分离IO操作事件,IO事件也就是诸如read/write的IO操作,派发/分离就是将单独的IO事件通知到上层模块

  • Dispatcher 分发器 Notifer 通知器, 事件到来时,使用DispatcherHandler进行分派,这个Dispatcher要对所有注册的Handler进行维护。同时有一个Demultiplexer 分拣器对多路的同步事件进行分拣。

  • 两个模式的相同点,都是对某个IO事件的事件通知,即告诉某个模块,这个IO操作可以进行或已经完成。

  • 在结构上,两者也有相同点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler

Reactor 用于同步IO

  • 同步情况下Reactor回调handler时,表示IO设备可以进行某个操作(can read or can write),handler这个时候开始提交操作

Proactor 用于异步IO

  • 异步情况下Proactor回调handler时,表示IO操作已经完成

网络通讯框架

  • TCP Server框架:

    • Apache MINA(Multipurpose Infrastructure for Network Applications)2.0.4

    • Netty 3.5.0Final

    • Grizzly 2.2

  • Quickserver是一个免费的开源Java库,用于快速创建健壮的多线程、多客户端TCP服务器应用程序。使用QuickServer,用户可以只集中处理应用程序的逻辑/协议

  • Cindy :强壮,可扩展,高效的异步I/O框架

  • xSocket:一个轻量级的基于nio的服务器框架用于开发高性能、可扩展、多线程的服务器。该框架封装了线程处理、异步读/写等方面

  • ACE 6.1.0 C++ADAPTIVE CommunicationEnvironment,

  • SmaxFoxServer 2.X :专门为Adobe Flash设计的跨平台socket服务器

消息编码协议

  • AMF/JSON/XML/自定义/ProtocolBuffer

  • 无论是做何种网络应用,必须要解决的问题之一就是应用层从字节流中拆分出消息的问题,也就是对于 TCP 这种字节流协议,接收方应用层能够从字节流中识别发送方传输的消息.

  • 使用特殊字符或者字符串作为消息的边界,应用层解析收到的字节流时,遇见此字符或者字符串则认为收到一个完整的消息

  • 为每个消息定义一个长度,应用层收到指定长度的字节流则认为收到了一个完整的消息: 消息分隔标识(separator)、消息头(header)、消息体(body)

粘包

  • TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

发送方引起的粘包

  • 是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续发送几次的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据

接收方引起的粘包

  • 是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据, 若下一包数据到达时前一包数据 尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,这样就形成了粘包, 用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据

解决措施

  • 对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件接收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;TCP-NO-DELAY-关闭了优化算法,不推荐

  • 对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象-当发送频率高时依然可能出现粘包

  • 接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。-效率低

  • 接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开

分包算法思路

  • 基本思路是首先将待处理的接收数据(长度设为m),强行转换成预定的结构数据形式,并从中取出数据结构长度字段,即n,而后根据n计算得到第一包数据长度

    • n < m,则表明数据流包含多包数据,从其头部截取n个字节存入临时缓冲区,剩余部分数据一次继续循环处理,直至结束。

    • n=m,则表明数据流内容恰好是一完整结构数据,直接将其存入临时缓冲区即可。

    • n>m,则表明数据流内容尚不够构成一个完整结构数据,需留待与下一包数据合并后再行处理。

存储

  • 计算机系统存储体系

  • 程序运行时内存结构

  • 计算机文件系统,页表结构

  • 内存池与对象池的实现原理,应用场景与区别

  • 关系型数据库MySQL的使用

  • 共享内存

程序

  • C/C++ 有较深的理解

  • 深刻理解接口,封装与多态,有实践经验

  • 深刻理解常用的数据结构:数组,链表,二叉树,哈希表

  • 熟悉常用的算法及相关复杂度:冒泡排序 快速排序

  • 清楚宏定义 模板的含义以及使用

  • 灵活使用 STL 模板中的对象

  • 数据结构知识的复习

游戏开发入门

防御式编程

  • 不要相信客户端数据,一定要检验; 作为服务端,是无法确定你的客户端是谁(可能是黑客伪造的请求)

  • 函数内部,对于收到的参数;函数调用,对于函数发返回值;这两者都要进行合法的判断

  • 内部子系统之间不要过于信任,要求高内聚,低耦合;

  • 插件式模块设计,功能模块的健壮性应该是内建的,而不是依赖外部环境的稳定

设计模式

  • 道法自然,不要迷信迷恋设计模式

  • 简化,简化,再简化

网络模型

  • 自己造轮子: Select Epoll , Epoll一定比Select高效么?

  • 开源框架: Libevent Libev ACE

数据持久化

  • 选择存储系统要考虑的因素:稳定性,性能,可扩展性

  • 自定义文件存储 ,如《梦幻西游》

  • 关系型数据库,如 MySQL

  • No-SQL数据库, 如 MongoDB

内存管理

  • 使用内存池和对象池,禁止运行期间动态分配内存

  • 对于输入输出的指针参数,严格检查,宁滥勿缺

  • 写内存包含,使用带内存保护的函数 strncpy memcpy snprintf vsnprintf

  • 严防数组下标越界

  • 防止读内存溢出, 确保字符串以 \0 结束

日志系统

  • 简单高效, 大量日志操作不应该影响程序性能

  • 稳定 做到服务器崩溃,日志不丢失

  • 完备 玩家关键操作一定记录在日志里, 理想状态下,通过日志能够重建任何时刻的玩家数据

  • 开关 开发日志要加级别开关控制

通信协议

  • PDL(Protocol Design Language) 如 Protobuf, 可以同时生成前后端代码,减少前后端联调成本,拓展性好

  • JSON 文本协议,简单,自解释,无联调成本,拓展性好,方便包过滤 以及 写入日志

  • 自定义二进制协议,精简,高效的传输性能,完全可控,几乎无拓展性

全局唯一Key GUID

  • 为合服做准备

  • 方便追踪道具,装备流向

  • 每个角色,装备,道具是全局唯一Key GUID

多线程与同步

  • 消息队列进行同步化处理

状态机

  • 强化角色的状态

  • 前置状态的检查校验

数据包操作

  • 合并,同一帧内的数据包进行合并,减少IO操作次数

  • 单副本,用一个包尽量只保存一份,减少内存复制次数

  • AOI同步中减少中间过程无用数据包

状态监控

  • 随时监控服务器内部状态

  • 内存池,对象池使用情况

  • 帧处理时间

  • 网络IO

  • 包处理性能

  • 各种业务逻辑的处理次数

包频率控制

  • 基于每个玩家每条协议的包频率控制,瘫痪变速齿轮

开关控制

  • 每个模块都有开关,可以紧急关闭任何出问题的功能模块

反外挂和反作弊

  • 包频率控制可以消灭变速齿轮

  • 包id自增校验,可以消灭 WPE

  • 包校验码可以消灭或者拦截篡改的包

  • 图形识别码,可以踢掉 99% 非人的操作

热更新

  • 核心配置逻辑的热更新,如防沉迷系统,包频率控制,开关控制

  • 代码基本热更新,如 Erlang Lua

防刷

  • 关键系统资源的产出日志

  • 资源的产出和消耗尽量依赖两个或者两个以上的独立条件的检测

  • 严格检查各项操作的前置条件

  • 检验参数合法性

防崩溃

  • 系统底层与具体业务逻辑无关,可以用大量机器人压力测试,暴露各种BUG,确保稳定

  • 业务逻辑建议使用脚本,系统性的保证游戏不会崩溃

性能优化

  • IO 操作异步化

  • IO 操作合并缓写

  • Cache 机制

  • 减少竞态条件,避免频繁进出切换,尽量减少锁定使用,多线程不一定比单线程快

  • 减少内存复制

  • 自己测试,用数据说话,别猜

运营支持

  • 接口支持: 实时查询,控制指令,数据监控,客服处理

  • 实现考虑提供 http 接口

容灾与故障预案

服务器架构

好的架构

  • 满足业务要求

  • 能迅速的实现策划需求,响应需求变更

  • 系统级的稳定性保障

  • 简化开发。将复杂性控制在架构底层,降低对开发人员的技术要求,逻辑开发不依赖于开发人员本身强大的技术实力,提高开发效率

  • 完善的运营支撑体系

架构实践的思考

  • 简单,满足需求的架构就是好架构

  • 设计性能,抓住重要的20%, 没必要从程序代码里面去抠性能

  • 热更新是必须的

  • 人难免会犯错,尽可能的用一套机制去保障逻辑的健壮性

  • 游戏服务器的发展也由以前的单服结构转变为多服机构,甚至出现了bigworld引擎的分布式解决方案,最近了解到Unreal的服务器解决方案atlas也是基于集群的方式。

基于功能和场景划分服务器结构

  • 负载均衡是一个很复杂的课题,这里暂不谈bigworldatlas的这类服务器的设计

思路

  1. 分离游戏中占用系统资源(cpu,内存,IO等)较多的功能,独立成服务器。

  2. 在同一服务器架构下的不同游戏,应尽可能的复用某些服务器(进程级别的复用)。

  3. 以多线程并发的编程方式适应多核处理器。

  4. 宁可在服务器之间多复制数据,也要保持清晰的数据流向。

  5. 主要按照场景划分进程,若需按功能划分,必须保持整个逻辑足够的简单,并满足以上1,2点。

服务器划分

Gateway 应用网关

  • 主要用于保持和client的连接,该服务器需要2种IO

    • 对client采用高并发连接,低吞吐量的网络模型,如IOCP等

    • 对服务器采用高吞吐量连接,如阻塞或异步IO

  • 分担了网络IO资源,同时,也分担了网络消息包的加解密,压缩解压等cpu密集的操作。

  • 隔离了client和内部服务器组,对client来说,它只需要知道网关的相关信息即可(ip和port)。client由于一直和网关保持常连接,所以切换场景服务器等操作对client来说是透明的。

  • 维护玩家登录状态。

World Server

  • 是一个控制中心,它负责把各种计算资源分布到各个服务器,它具有以下职责

  • 管理和维护多个Scene Server

  • 管理和维护多个功能服务器,主要是同步数据到功能服务器

  • 复杂转发其他服务器和Gateway之间的数据

  • 实现其他需要跨场景的功能,如组队,聊天,帮派等

Phys Server

  • 主要用于玩家移动,碰撞等检测

  • 所有玩家的移动类操作都在该服务器上做检查,所以该服务器本身具备所有地图的地形等相关信息。

  • 具体检查过程是这样的:Worldserver收到一个移动信息,WorldServer收到后向Phys Server请求检查,Phys Server检查成功后再返回给world Server,然后world server传递给相应的Scene Server

Scene Server

  • 场景服务器,按场景划分,每个服务器负责的场景应该是可以配置的。理想情况下是可以动态调节的

ItemMgr Server

  • 物品管理服务器,负责所有物品的生产过程。在该服务器上存储一个物品掉落数据库,服务器初始化的时候载入到内存。任何需要产生物品的服务器均与该服务器直接通信。

AIServer

  • 又一个功能服务器,负责管理所有NPCAIAI Server通常有2个输入: 一个是Scene Server发送过来的玩家相关操作信息,另一个时钟Timer驱动

  • 在这个设计中,对其他服务器来说,AI Server就是一个拥有很多个NPC的客户端。AI server需要同步所有与AI相关的数据,包括很多玩家数据。由于AI ServerTimer 驱动特性,可在很大程度上使用TBB程序库来发挥多核的性能。