協程在遊戲服務器開發中的應用

之前在客戶端開發中,就發現了協程在代碼中的方便之處。比如我在獲取一個資源時,不使用協程的情況下,只能使用回調函數,代碼大致如下:


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)中的邏輯就相當流暢了。


如果有語法錯誤請大家包涵,文章我在有這個想法但是在實際編碼之前就寫了,但是現在我們項目的整個遊戲框架已經運行起來了,證明這個思想是可行的。

另外一個比較複雜的地方是邏輯層入口處對協程的管理,如果有興趣並且遇到麻煩了的話可以進一步交流。完整源代碼我不能拿出來,是公司的項目,但是可以幫助你解決遇到的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章