游戏服务端开发面试
专业基础
网络
网络协议
- TCP 每次发送一个数据包后都要等待接收方发送一个应答信息,这样TCP才可以确认数据包通过因特网完整地送到了接收方。如果在一段时间内TCP没有收到 接收方的应答,他就会停止发送新的数据包,转而去重新发送没有收到应答的数据包,并且持续这种发送状态,直到收到接收方的应答。所以这会造成网络数据传输的延迟,若网络情况不好,发送方会等待相当长一段时间 
- UDP 无连接,不可靠,不保证顺序,快 
长连接/短连接
- 长连接指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维 : 连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接
- 短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接,如Http : 连接→数据传输→关闭连接
- 滑动窗口技术 
- 建立连接的三次握手 与 断开链接的四次挥手 
- TCP/IP协议的传输效率 
- 解释 - DOS攻击与- DRDOS攻击的基本原理
- 一个 - 100Byte的数据包精简到了- 50Byte,起传输效率提高了 50%
- TIMEWAIT状态怎么解释
- 常用的网络通信模型 - Select
- 常用的网络通信模型 - epoll, 边缘触发与平台触发的区别和应用
- Select和- epoll的区别和应用
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 通知器, 事件到来时,使用- Dispatcher对- Handler进行分派,这个- 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也是基于集群的方式。
基于功能和场景划分服务器结构
- 负载均衡是一个很复杂的课题,这里暂不谈 - bigworld和- atlas的这类服务器的设计
思路
- 分离游戏中占用系统资源(cpu,内存,IO等)较多的功能,独立成服务器。 
- 在同一服务器架构下的不同游戏,应尽可能的复用某些服务器(进程级别的复用)。 
- 以多线程并发的编程方式适应多核处理器。 
- 宁可在服务器之间多复制数据,也要保持清晰的数据流向。 
- 主要按照场景划分进程,若需按功能划分,必须保持整个逻辑足够的简单,并满足以上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
- 又一个功能服务器,负责管理所有 - NPC的- AI。- AI Server通常有2个输入: 一个是- Scene Server发送过来的玩家相关操作信息,另一个时钟Timer驱动
- 在这个设计中,对其他服务器来说, - AI Server就是一个拥有很多个- NPC的客户端。- AI server需要同步所有与- AI相关的数据,包括很多玩家数据。由于- AI Server的- Timer驱动特性,可在很大程度上使用- TBB程序库来发挥多核的性能。