BlockOS開發(fā)日志(五)多人聯(lián)機設計
要做中期答辯了,學校要求得有功能實現(xiàn)出來。我本身報的畢設項目其實是它的服務端,所以也不得不先把視角從前端轉到后端這塊來,實現(xiàn)一個能跑的起來的服務器了。
其實我對游戲服務器這一塊一竅不通,所有的思路均來自各種開源項目的捕風捉影和自己的想象,如果有幸有大佬刷到這篇,還希望能指點一二。
對整個通信流程的推理分析
客戶機與服務器通信的過程,我認為可以先從現(xiàn)實出發(fā)。想象一場特殊的棋局:黑方和白方分別在兩個獨立的房間里,不得出房間。但是裁判可以出入雙方的房間,把話帶給對方。那么要怎么去完成這個棋局呢?
嗯...先設立一個具體情境吧。假定我是棋手之一,想要和小明下棋,那么我的第一句話是:“裁判,我要和小明下棋!”那么裁判聽到后,就從走廊趕過來:“好嘞!”。那萬一裁判沒聽見或者忙忘了怎么辦呢?那當然是再催一下:“搞快點,我要和小明下棋!”,直到裁判過來為止?!耙托∶飨缕迨前桑?。”裁判說著便記下了這個請求。但是裁判也不能就這么走了,如果是認識我是誰還好說,不然跑到小明房間才想起來沒問我是誰,白跑一趟不是。“那請問下你是誰?我到時候方便告訴他你要找他下?!辈门袉?。“叫我小澤就好?!蔽一卮?。裁判見狀,便回復道:“行,小澤要和小明下棋,我這就去轉告小明?!辈门姓业叫∶?,問道:“小澤想找你下棋,下不下?”小明答:“好啊?!比缓蟛门斜慊貋砀嬖V我:“小明同意了,可以開始了?!敝链耍粓銎寰直闼闶情_始了。
等等,那要是小明沒來呢?那應該是這樣的情況:裁判跑遍了所有房間,一個個問:“你是不是小明?”答曰:“不是?!敝钡阶詈笠粋€房間。最后只好回來和我說:“不好意思,小明沒來呢?!边@樣好像有點累哦,應該在房間門口放塊黑板。每個人進房間的時候就把自己名字寫到黑板上,離開房間的時候就把自己的名字擦掉。這樣裁判只要看一眼房間前面的黑版就知道誰在房間了,能省不少力氣。
還有還有,那要是我只是想隨便找個人下棋呢?那么情境應該就變成了這樣。依舊是我先叫裁判過來,告訴他:“裁判我要下棋!”這時候裁判應該有一個登記本,用來記錄想下棋的人。如果本子上還沒記任何人,那么裁判這時候應該說:“好嘞。我登記一下,等有人想下棋的時候我第一時間通知你。”誒,這時候就多虧了黑板,在進來之前裁判就知道我是誰了,于是裁判就在登記本上寫到:“小澤,渴望下棋?!蹦侨绻巧厦嬉呀?jīng)登記上了名字呢?裁判就會說:“來得正好,小明在等著下棋呢?!比缓箢┮谎酆诎迳蠈懙淖?,轉頭跑到小明的房間:“來了來了,小澤要跟你下棋呢?!苯又门惺盏诫p方的同意后,對局就開始了...嗎?!萬一這會小明耐不住性子走了呢?這時候裁判又只好回來說:“不好意思,小明剛不耐煩走了,我給你記一下,到時候有人來了通知你?!?/p>
最后,從裁判視角來看,還有一種情況。有人進入了房間,但是他不一定是來下棋的,有可能只是誤入房間,這里裁判也可以用兩種辦法處理。一是到房間里去問:“請問你是來下棋的嗎,如果是的話麻煩在黑板上寫一下稱呼?!倍侵苯釉跇峭鈱懨饕?guī)矩:“來下棋者請在房間前的黑板上寫好名字,未寫名字者將被請出棋館?!?/p>
那么至此建立一場棋局就完成了。那要怎么下棋呢?還是有兩種方法。一種是裁判可以自己準備一個棋盤,每當一方下完一步之后,就告訴裁判,然后裁判往自己的棋盤上下好棋子。按照規(guī)則處理完成后(如落子,提子等)把棋盤的新變化告訴雙方。另一種方法是裁判往兩個人的房間來回跑,一方下好后,跑過去另一方那里告訴他,在這里下好了棋子,然后雙方都按規(guī)則做出自己的判斷,更新棋子??梢钥闯鰜韮煞N方式各有優(yōu)劣,前者的好處是不用時時刻刻費心去維護棋盤的狀態(tài),但是裁判需要花更多心思;后者的好處是可以少用一個棋盤,裁判也不需要思考可以直接往兩邊遞話,但是如果一方出錯了,裁判需要一個個去對問題出在什么地方。后者在判斷特殊事件(如判斷勝負)的時候也不方便,一個人的棋盤不一定是正確的棋盤,裁判需要綜合雙方的棋盤判斷無誤后才可做出決策,前者相對來說就要容易得多,但是相對的,裁判要說更多的話,干更多的事,可能忙不過來。圖個方便,還是讓裁判多操心一下吧。

