之前在客戶端開發中,就發現了協程在代碼中的方便之處。比如我在獲取一個資源時,不使用協程的情況下,只能使用回調函數,代碼大致如下:
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)中的邏輯就相當流暢了。
如果有語法錯誤請大家包涵,文章我在有這個想法但是在實際編碼之前就寫了,但是現在我們項目的整個遊戲框架已經運行起來了,證明這個思想是可行的。
另外一個比較複雜的地方是邏輯層入口處對協程的管理,如果有興趣並且遇到麻煩了的話可以進一步交流。完整源代碼我不能拿出來,是公司的項目,但是可以幫助你解決遇到的問題。