之前在客户端开发中,就发现了协程在代码中的方便之处。比如我在获取一个资源时,不使用协程的情况下,只能使用回调函数,代码大致如下:
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)中的逻辑就相当流畅了。
如果有语法错误请大家包涵,文章我在有这个想法但是在实际编码之前就写了,但是现在我们项目的整个游戏框架已经运行起来了,证明这个思想是可行的。
另外一个比较复杂的地方是逻辑层入口处对协程的管理,如果有兴趣并且遇到麻烦了的话可以进一步交流。完整源代码我不能拿出来,是公司的项目,但是可以帮助你解决遇到的问题。