协程在游戏服务器开发中的应用

2019-04-15 14:54发布

之前在客户端开发中,就发现了协程在代码中的方便之处。比如我在获取一个资源时,不使用协程的情况下,只能使用回调函数,代码大致如下:
void ProcessPic(string picName) { Pictrue pic = getPic(picName); if(pic == null) { requestPicFromServer(picName, ProcessPic2); return; } ProcessPic2(Picture pic); }
void ProcessPic2(Picture pic) { ....... }
也就是代码逻辑被打断了。而使用协程可以避免这种情况,代码大致如下:
void ProcessPic(string picName) { Pictrue pic = getPic(picName); if(pic == null) { requestPicFromServer(picName); do { yield return; }while(null == (pic = getPic(picName))) } ........ }
在开发服务器代码时,发现这个特点也可以用在服务器逻辑上,可以起到相同的作用。 我们的服务器架构如下:

session和客户端直接相连,负责收包和发包; logic负责处理逻辑,他会调用script来做具体处理; script使用脚本语言做游戏逻辑处理,比如lua语言; transmitter负责发送调度,判断某个包应该发送给哪个session,或者应该发送给client以便其他处理; client用与连接其他服务器,比如数据库服务器,将需要花费时间的处理发送到其他服务器,并将回包发送给logic处理,。
举一个例子,玩家登录时的处理流程,如果使用传统的回调函数处理方式,情况如下:
A1 session收到客户端的请求包,将包发送给logic; A2 logic调用script处理,script代码发现此包是玩家登录请求,所以他需要获取玩家信息; A3 script在本地内存中找不到对应的玩家信息,所以他将玩家信息查询数据库请求和此次操作的上下文信息返回给logic; A4 logic将包发送到transmitter; A5 transmitter发现此包不是发送到客户端的,而是需要发送到其他服务器的,所以将此包发送到了client; A6 client将请求发送给数据库服务器,此次流程结束,这时客户端还没有收到玩家登录的回包。
B1 当client收到数据库返回的玩家信息时,将包发送到logic; B2 logic调用script处理,script代码发现是玩家信息回包,并且回包中带有上次操作的上下文信息,所以他继续处理登录请求,完成后将结果包返回给logic; B3 logic将包发送到transmitter; B4 transmitter发现此包是发送给客户端的,所以他将包发送到指定的session; B5 session将包发送给客户端,流程结束,客户端收到了玩家登录的回包。
在上面的流程中,步骤A3需要将操作上下文封装在包信息中,这样在步骤B2中才知道应该怎么处理这个回包,而且处理流程也被打断为多个回调函数。 如果游戏中能确保除了首次登录时需要查询数据库信息,其他时候都基本能在本地获取到信息,那么这个回调机制还是可以接受,否则的话,逻辑层将面临着处处代码都被分割为多个回调函数的尴尬情况。
如果使用协程,则可以很好地解决这个问题。示例代码如下:
文件 UserServ.lua
function UserServ:login(userId) local dao = require "classes.dao.userDao" local user = dao:fetchById(userId) -- 此处user永远不为空,所以不用做判断和特殊处理 -- 处理其他逻辑 .......... end


文件 UserDao.lua
local userTable = {} function UserDao:fetchById(userId) local user = userTable[userId] if user == nil then -- 如果玩家信息为空,则填充请求玩家数据的消息 sendRequest("queryUser", userId) -- 死循环等待玩家信息,由tick触发循环,我们服务器框架为20ms触发一次tick repeat coroutine.yield() until nil ~= (user = userTable[userId]) end return user end -- 获取到用户信息的回调函数,填写内存中的用户表 function UserDao:setUser(user) userTable[user.id] = user end


script在处理用户登录时发现本地没有用户数据,他将数据查询请求作为回包(不需要逻辑处理的上下文信息),并且将自身协程暂停。在收到数据库服务器的查询结果后,调用固定的设置函数将用户信息设置好;这样之前暂停的协程可以继续运行,返回有效的玩家信息。 从上面的代码可以看出,将获用户信息的复杂操作封装到数据层(Dao),代码主要逻辑层(Serv)中的逻辑就相当流畅了。
如果有语法错误请大家包涵,文章我在有这个想法但是在实际编码之前就写了,但是现在我们项目的整个游戏框架已经运行起来了,证明这个思想是可行的。 另外一个比较复杂的地方是逻辑层入口处对协程的管理,如果有兴趣并且遇到麻烦了的话可以进一步交流。完整源代码我不能拿出来,是公司的项目,但是可以帮助你解决遇到的问题。