原文地址:https://blog.csdn.net/erlib/article/details/8936990
登錄服的設(shè)計(jì) -- 功能需求
正如我們?cè)谇懊嬖懻撨^(guò)的刊殉,登錄服要實(shí)現(xiàn)的功能相當(dāng)簡(jiǎn)單,就是帳號(hào)驗(yàn)證而线。為了便于描述萧落,我們暫不引入那些討論過(guò)的優(yōu)化手段细疚,先以最簡(jiǎn)單的方式實(shí)現(xiàn)谋作,另外也將基本以mangos的代碼作為參考來(lái)進(jìn)行描述息裸。
想象一下帳號(hào)驗(yàn)證的實(shí)現(xiàn)方法舀锨,最容易的那就是把用戶輸入的明文用帳號(hào)和密碼直接發(fā)給登錄服岭洲,服務(wù)器根據(jù)帳號(hào)從數(shù)據(jù)庫(kù)中取出密碼,與用戶輸入的密碼相比較坎匿。
這個(gè)方法存在的安全隱患實(shí)在太大盾剩,明文的密碼傳輸太容易被截獲了。那我們?cè)囍趥鬏斨跋燃右幌旅芴媸撸瑸榱朔?wù)器能進(jìn)行密碼比較告私,我們應(yīng)該采用一個(gè)可逆的加密算法,在服務(wù)器端把這個(gè)加密后的字串還原為原始的明文密碼承桥,然后與數(shù)據(jù)庫(kù)密碼進(jìn)行比較驻粟。既然是一個(gè)可逆的過(guò)程,那外掛制作者總有辦法知道我們的加密過(guò)程凶异,所以蜀撑,這個(gè)方法仍不夠安全。
哦剩彬,如果我們只是希望密碼不可能被還原出來(lái)酷麦,那還不容易嗎,使用一個(gè)不可逆的散列算法就行了喉恋。用戶在登錄時(shí)發(fā)送給服務(wù)器的是明文的帳號(hào)和經(jīng)散列后的不可逆密碼串沃饶,服務(wù)器取出密碼后也用同樣的算法進(jìn)行散列后再進(jìn)行比較。比如轻黑,我們就用使用最廣泛的md5算法吧糊肤。噢,不要管那個(gè)王小云的什么論文苔悦,如果我真有那么好的運(yùn)氣轩褐,早中500w了,還用在這考慮該死的服務(wù)器設(shè)計(jì)嗎玖详?
似乎是一個(gè)很完美的方案把介,外掛制作者再也偷不到我們的密碼了勤讽。慢著,外掛偷密碼的目的是什么拗踢?是為了能用我們的帳號(hào)進(jìn)游戲脚牍!如果我們總是用一種固定的算法來(lái)對(duì)密碼做散列,那外掛只需要記住這個(gè)散列后的字串就行了巢墅,用這個(gè)做密碼就可以成功登錄诸狭。
嗯,這個(gè)問(wèn)題好解決君纫,我們不要用固定的算法進(jìn)行散列就是了驯遇。只是,問(wèn)題在于服務(wù)器與客戶端采用的散列算法得出的字串必須是相同的蓄髓,或者是可驗(yàn)證其是否匹配的叉庐。很幸運(yùn)的是,偉大的數(shù)學(xué)字們?cè)缇蜑槲覀儨?zhǔn)備好了很多優(yōu)秀的這類(lèi)算法会喝,而且經(jīng)理論和實(shí)踐都證明他們也確實(shí)是足夠安全的陡叠。
這其中之一是一個(gè)叫做SRP的算法,全稱(chēng)叫做Secure Remote Password肢执,即安全遠(yuǎn)程密碼枉阵。wow使用的是第6版,也就是SRP6算法预茄。有關(guān)其中的數(shù)學(xué)證明兴溜,如果有人能向我解釋清楚,并能讓我真正弄明白的話反璃,我將非常感激昵慌。不過(guò)其代碼實(shí)現(xiàn)步驟倒是并不復(fù)雜,mangos中的代碼也還算清晰淮蜈,我們也不再贅述斋攀。
登錄服除了帳號(hào)驗(yàn)證外還得提供另一項(xiàng)功能,就是在玩家的帳號(hào)驗(yàn)證成功后返回給他一個(gè)服務(wù)器列表讓他去選擇梧田。這個(gè)列表的狀態(tài)要定時(shí)刷新淳蔼,可能有新的游戲世界開(kāi)放了,也可能有些游戲世界非常不幸地停止運(yùn)轉(zhuǎn)了裁眯,這些狀態(tài)的變化都要盡可能及時(shí)地讓玩家知道鹉梨。不管發(fā)生了什么事,用戶都有權(quán)利知道穿稳,特別是對(duì)于付過(guò)費(fèi)的用戶來(lái)說(shuō)存皂,我們不該藏著掖著,不是嗎?
這個(gè)游戲世界列表的功能將由大區(qū)服來(lái)提供旦袋,具體的結(jié)構(gòu)我們?cè)谥耙裁枋鲞^(guò)骤菠,這里暫不做討論。登錄服將從大區(qū)服上獲取到的游戲世界列表發(fā)給已驗(yàn)證通過(guò)的客戶端即可疤孕。好了商乎,登錄服要實(shí)現(xiàn)的功能就這些,很簡(jiǎn)單祭阀,是吧鹉戚。
確實(shí)是太簡(jiǎn)單了,不過(guò)簡(jiǎn)單的結(jié)構(gòu)正好更適合我們來(lái)看一看游戲服務(wù)器內(nèi)部的模塊結(jié)構(gòu)专控,以及一些服務(wù)器共有組件的實(shí)現(xiàn)方法抹凳。這就留作下一篇吧。
服務(wù)器公共組件實(shí)現(xiàn) -- mangos的游戲主循環(huán)
當(dāng)閱讀一項(xiàng)工程的源碼時(shí)伦腐,我們大概會(huì)選擇從main函數(shù)開(kāi)始却桶,而當(dāng)開(kāi)始一項(xiàng)新的工程時(shí),第一個(gè)寫(xiě)下的函數(shù)大多也是main蔗牡。那我們就先來(lái)看看,游戲服務(wù)器代碼實(shí)現(xiàn)中嗅剖,main函數(shù)都做了些什么辩越。
由于我在讀技術(shù)文章時(shí)最不喜看到的就是大段大段的代碼,特別是那些直接Ctrl+C再Ctrl+V后未做任何修改的代碼信粮,用句時(shí)髦的話說(shuō)黔攒,一點(diǎn)技術(shù)含量都沒(méi)有!所以在我們今后所要討論的內(nèi)容中强缘,盡量會(huì)避免出現(xiàn)直接的代碼督惰,在有些地方確實(shí)需要代碼來(lái)表述時(shí),也將會(huì)選擇使用偽碼旅掂。
先從mangos的登錄服代碼開(kāi)始赏胚。mangos的登錄服是一個(gè)單線程的結(jié)構(gòu),雖然在數(shù)據(jù)庫(kù)連接中可以開(kāi)啟一個(gè)獨(dú)立的線程商虐,但這個(gè)線程也只是對(duì)無(wú)返回結(jié)果的執(zhí)行類(lèi)SQL做緩沖觉阅,而對(duì)需要有返回結(jié)果的查詢(xún)類(lèi)SQL還是在主邏輯線程中阻塞調(diào)用的。
登錄服中唯一的這一個(gè)線程秘车,也就是主循環(huán)線程對(duì)監(jiān)聽(tīng)的socket做select操作典勇,為每個(gè)連接進(jìn)來(lái)的客戶端讀取其上的數(shù)據(jù)并立即進(jìn)行處理,直到服務(wù)器收到SIGABRT或SIGBREAK信號(hào)時(shí)結(jié)束叮趴。
所以割笙,mangos登錄服主循環(huán)的邏輯,也包括后面游戲服的邏輯眯亦,主循環(huán)的關(guān)鍵代碼其實(shí)是在SocketHandler中伤溉,也就是那個(gè)Select函數(shù)中般码。檢查所有的連接,對(duì)新到來(lái)的連接調(diào)用OnAccept方法谈火,有數(shù)據(jù)到來(lái)的連接則調(diào)用OnRead方法侈询,然后socket處理器自己定義對(duì)接收到的數(shù)據(jù)如何處理。
很簡(jiǎn)單的結(jié)構(gòu)糯耍,也比較容易理解扔字。
只是,在對(duì)性能要求比較高的服務(wù)器上温技,select一般不會(huì)是最好的選擇革为。如果我們使用windows平臺(tái),那IOCP將是首選舵鳞;如果是linux震檩,epool將是不二選擇。我們也不打算討論基于IOCP或是基于epool的服務(wù)器實(shí)現(xiàn)蜓堕,如果僅僅只是要實(shí)現(xiàn)服務(wù)器功能抛虏,很簡(jiǎn)單的幾個(gè)API調(diào)用即可,而且網(wǎng)上已有很多好的教程套才;如果是要做一個(gè)成熟的網(wǎng)絡(luò)服務(wù)器產(chǎn)品迂猴,不是我?guī)灼?jiǎn)單的技術(shù)介紹文章所能達(dá)到。
另外背伴,在服務(wù)器實(shí)現(xiàn)上沸毁,網(wǎng)絡(luò)IO與邏輯處理一般會(huì)放在不同的線程中,以免耗時(shí)較長(zhǎng)的IO過(guò)程阻塞住了需要立即反應(yīng)的游戲邏輯傻寂。
數(shù)據(jù)庫(kù)的處理也類(lèi)似息尺,會(huì)使用異步的方式,也是避免耗時(shí)的查詢(xún)過(guò)程將游戲服務(wù)器主循環(huán)阻塞住疾掰。想象一下搂誉,因某個(gè)玩家上線而發(fā)起的一次數(shù)據(jù)庫(kù)查詢(xún)操作導(dǎo)致服務(wù)器內(nèi)所有在線玩家都卡住不動(dòng)將是多么恐怖的一件事!
另外還有一些如事件静檬、腳本勒葱、消息隊(duì)列、狀態(tài)機(jī)巴柿、日志和異常處理等公共組件凛虽,我們也會(huì)在接下來(lái)的時(shí)間里進(jìn)行探討。
服務(wù)器公共組件實(shí)現(xiàn) -- 繼續(xù)來(lái)說(shuō)主循環(huán)
前面我們只簡(jiǎn)單了解了下mangos登錄服的程序結(jié)構(gòu)广恢,也發(fā)現(xiàn)了一些不足之處凯旋,現(xiàn)在我們就來(lái)看看如何提供一個(gè)更好的方案。
正如我們?cè)懻撨^(guò)的,為了游戲主邏輯循環(huán)的流暢運(yùn)行至非,所有比較耗時(shí)的IO操作都會(huì)分享到單獨(dú)的線程中去做钠署,如網(wǎng)絡(luò)IO,數(shù)據(jù)庫(kù)IO和日志IO等荒椭。當(dāng)然谐鼎,也有把這些分享到單獨(dú)的進(jìn)程中去做的。
另外對(duì)于大多數(shù)服務(wù)器程序來(lái)說(shuō)趣惠,在運(yùn)行時(shí)都是作為精靈進(jìn)程或服務(wù)進(jìn)程的狸棍,所以我們并不需要服務(wù)器能夠處理控制臺(tái)用戶輸入,我們所要處理的數(shù)據(jù)來(lái)源都來(lái)自網(wǎng)絡(luò)味悄。
這樣草戈,主邏輯循環(huán)所要做的就是不停要取消息包來(lái)處理,當(dāng)然這些消息包不僅有來(lái)自客戶端的玩家操作數(shù)據(jù)包侍瑟,也有來(lái)自GM服務(wù)器的管理命令唐片,還包括來(lái)自數(shù)據(jù)庫(kù)查詢(xún)線程的返回結(jié)果消息包。這個(gè)循環(huán)將一直持續(xù)涨颜,直到收到一個(gè)通知服務(wù)器關(guān)閉的消息包费韭。
主邏輯循環(huán)的結(jié)構(gòu)還是很簡(jiǎn)單的,復(fù)雜的部分都在如何處理這些消息包的邏輯上庭瑰。我們可以用一段簡(jiǎn)單的偽碼來(lái)描述這個(gè)循環(huán)過(guò)程:
while (Message* msg = getMessage())
{
if (msg為服務(wù)器關(guān)閉消息)
break;
處理msg消息;
}
這里就有一個(gè)問(wèn)題需要探討了揽思,在getMessage()的時(shí)候,我們應(yīng)該去哪里取消息见擦?前面我們考慮過(guò),至少會(huì)有三個(gè)消息來(lái)源羹令,而我們還討論過(guò)鲤屡,這些消息源的IO操作都是在獨(dú)立的線程中進(jìn)行的,我們這里的主線程不應(yīng)該直接去那幾處消息源進(jìn)行阻塞式的IO操作福侈。
很簡(jiǎn)單酒来,讓那些獨(dú)立的IO線程在接收完數(shù)據(jù)后自己送過(guò)來(lái)就是了。好比是肪凛,我這里提供了一個(gè)倉(cāng)庫(kù)堰汉,有很多的供貨商,他們有貨要給我的時(shí)候只需要交到倉(cāng)庫(kù)伟墙,然后我再到倉(cāng)庫(kù)去取就是了翘鸭,這個(gè)倉(cāng)庫(kù)也就是消息隊(duì)列。消息隊(duì)列是一個(gè)普通的隊(duì)列實(shí)現(xiàn)戳葵,當(dāng)然必須要提供多線程互斥訪問(wèn)的安全性支持就乓,其基本的接口定義大概類(lèi)似這樣:
IMessageQueue
{
void putMessage(Message*);
Message* getMessage();
}
網(wǎng)絡(luò)IO,數(shù)據(jù)庫(kù)IO線程把整理好的消息包都加入到主邏輯循環(huán)線程的這個(gè)消息隊(duì)列中便返回。有關(guān)消息隊(duì)列的實(shí)現(xiàn)和線程間消息的傳遞在ACE中有比較完全的代碼實(shí)現(xiàn)及描述生蚁,還有一些使用示例噩翠,是個(gè)很好的參考。
這樣的話邦投,我們的主循環(huán)就很清晰了伤锚,從主線程的消息隊(duì)列中取消息,處理消息志衣,再取下一條消息......
服務(wù)器公共組件實(shí)現(xiàn) -- 消息隊(duì)列
既然說(shuō)到了消息隊(duì)列屯援,那我們繼續(xù)來(lái)稍微多聊一點(diǎn)吧。
我們所能想到的最簡(jiǎn)單的消息隊(duì)列可能就是使用stl的list來(lái)實(shí)現(xiàn)了蠢涝,即消息隊(duì)列內(nèi)部維護(hù)一個(gè)list和一個(gè)互斥鎖玄呛,putMessage時(shí)將message加入到隊(duì)列尾,getMessage時(shí)從隊(duì)列頭取一個(gè)message返回和二,同時(shí)在getMessage和putMessage之前都要求先獲取鎖資源徘铝。
實(shí)現(xiàn)雖然簡(jiǎn)單,但功能是絕對(duì)滿足需求的惯吕,只是性能上可能稍稍有些不盡如人意惕它。其最大的問(wèn)題在頻繁的鎖競(jìng)爭(zhēng)上。
對(duì)于如何減少鎖競(jìng)爭(zhēng)次數(shù)的優(yōu)化方案废登,Ghost Cheng提出了一種淹魄。提供一個(gè)隊(duì)列容器,里面有多個(gè)隊(duì)列堡距,每個(gè)隊(duì)列都可固定存放一定數(shù)量的消息甲锡。網(wǎng)絡(luò)IO線程要給邏輯線程投遞消息時(shí),會(huì)從隊(duì)列容器中取一個(gè)空隊(duì)列來(lái)使用羽戒,直到將該隊(duì)列填滿后再放回容器中換另一個(gè)空隊(duì)列缤沦。而邏輯線程取消息時(shí)是從隊(duì)列容器中取一個(gè)有消息的隊(duì)列來(lái)讀取,處理完后清空隊(duì)列再放回到容器中易稠。
這樣便使得只有在對(duì)隊(duì)列容器進(jìn)行操作時(shí)才需要加鎖缸废,而IO線程和邏輯線程在操作自己當(dāng)前使用的隊(duì)列時(shí)都不需要加鎖,所以鎖競(jìng)爭(zhēng)的機(jī)會(huì)大大減少了驶社。
這里為每個(gè)隊(duì)列設(shè)了個(gè)最大消息數(shù)企量,看來(lái)好像是打算只有當(dāng)IO線程寫(xiě)滿隊(duì)列時(shí)才會(huì)將其放回到容器中換另一個(gè)隊(duì)列。那這樣有時(shí)也會(huì)出現(xiàn)IO線程未寫(xiě)滿一個(gè)隊(duì)列亡电,而邏輯線程又沒(méi)有數(shù)據(jù)可處理的情況届巩,特別是當(dāng)數(shù)據(jù)量很少時(shí)可能會(huì)很容易出現(xiàn)。Ghost Cheng在他的描述中沒(méi)有講到如何解決這種問(wèn)題份乒,但我們可以先來(lái)看看另一個(gè)方案姆泻。
這個(gè)方案與上一個(gè)方案基本類(lèi)似零酪,只是不再提供隊(duì)列容器,因?yàn)樵谶@個(gè)方案中只使用了兩個(gè)隊(duì)列拇勃,arthur在他的一封郵件中描述了這個(gè)方案的實(shí)現(xiàn)及部分代碼四苇。兩個(gè)隊(duì)列,一個(gè)給邏輯線程讀方咆,一個(gè)給IO線程用來(lái)寫(xiě)月腋,當(dāng)邏輯線程讀完隊(duì)列后會(huì)將自己的隊(duì)列與IO線程的隊(duì)列相調(diào)換。所以瓣赂,這種方案下加鎖的次數(shù)會(huì)比較多一些榆骚,IO線程每次寫(xiě)隊(duì)列時(shí)都要加鎖,邏輯線程在調(diào)換隊(duì)列時(shí)也需要加鎖煌集,但邏輯線程在讀隊(duì)列時(shí)是不需要加鎖的妓肢。
雖然看起來(lái)鎖的調(diào)用次數(shù)是比前一種方案要多很多,但實(shí)際上大部分鎖調(diào)用都是不會(huì)引起阻塞的苫纤,只有在邏輯線程調(diào)換隊(duì)列的那一瞬間可能會(huì)使得某個(gè)線程阻塞一下碉钠。另外對(duì)于鎖調(diào)用過(guò)程本身來(lái)說(shuō),其開(kāi)銷(xiāo)是完全可以忽略的卷拘,我們所不能忍受的僅僅是因?yàn)殒i調(diào)用而引起的阻塞而已喊废。
兩種方案都是很優(yōu)秀的優(yōu)化方案,但也都是有其適用范圍的栗弟。Ghost Cheng的方案因?yàn)樘峁┝硕鄠€(gè)隊(duì)列污筷,可以使得多個(gè)IO線程可以總工程師的,互不干擾的使用自己的隊(duì)列乍赫,只是還有一個(gè)遺留問(wèn)題我們還不了解其解決方法瓣蛀。arthur的方案很好的解決了上一個(gè)方案遺留的問(wèn)題,但因?yàn)橹挥幸粋€(gè)寫(xiě)隊(duì)列,所以當(dāng)想要提供多個(gè)IO線程時(shí),線程間互斥地寫(xiě)入數(shù)據(jù)可能會(huì)增大競(jìng)爭(zhēng)的機(jī)會(huì)扒怖,當(dāng)然,如果只有一個(gè)IO線程那將是非常完美的。
服務(wù)器公共組件實(shí)現(xiàn) -- 環(huán)形緩沖區(qū)
消息隊(duì)列鎖調(diào)用太頻繁的問(wèn)題算是解決了溪猿,另一個(gè)讓人有些苦惱的大概是這太多的內(nèi)存分配和釋放操作了钩杰。頻繁的內(nèi)存分配不但增加了系統(tǒng)開(kāi)銷(xiāo),更使得內(nèi)存碎片不斷增多诊县,非常不利于我們的服務(wù)器長(zhǎng)期穩(wěn)定運(yùn)行讲弄。也許我們可以使用內(nèi)存池,比如SGI STL中附帶的小內(nèi)存分配器依痊。但是對(duì)于這種按照嚴(yán)格的先進(jìn)先出順序處理的避除,塊大小并不算小的怎披,而且塊大小也并不統(tǒng)一的內(nèi)存分配情況來(lái)說(shuō),更多使用的是一種叫做環(huán)形緩沖區(qū)的方案瓶摆,mangos的網(wǎng)絡(luò)代碼中也有這么一個(gè)東西凉逛,其原理也是比較簡(jiǎn)單的。
就好比兩個(gè)人圍著一張圓形的桌子在追逐群井,跑的人被網(wǎng)絡(luò)IO線程所控制状飞,當(dāng)寫(xiě)入數(shù)據(jù)時(shí),這個(gè)人就往前跑书斜;追的人就是邏輯線程诬辈,會(huì)一直往前追直到追上跑的人。如果追上了怎么辦荐吉?那就是沒(méi)有數(shù)據(jù)可讀了焙糟,先等會(huì)兒?jiǎn)h,等跑的人向前跑幾步了再追样屠,總不能讓游戲沒(méi)得玩了吧穿撮。那要是追的人跑的太慢,跑的人轉(zhuǎn)了一圈過(guò)來(lái)反追上追的人了呢瞧哟?那您也先歇會(huì)兒吧混巧。要是一直這么反著追,估計(jì)您就只能換一個(gè)跑的更快的追逐者了勤揩,要不這游戲還真沒(méi)法玩下去咧党。
前面我們特別強(qiáng)調(diào)了,按照嚴(yán)格的先進(jìn)先出順序進(jìn)行處理陨亡,這是環(huán)形緩沖區(qū)的使用必須遵守的一項(xiàng)要求傍衡。也就是,大家都得遵守規(guī)定负蠕,追的人不能從桌子上跨過(guò)去蛙埂,跑的人當(dāng)然也不允許反過(guò)來(lái)跑。至于為什么遮糖,不需要多做解釋了吧绣的。
環(huán)形緩沖區(qū)是一項(xiàng)很好的技術(shù),不用頻繁的分配內(nèi)存欲账,而且在大多數(shù)情況下屡江,內(nèi)存的反復(fù)使用也使得我們能用更少的內(nèi)存塊做更多的事。
在網(wǎng)絡(luò)IO線程中赛不,我們會(huì)為每一個(gè)連接都準(zhǔn)備一個(gè)環(huán)形緩沖區(qū)惩嘉,用于臨時(shí)存放接收到的數(shù)據(jù),以應(yīng)付半包及粘包的情況踢故。在解包及解密完成后文黎,我們會(huì)將這個(gè)數(shù)據(jù)包復(fù)制到邏輯線程消息隊(duì)列中惹苗,如果我們只使用一個(gè)隊(duì)列,那這里也將會(huì)是個(gè)環(huán)形緩沖區(qū)耸峭,IO線程往里寫(xiě)桩蓉,邏輯線程在后面讀,互相追逐抓艳〈セ可要是我們使用了前面介紹的優(yōu)化方案后,可能這里便不再需要環(huán)形緩沖區(qū)了玷或,至少我們并不再需要他們是環(huán)形的了儡首。因?yàn)槲覀儗?duì)同一個(gè)隊(duì)列不再會(huì)出現(xiàn)同時(shí)讀和寫(xiě)的情況,每個(gè)隊(duì)列在寫(xiě)滿后交給邏輯線程去讀偏友,邏輯線程讀完后清空隊(duì)列再交給IO線程去寫(xiě)蔬胯,一段固定大小的緩沖區(qū)即可。沒(méi)關(guān)系位他,這么好的技術(shù)氛濒,在別的地方一定也會(huì)用到的。
服務(wù)器公共組件實(shí)現(xiàn) -- 發(fā)包的方式
前面一直都在說(shuō)接收數(shù)據(jù)時(shí)的處理方法鹅髓,我們應(yīng)該用專(zhuān)門(mén)的IO線程舞竿,接收到完整的消息包后加入到主線程的消息隊(duì)列,但是主線程如何發(fā)送數(shù)據(jù)還沒(méi)有探討過(guò)窿冯。
一般來(lái)說(shuō)最直接的方法就是邏輯線程什么時(shí)候想發(fā)數(shù)據(jù)了就直接調(diào)用相關(guān)的socket API發(fā)送骗奖,這要求服務(wù)器的玩家對(duì)象中保存其連接的socket句柄。但是直接send調(diào)用有時(shí)候有會(huì)存在一些問(wèn)題醒串,比如遇到系統(tǒng)的發(fā)送緩沖區(qū)滿而阻塞住的情況执桌,或者只發(fā)送了一部分?jǐn)?shù)據(jù)的情況也時(shí)有發(fā)生。我們可以將要發(fā)送的數(shù)據(jù)先緩存一下芜赌,這樣遇到未發(fā)送完的仰挣,在邏輯線程的下一次處理時(shí)可以接著再發(fā)送。
考慮數(shù)據(jù)緩存的話缠沈,那這里這可以有兩種實(shí)現(xiàn)方式了膘壶,一是為每個(gè)玩家準(zhǔn)備一個(gè)緩沖區(qū),另外就是只有一個(gè)全局的緩沖區(qū)洲愤,要發(fā)送的數(shù)據(jù)加入到全局緩沖區(qū)的時(shí)候同時(shí)要指明這個(gè)數(shù)據(jù)是發(fā)到哪個(gè)socket的颓芭。如果使用全局緩沖區(qū)的話,那我們可以再進(jìn)一步禽篱,使用一個(gè)獨(dú)立的線程來(lái)處理數(shù)據(jù)發(fā)送畜伐,類(lèi)似于邏輯線程對(duì)數(shù)據(jù)的處理方式馍惹,這個(gè)獨(dú)立發(fā)送線程也維護(hù)一個(gè)消息隊(duì)列躺率,邏輯線程要發(fā)數(shù)據(jù)時(shí)也只是把數(shù)據(jù)加入到這個(gè)隊(duì)列中玛界,發(fā)送線程循環(huán)取包來(lái)執(zhí)行send調(diào)用,這時(shí)的阻塞也就不會(huì)對(duì)邏輯線程有任何影響了悼吱。
采用第二種方式還可以附帶一個(gè)優(yōu)化方案慎框。一般對(duì)于廣播消息而言,發(fā)送給周?chē)婕业臄?shù)據(jù)都是完全相同的后添,我們?nèi)绻捎媒o每個(gè)玩家一個(gè)緩沖隊(duì)列的方式笨枯,這個(gè)數(shù)據(jù)包將需要拷貝多份,而采用一個(gè)全局發(fā)送隊(duì)列時(shí)遇西,我們只需要把這個(gè)消息入隊(duì)一次馅精,同時(shí)指明該消息包是要發(fā)送給哪些socket的即可。有關(guān)該優(yōu)化的說(shuō)明在云風(fēng)描述其連接服務(wù)器實(shí)現(xiàn)的blog文章中也有講到粱檀,有興趣的可以去閱讀一下洲敢。
服務(wù)器公共組件實(shí)現(xiàn) -- 狀態(tài)機(jī)
有關(guān)State模式的設(shè)計(jì)意圖及實(shí)現(xiàn)就不從設(shè)計(jì)模式中摘抄了,我們只來(lái)看看游戲服務(wù)器編程中如何使用State設(shè)計(jì)模式茄蚯。
首先還是從mangos的代碼開(kāi)始看起压彭,我們注意到登錄服在處理客戶端發(fā)來(lái)的消息時(shí)用到了這樣一個(gè)結(jié)構(gòu)體:
struct AuthHandler
{
eAuthCmd cmd;
uint32 status;
bool (AuthSocket::*handler)(void);
};
該結(jié)構(gòu)體定義了每個(gè)消息碼的處理函數(shù)及需要的狀態(tài)標(biāo)識(shí),只有當(dāng)前狀態(tài)滿足要求時(shí)才會(huì)調(diào)用指定的處理函數(shù)渗常,否則這個(gè)消息碼的出現(xiàn)是不合法的壮不。這個(gè)status狀態(tài)標(biāo)識(shí)的定義是一個(gè)宏,有兩種有效的標(biāo)識(shí)皱碘,STATUS_CONNECTED和STATUS_AUTHED询一,也就是未認(rèn)證通過(guò)和已認(rèn)證通過(guò)。而這個(gè)狀態(tài)標(biāo)識(shí)的改變是在運(yùn)行時(shí)進(jìn)行的尸执,確切的說(shuō)是在收到某個(gè)消息并正確處理完后改變的家凯。
我們?cè)賮?lái)看看設(shè)計(jì)模式中對(duì)State模式的說(shuō)明,其中關(guān)于State模式適用情況里有一條如失,當(dāng)操作中含有龐大的多分支的條件語(yǔ)句绊诲,且這些分支依賴(lài)于該對(duì)象的狀態(tài),這個(gè)狀態(tài)通常用一個(gè)或多個(gè)枚舉變量表示褪贵。
描述的情況與我們這里所要處理的情況是如此的相似掂之,也許我們可以試一試。那再看看State模式提供的解決方案是怎樣的脆丁,State模式將每一個(gè)條件分支放入一個(gè)獨(dú)立的類(lèi)中世舰。
由于這里的兩個(gè)狀態(tài)標(biāo)識(shí)只區(qū)分出了兩種狀態(tài),所以槽卫,我們僅需要兩個(gè)獨(dú)立的類(lèi)跟压,用以表示兩種狀態(tài)即可。然后歼培,按照State模式的描述震蒋,我們還需要一個(gè)Context類(lèi)茸塞,也就是狀態(tài)機(jī)管理類(lèi),用以管理當(dāng)前的狀態(tài)類(lèi)查剖。稍作整理钾虐,大概的代碼會(huì)類(lèi)似這樣:
狀態(tài)基類(lèi)接口:
StateBase
{
void Enter() = 0;
void Leave() = 0;
void Process(Message* msg) = 0;
};
狀態(tài)機(jī)基類(lèi)接口:
MachineBase
{
void ChangeState(StateBase* state) = 0;
StateBase* m_curState;
};
我們的邏輯處理類(lèi)會(huì)從MachineBase派生,當(dāng)取出數(shù)據(jù)包后交給當(dāng)前狀態(tài)處理笋庄,前面描述的兩個(gè)狀態(tài)類(lèi)從StateBase派生效扫,每個(gè)狀態(tài)類(lèi)只處理該狀態(tài)標(biāo)識(shí)下需要處理的消息。當(dāng)要進(jìn)行狀態(tài)轉(zhuǎn)換時(shí)直砂,調(diào)用MachineBase的ChangeState()方法菌仁,顯示地告訴狀態(tài)機(jī)管理類(lèi)自己要轉(zhuǎn)到哪一個(gè)狀態(tài)。所以静暂,狀態(tài)類(lèi)內(nèi)部需要保存狀態(tài)機(jī)管理類(lèi)的指針掘托,這個(gè)可以在狀態(tài)類(lèi)初始化時(shí)傳入。具體的實(shí)現(xiàn)細(xì)節(jié)就不做過(guò)多描述了籍嘹。
使用狀態(tài)機(jī)雖然避免了復(fù)雜的判斷語(yǔ)句闪盔,但也引入了新的麻煩。當(dāng)我們?cè)谶M(jìn)行狀態(tài)轉(zhuǎn)換時(shí)辱士,可能會(huì)需要將一些現(xiàn)場(chǎng)數(shù)據(jù)從老狀態(tài)對(duì)象轉(zhuǎn)移到新?tīng)顟B(tài)對(duì)象泪掀,這需要在定義接口時(shí)做一下考慮。如果不希望執(zhí)行拷貝颂碘,那么這里公有的現(xiàn)場(chǎng)數(shù)據(jù)也可放到狀態(tài)機(jī)類(lèi)中异赫,只是這樣在使用時(shí)可能就不那么優(yōu)雅了。 <
BR>
正如同在設(shè)計(jì)模式中所描述的头岔,所有的模式都是已有問(wèn)題的另一種解決方案塔拳,也就是說(shuō)這并不是唯一的解決方案。放到我們今天討論的State模式中峡竣,就拿登錄服所處理的兩個(gè)狀態(tài)來(lái)說(shuō)靠抑,也許用mangos所采用的遍歷處理函數(shù)的方法可能更簡(jiǎn)單,但當(dāng)系統(tǒng)中的狀態(tài)數(shù)量增多适掰,狀態(tài)標(biāo)識(shí)也變多的時(shí)候颂碧,State模式就顯得尤其重要了。
比如在游戲服務(wù)器上玩家的狀態(tài)管理类浪,還有在實(shí)現(xiàn)NPC人工智能時(shí)的各種狀態(tài)管理载城,這些就留作以后的專(zhuān)題吧。
服務(wù)器公共組件 -- 事件與信號(hào)
關(guān)于這一節(jié)费就,這幾天已經(jīng)打了好幾遍草稿诉瓦,總覺(jué)得說(shuō)不清楚,也不好組織這些內(nèi)容,但是打鐵要趁熱睬澡,為避免熱情消退呼寸,先整理一點(diǎn)東西放這,好繼續(xù)下面的主題猴贰,以后如果有機(jī)會(huì)再回來(lái)完善吧。本節(jié)內(nèi)容欠考慮河狐,希望大家多給點(diǎn)意見(jiàn)米绕。
有些類(lèi)似于QT中的event與signal,我將一些動(dòng)作請(qǐng)求消息定義為事件馋艺,而將狀態(tài)改變消息定義為信號(hào)栅干。比如在QT應(yīng)用程序中,用戶的一次鼠標(biāo)點(diǎn)擊會(huì)產(chǎn)生一個(gè)鼠標(biāo)點(diǎn)擊事件加入到事件隊(duì)列中捐祠,當(dāng)處理此事件時(shí)可能會(huì)導(dǎo)致某個(gè)按鈕控件產(chǎn)生一個(gè)clicked()信號(hào)碱鳞。
對(duì)應(yīng)到我們的服務(wù)器上的一個(gè)例子,玩家登錄時(shí)會(huì)發(fā)給服務(wù)器一個(gè)請(qǐng)求登錄的數(shù)據(jù)包踱蛀,服務(wù)器可將其當(dāng)作一個(gè)用戶登錄事件窿给,該事件處理完后可能會(huì)產(chǎn)生一個(gè)用戶已登錄信號(hào)。
這樣率拒,與QT類(lèi)似崩泡,對(duì)于事件我們可以重定義其處理方法,甚至過(guò)濾掉某些事件使其不被處理猬膨,但對(duì)于信號(hào)我們只是收到了一個(gè)通知角撞,有些類(lèi)似于Observe模式中的觀察者,當(dāng)收到更新通知時(shí)勃痴,我們只能更新自己的狀態(tài)谒所,對(duì)剛剛發(fā)生的事件我不已不能做任何影響。
仔細(xì)來(lái)看沛申,事件與信號(hào)其實(shí)并無(wú)多大差別劣领,從我們對(duì)其需求上來(lái)說(shuō),都只要能注冊(cè)事件或信號(hào)響應(yīng)函數(shù)铁材,在事件或信號(hào)產(chǎn)生時(shí)能夠被通知到即可剖踊。但有一項(xiàng)區(qū)別在于,事件處理函數(shù)的返回值是有意義的衫贬,我們要根據(jù)這個(gè)返回值來(lái)確定是否還要繼續(xù)事件的處理德澈,比如在QT中,事件處理函數(shù)如果返回true固惯,則這個(gè)事件處理已完成梆造,QApplication會(huì)接著處理下一個(gè)事件,而如果返回false,那么事件分派函數(shù)會(huì)繼續(xù)向上尋找下一個(gè)可以處理該事件的注冊(cè)方法镇辉。信號(hào)處理函數(shù)的返回值對(duì)信號(hào)分派器來(lái)說(shuō)是無(wú)意義的屡穗。
簡(jiǎn)單點(diǎn)說(shuō),就是我們可以為事件定義過(guò)濾器忽肛,使得事件可以被過(guò)濾村砂。這一功能需求在游戲服務(wù)器上是到處存在的。
關(guān)于事件和信號(hào)機(jī)制的實(shí)現(xiàn)屹逛,網(wǎng)絡(luò)上的開(kāi)源訓(xùn)也比較多础废,比如FastDelegate,sigslot罕模,boost::signal等评腺,其中sigslot還被Google采用,在libjingle的代碼中我們可以看到他是如何被使用的淑掌。
在實(shí)現(xiàn)事件和信號(hào)機(jī)制時(shí)或許可以考慮用同一套實(shí)現(xiàn)蒿讥,在前面我們就分析過(guò),兩者唯一的區(qū)別僅在于返回值的處理上抛腕。
另外還有一個(gè)需要我們關(guān)注的問(wèn)題是事件和信號(hào)處理時(shí)的優(yōu)先級(jí)問(wèn)題芋绸。在QT中,事件因?yàn)槎际桥c窗口相關(guān)的担敌,所以事件回調(diào)時(shí)都是從當(dāng)前窗口開(kāi)始侥钳,一級(jí)一級(jí)向上派發(fā),直到有一個(gè)窗口返回true柄错,截?cái)嗔耸录奶幚頌橹瓜隙帷?duì)于信號(hào)的處理則比較簡(jiǎn)單,默認(rèn)是沒(méi)有順序的售貌,如果需要明確的順序给猾,可以在信號(hào)注冊(cè)時(shí)顯示地指明槽的位置。
在我們的需求中颂跨,因?yàn)闆](méi)有窗口的概念敢伸,事件的處理也與信號(hào)類(lèi)似,對(duì)注冊(cè)過(guò)的處理器要按某個(gè)順序依次回調(diào)恒削,所以?xún)?yōu)先級(jí)的設(shè)置功能是需要的池颈。
最后需要我們考慮的是事件和信號(hào)的處理方式。在QT中钓丰,事件使用了一個(gè)事件隊(duì)列來(lái)維護(hù)躯砰,如果事件的處理中又產(chǎn)生了新的事件,那么新的事件會(huì)加入到隊(duì)列尾携丁,直到當(dāng)前事件處理完畢后琢歇,QApplication再去隊(duì)列頭取下一個(gè)事件來(lái)處理。而信號(hào)的處理方式有些不同,信號(hào)處理是立即回調(diào)的李茫,也就是一個(gè)信號(hào)產(chǎn)生后揭保,他上面所注冊(cè)的所有槽都會(huì)立即被回調(diào)。這樣就會(huì)產(chǎn)生一個(gè)遞歸調(diào)用的問(wèn)題魄宏,比如某個(gè)信號(hào)處理器中又產(chǎn)生了一個(gè)信號(hào)秸侣,會(huì)使得信號(hào)的處理像一棵樹(shù)一樣的展開(kāi)。我們需要注意的一個(gè)很重要的問(wèn)題是會(huì)不會(huì)引起循環(huán)調(diào)用宠互。
關(guān)于事件機(jī)制的考慮其實(shí)還很多味榛,但都是一些不成熟的想法。在上面的文字中就同時(shí)出現(xiàn)了消息名秀、事件和信號(hào)三個(gè)相近的概念,而在實(shí)際處理中藕溅,經(jīng)常發(fā)現(xiàn)三者不知道如何界定的情況匕得,實(shí)際的情況比我在這里描述的要混亂的多。
這里也就當(dāng)是挖下一個(gè)坑巾表,希望能夠有所交流汁掠。
再談登錄服的實(shí)現(xiàn)
離我們的登錄服實(shí)現(xiàn)已經(jīng)太遠(yuǎn)了,先拉回來(lái)一下集币。
關(guān)于登錄服考阱、大區(qū)服及游戲世界服的結(jié)構(gòu)之前已做過(guò)探討,這里再把各自的職責(zé)和關(guān)系列一下鞠苟。
GateWay/WorldServer GateWay/WodlServer LoginServer LoginServer DNSServer WorldServerMgr
| | | | |
---------------------------------------------------------------------------------------------
| | |
internet
|
clients
其中DNSServer負(fù)責(zé)帶負(fù)載均衡的域名解析服務(wù)乞榨,返回LoginServer的IP地址給客戶端。WorldServerMgr維護(hù)當(dāng)前大區(qū)內(nèi)的世界服列表当娱,LoginServer會(huì)從這里取世界列表發(fā)給客戶端吃既。LoginServer處理玩家的登錄及世界服選擇請(qǐng)求。GateWay/WorldServer為各個(gè)獨(dú)立的世界服或者通過(guò)網(wǎng)關(guān)連接到后面的世界服跨细。
在mangos的代碼中鹦倚,我們注意到登錄服是從數(shù)據(jù)庫(kù)中取的世界列表,而在wow官方服務(wù)器中冀惭,我們卻會(huì)注意到震叙,這個(gè)世界服列表并不是一開(kāi)始就固定,而是動(dòng)態(tài)生成的散休。當(dāng)每周一次的維護(hù)完成之后媒楼,我們可以很明顯的看到這個(gè)列表生成的過(guò)程。剛開(kāi)始時(shí)戚丸,世界列表是空的匣砖,慢慢的,世界服會(huì)一個(gè)個(gè)加入進(jìn)來(lái),而這里如果有世界服當(dāng)機(jī)猴鲫,他會(huì)顯示為離線对人,不會(huì)從列表中刪除。但是當(dāng)下一次服務(wù)器再維護(hù)后拂共,所有的世界服都不存在了牺弄,全部重新開(kāi)始添加。
從上面的過(guò)程描述中宜狐,我們很容易想到利用一個(gè)臨時(shí)的列表來(lái)保存世界服信息势告,這也是我們?cè)黾覹orldServerMgr服務(wù)器的目的所在。GateWay/WorldServer在啟動(dòng)時(shí)會(huì)自動(dòng)向WorldServerMgr注冊(cè)自己抚恒,這樣就把自己所代表的游戲世界添加到世界列表中了咱台。類(lèi)似的,如果DNSServer也可以讓LoginServer自己去注冊(cè)俭驮,這樣在臨時(shí)LoginServer時(shí)就不需要去改動(dòng)DNSServer的配置文件了回溺。
WorldServerMgr內(nèi)部的實(shí)現(xiàn)很簡(jiǎn)單,監(jiān)聽(tīng)一個(gè)固定的端口混萝,接受來(lái)自WorldServer的主動(dòng)連接遗遵,并檢測(cè)其狀態(tài)。這里可以用一個(gè)心跳包來(lái)實(shí)現(xiàn)其狀態(tài)的檢測(cè)逸嘀,如果WorldServer的連接斷開(kāi)或者在規(guī)定時(shí)間內(nèi)未收到心跳包车要,則將其狀態(tài)更新為離線。另外WorldServerMgr還處理來(lái)自LoginServer的列表請(qǐng)求崭倘。由于世界列表并不常變化翼岁,所以LoginServer沒(méi)有必要每次發(fā)送世界列表時(shí)都到WorldServerMgr上去取,LoginServer完全可以自己維護(hù)一個(gè)列表司光,當(dāng)WorldServerMgr上的列表發(fā)生變化時(shí)登澜,WorldServerMgr會(huì)主動(dòng)通知所有的LoginServer也更新一下自己的列表。這個(gè)或許就可以用前面描述過(guò)的事件方式飘庄,或者就是觀察者模式了脑蠕。
WorldServerMgr實(shí)現(xiàn)所要考慮的內(nèi)容就這些,我們?cè)賮?lái)看看LoginServer跪削,這才是我們今天要重點(diǎn)討論的對(duì)象谴仙。
前面探討一些服務(wù)器公共組件,那我們這里也應(yīng)該試用一下碾盐,不能只是停留在理論上晃跺。先從狀態(tài)機(jī)開(kāi)始,前面也說(shuō)過(guò)了毫玖,登錄服上的連接會(huì)有兩種狀態(tài)掀虎,一是帳號(hào)密碼驗(yàn)證狀態(tài)凌盯,一是服務(wù)器列表選擇狀態(tài),其實(shí)還有另外一個(gè)狀態(tài)我們未曾討論過(guò)烹玉,因?yàn)樗c我們的登錄過(guò)程并無(wú)多大關(guān)系驰怎,這就是升級(jí)包發(fā)送狀態(tài)。三個(gè)狀態(tài)的轉(zhuǎn)換流程大致為:
LogonState -- 驗(yàn)證成功 -- 版本檢查 -- 版本低于最新值 -- 轉(zhuǎn)到UpdateState
|
-- 版本等于最新值 -- 轉(zhuǎn)到WorldState
這個(gè)版本檢查的和決定下一個(gè)狀態(tài)的過(guò)程是在LogonState中進(jìn)行的二打,下一個(gè)狀態(tài)的選擇是由當(dāng)前狀態(tài)來(lái)決定县忌。密碼驗(yàn)證的過(guò)程使用了SRP6協(xié)議,具體過(guò)程就不多做描述继效,每個(gè)游戲使用的方式也都不大一樣症杏。而版本檢查的過(guò)程就更無(wú)值得探討的東西,一個(gè)if-else即可瑞信。
升級(jí)狀態(tài)其實(shí)就是文件傳輸過(guò)程厉颤,文件發(fā)送完畢后通知客戶端開(kāi)始執(zhí)行升級(jí)文件并關(guān)閉連接。世界選擇狀態(tài)則提供了一個(gè)列表給客戶端凡简,其中包括了所有游戲世界網(wǎng)關(guān)服務(wù)器的IP逼友、PORT和當(dāng)前負(fù)載情況。如果客戶端一直連接著潘鲫,則該狀態(tài)會(huì)以每5秒一次的頻率不停刷新列表給客戶端翁逞,當(dāng)然是否值得這樣做還是有待商榷肋杖。
整個(gè)過(guò)程似乎都沒(méi)有值得探討的內(nèi)容溉仑,但是,還沒(méi)有完状植。當(dāng)客戶端選擇了一個(gè)世界之后該怎么辦浊竟?wow的做法是,當(dāng)客戶端選擇一個(gè)游戲世界時(shí)津畸,客戶端
會(huì)主動(dòng)去連接該世界服的IP和PORT振定,然后進(jìn)入這個(gè)游戲世界。與此同時(shí)肉拓,與登錄服的連接還沒(méi)有斷開(kāi)后频,直到客戶端確實(shí)連接上了選定的世界服并且走完了排隊(duì)過(guò)程為止。這是一個(gè)很必要的設(shè)計(jì)暖途,保證了我們?cè)谝蛞馔馇闆r連接不上世界服或者發(fā)現(xiàn)世界服正在排隊(duì)而想換另外一個(gè)試試時(shí)不會(huì)需要重新進(jìn)行密碼驗(yàn)證卑惜。
但是我們所要關(guān)注的還不是這些,而是客戶端去連接游戲世界的網(wǎng)關(guān)服時(shí)服務(wù)器該如何識(shí)別我們驻售。打個(gè)比方露久,有個(gè)不自覺(jué)的玩家不遵守游戲規(guī)則,沒(méi)有去驗(yàn)證帳號(hào)密碼就直接跑去連接世界服了欺栗,就如同一個(gè)不自覺(jué)的乘客沒(méi)有換登機(jī)牌就直接跑到登機(jī)口一樣毫痕。這時(shí)征峦,乘務(wù)員會(huì)客氣地告訴你要先換登機(jī)牌,那登機(jī)牌又從哪來(lái)消请?檢票口換的栏笆,人家會(huì)先驗(yàn)明你的身份,確認(rèn)后才會(huì)發(fā)給你登機(jī)牌梯啤。一樣的處理過(guò)程竖伯,我們的登錄服在驗(yàn)明客戶端身份后,也會(huì)發(fā)給客戶端一個(gè)登機(jī)牌因宇,這個(gè)登機(jī)牌還有一個(gè)學(xué)名七婴,叫做session key。
客戶端拿著這個(gè)session key去世界服網(wǎng)關(guān)處就可正確登錄了嗎察滑?似乎還是有個(gè)疑問(wèn)打厘,他怎么知道我這個(gè)key是不是造假的?沒(méi)辦法贺辰,中國(guó)的假貨太多户盯,我們不得不到處都考慮假貨的問(wèn)題。方法很簡(jiǎn)單饲化,去找給他登機(jī)牌的那個(gè)檢票員問(wèn)一下莽鸭,這張牌是不是他發(fā)的不就得了〕钥浚可是硫眨,那么多的LoginServer,要一個(gè)個(gè)問(wèn)下來(lái)巢块,這效率也太低了礁阁,后面排的長(zhǎng)隊(duì)一定會(huì)開(kāi)始叫喚了。那么族奢,LoginServer將這個(gè)key存到數(shù)據(jù)庫(kù)中姥闭,讓網(wǎng)關(guān)服自己去數(shù)據(jù)庫(kù)驗(yàn)證?似乎也是個(gè)可行的方案越走。
如果覺(jué)得這樣給數(shù)據(jù)庫(kù)帶來(lái)了太大的壓力的話棚品,也可以考慮類(lèi)似WorldServerMgr的做法,用一個(gè)臨時(shí)的列表來(lái)保存廊敌,甚至可以將這個(gè)列表就保存到WorldServerMgr上铜跑,他正好是全區(qū)唯一的。這兩種方案的本質(zhì)并無(wú)差別庭敦,只是看你愿意將負(fù)載放在哪里疼进。而不管在哪里,這個(gè)查詢(xún)的壓力都是有點(diǎn)大的秧廉,想想伞广,全區(qū)所有玩家呢拣帽。所以,我們也可以試著考慮一種新的方案嚼锄,一種不需要去全區(qū)唯一一個(gè)入口查詢(xún)的方案减拭。
那我們將這些session key分開(kāi)存儲(chǔ)不就得了。一個(gè)可行的方案是区丑,讓任意時(shí)刻只有一個(gè)地方保存一個(gè)客戶端的session key拧粪,這個(gè)地方可能是客戶端當(dāng)前正連接著的服務(wù)器,也可以是它正要去連接的服務(wù)器沧侥。讓我們來(lái)詳細(xì)描述一下這個(gè)過(guò)程可霎,客戶端在LoginServer上驗(yàn)證通過(guò)時(shí),LoginServer為其生成了本次會(huì)話的session key宴杀,但只是保存在當(dāng)前的LoginServer上癣朗,不會(huì)存數(shù)據(jù)庫(kù),也不會(huì)發(fā)送給WorldServerMgr旺罢。如果客戶端這時(shí)想要去某個(gè)游戲世界旷余,那么他必須先通知當(dāng)前連接的LoginServer要去的服務(wù)器地址,LoginServer將session key安全轉(zhuǎn)移給目標(biāo)服務(wù)器扁达,轉(zhuǎn)移的意思是要確保目標(biāo)服務(wù)器收到了session key正卧,本地保存的要?jiǎng)h除掉。轉(zhuǎn)移成功后LoginServer通知客戶端再去連接目標(biāo)服務(wù)器跪解,這時(shí)目標(biāo)服務(wù)器在驗(yàn)證session key合法性的時(shí)候就不需要去別處查詢(xún)了炉旷,只在本地保存的session key列表中查詢(xún)即可。
當(dāng)然了惠遏,為了session key的安全砾跃,所有的服務(wù)器在收到一個(gè)新的session key后都會(huì)為其設(shè)一個(gè)有效期骏啰,在有效期過(guò)后還沒(méi)來(lái)認(rèn)證的节吮,則該session key會(huì)被自動(dòng)刪除。同時(shí)判耕,所有服務(wù)器上的session key在連接關(guān)閉后一定會(huì)被刪除透绩,保證一個(gè)session key真正只為一次連接會(huì)話服務(wù)。
但是壁熄,很顯然的帚豪,wow并沒(méi)有采用這種方案,因?yàn)榭蛻舳嗽谶x擇世界服時(shí)并沒(méi)有向服務(wù)器發(fā)送要求確認(rèn)的消息草丧。wow中的session key應(yīng)該是保存在一個(gè)類(lèi)似于WorldServerMgr的地方狸臣,或者如mangos一樣,就是保存在了數(shù)據(jù)庫(kù)中昌执。不管是怎樣一種方式烛亦,了解了其過(guò)程诈泼,代碼實(shí)現(xiàn)都是比較簡(jiǎn)單的,我們就不再贅述了煤禽。