前言
最近一直在思考某些事情,然后就拖更了一个月233
其实代码也一直在写,游戏的主流程也基本上通了,就是一直懒得写博客。
OK我们今天来介绍下游戏的服务端是怎么实现的。
服务端结构
BounceArena的服务端使用node.js开发,这次用了三个进程,分别处理日志(main.js也是程序入口),socket通信(SFSocketHandler.js)和具体的业务逻辑(SFGameServer.js)。
main.js
main.js
为程序入口,我们在server/app
目录下执行node ./
指令就可以了。
这个进程会启动两个子进程SFSocketHandler.js
和SFGameServer.js
,这两个进程运行过程中产生的日志会通过node的进程通信机制发送给main.js
,然后主进程统一处理这些信息,比如格式化输出,另存到文件等等。
主要代码如下:
1 | const main = function() { |
其中onExit()
后面会详细说明,然后log()
是用于输出日志的方法(其实也可以用log4js之类的库,我当时不知道有这个东西,写完了才发现有个现成的库可以用orz)
不过既然写了,就姑且贴出来吧233
1 | /** |
这里使用了color
库来方便地设置文本的颜色
我们想结束程序的时候,会按下ctrl+c
组合键,为了使所有进程全部正常安全地退出,我这里监听了SIGINT
中断事件,当主进程接收到该信号时,不会立即退出,而是等待子进程全部安全正常地结束之后才会退出
1 | const onExit = function() { |
main.js
的主要内容就是这些了
SFSocketHandler.js
主进程会执行这个文件作为一个子进程
这个进程负责的事情是开启TCP服务器,承载TCP连接,接收来自于客户端的原始数据并作出第一步的处理,然后把整理过的数据发送给SFGameServer来处理具体的业务逻辑
主要代码如下:
1 | const main = function() { |
进程通信使用redis的订阅机制,经过测试,node自带的process.send()
不好用,延迟非常高,用redis的订阅的话,延迟可以大幅降低,所以就采用redis来做进程通信了
然后就是onSocket
这个主要的方法了:
1 | const onSocket = function(socket) { |
经过之前踩过的坑,socket在接收数据时,由于网络拥堵等原因可能会发生粘包或者断包,这时就要自己处理分包逻辑。这里约定数据包的格式为JSON字符串+\r\n\r\n
四个字符,以此来划分粘连在一起的数据包。大致逻辑如下:
1 | socket.on("data", function(data) { |
GameServer处理完请求数据后,必定会发送一个相应返回给客户端,同样的,Response信息将会由GameServer先发送给SocketHandler,然后由后者发送给相应的socket连接
1 | /** |
给客户端发送数据时,如果网络连接不畅而且发送的数据量特别大,可能会导致系统的发送缓冲区溢出,导致客户端不能收到全部的信息,就不妙了。
还好node的socket在发送方法socket.write()
提供了一个返回值,如果返回false的话则说明缓冲区已经开始紧张了,此时如果再有数据需要发送则可能会出问题,所以我们就先把接下来需要发送的数据全部暂存在writeBuffer中,直到收到drain
事件,说明缓冲区已清空, 我们就可以继续发送数据了
1 | /** |
SFGameServer.js
主进程会执行这个文件作为另外一个子进程
这个进程负责处理具体的业务逻辑。大致的思路是根据协议号pid来选择合适的Controller
来处理逻辑,初始化过程如下:
1 | /** |
然后根据从SocketHandler收到的请求数据,选择相应的Controller。
1 | /** |
当然还要准备推送Response给客户端的方法pushMessage
1 | /** |
之后就是具体各个Controller的实现了,具体逻辑我们下次再说
需要注意的是,每个Controller都要提供setPuhser()
方法用来设置推送方法,以及onRequest()
方法用来处理请求信息
1 | // 文件: SFUserController.js |
完整代码
上面贴出的代码片段由于篇幅限制只保留了关键部分,完整的代码可在我的github上找到