下面就來分析怎么把這些流程轉譯到服務器。在這里服務器的角色對應的是裁判的角色。首先,棋手進入房間。這里的房間和棋手無關,只有裁判負責管理,每個棋手只和自己的房間相關。在這個瞬間,棋館當局只知道有人進入了一個具體的房間,但是在他把名字寫到黑板上之前并不知道他是誰。
這個過程放到計算機網(wǎng)絡里最像的應該是TCP的accept過程,服務器知道某一個IP地址的客戶端加入了連接,但是也僅此而已。如果服務器看一個TCP連接長時間得不到想要的回應(比如提供用戶名),那么就應該從服務器端主動斷開連接。
但這僅僅只是TCP,而我的目標應該是建立一個統(tǒng)一的通信接口,不論是TCP還是UDP還是任何別的線程或進程之間的通信方式,只要實現(xiàn)了這個接口,就都應該被服務器所識別,使用到。
提煉一下上面的關鍵詞:連接。也許我需要一個IConnection接口來描述一次連接,或者說故事里的房間。首先它應該有一個HasMessage和一個GetMessage方法。因為在這個棋館里不止一對棋局需要處理,可能在一個瞬間有很多房間都提出了要求,但是裁判只能同時處理一件事。因此裁判需要把處理這件事的時,其他房間提出的新要求寫入待辦。以后空閑時就用HasMessage方法查詢待辦是否還有未處理的要求,如果有,就把這個要求從待辦中劃掉,去處理。然后還需要一個SendMessage方法。因為裁判需要跑到房間去把消息告訴棋手。最后它還應該有一個Disconnect方法和一個IsDisconnected,用于把誤入的路人請出去或者判斷棋手是否有事離開了。
嗯,對于一個很多人的棋館,只有一個裁判還是有點忙不過來,還是請個房間管理員吧,就叫ConnectionManager好了。這樣裁判就只要負責把消息告訴管理員,然后管理員按照房間名字去干上面的事情。管理員每做一個來回,還要負責記錄房間們提出的新要求,到時候裁判還要通過PollMessage方法把這些要求取出來,然后裁判根據(jù)管理員帶來的新要求,做出更新,然后把新的任務寫進任務清單,然后用ProcessTasks方法告訴管理員去處理??偹爿p松一些了,裁判只要坐在裁判室負責維護各個對局的棋盤或者建立棋盤的任務,別的都交給房間管理員去處理。
房間管理員要怎么做呢?房間管理員首先應該要知道各個房間要怎么走,然后要知道誰在哪個房間,因為裁判只知道要把消息按名字派給誰,但是誰在哪個房間,只有管理員知道。所以管理員內(nèi)部應該維護一個映射表Map<string,IConnection>,用于記錄誰在哪個房間。啊,這時候原來需要裁判干的將路人請出房間的事情就可以交給管理員來做,裁判只需要知道來下棋的客人就好了。多一個人還是省了不少心思啊。哦,還少了一步,管理員還要把來真正來下棋的人的消息交給裁判,那么IConnection提供的信息有點少,就把房間進化為User吧,User除了提供IConnection的方法外,還有一個自己的名字,這樣管理員接到消息后,順便把消息來源給貼上,這樣裁判就知道這一趟的消息來自于誰了。
而裁判呢,只需要看著各個棋盤,消息清單來了,就依次按消息清單進行處理,如果是建立棋盤相關的消息,就看進行到哪一步了,把要下一步發(fā)給誰寫入任務清單。如果是對局相關的消息,就把操作加入到棋盤,然后把更新后的棋盤信息和雙方也寫入任務清單。等下一趟房間管理員來的時候把清單交給他處理就好。
還有嗎?
還可以更進一步,比如管理員還可以請更多的下屬讓他們對這些房間分片區(qū)管理。而裁判那邊呢,一樣也可以請至少兩個人,一個專門負責對局建立,一個專門負責棋盤更新,裁判收到消息后,根據(jù)消息的種類把任務再派給這兩個人,等他們完成后,把任務匯總交給房間管理員處理。對應到計算機上呢,就是開更多線程負責處理這些事,然后由房間管理員線程和裁判線程來進行匯總。但是這中間又有不少協(xié)調(diào)工作(線程同步)要做。由于時間問題就留待后面優(yōu)化了。
總結
首先還是再說明一下這方面的資料確實太少了,我只能從現(xiàn)實出發(fā)來腦洞實現(xiàn)方式,大概率不是最優(yōu)解。還請各位指出我的不足之處。
然后就是一些根據(jù)這個畫出的UML圖。

一些根據(jù)上文寫出來的偽碼。