使用GoWorld游戲服務器引擎輕松實現分布式聊天服務器


GoWorld游戲服務器引擎簡介

GoWorld是一款開源的分布式可擴展的游戲服務器引擎,使用Go語言(Golang)編寫。它采用類似BigWorld的結構,使用了簡化的場景-對象框架。以一個典型的MMORPG為例,每個服務器上會有多個場景,每個場景里可以包含多個對象,這些對象包括玩家、NPC、怪物等。GoWorld服務器可以將場景分配到在不同的進程甚至不同的機器上,從而使得游戲服務器的負載是可擴展的。

開源分布式游戲服務器引擎:https://github.com/xiaonanln/goworld,歡迎賞星,共同學習 也可在GitHub Wiki查看本文

聊天室是游戲里非常常見的一個功能,例如一個MMORPG游戲里會有世界聊天室、職業聊天室、幫派聊天室等。GoWorld為此提供了非常簡單且高效率的支持,使得開發者可以輕松實現分布式的聊天室功能。這里我們就是嘗試使用GoWorld所提供的功能實現一個分布式可擴展的聊天服務器。

聊天室功能說明

我們要實現的聊天室包含以下的一些功能:

  • 注冊
  • 登錄
  • 說話
  • 切換聊天室

為了面向更廣大的游戲開發者,我們使用Cocos Creater 1.5.2開發聊天室客戶端,客戶端編程語言使用Javascript。 由於這僅僅只是一個Demo,因此服務端和客戶端的功能都相對簡單,但是依然用到了GoWorld的一些非常卓越的特性。

使用GoWorld開發分布式聊天服務器

安裝Go 1.8.3

編譯GoWorld需要安裝Golang 1.8.3,請確保自己的Go的版本足夠新。

獲得GoWorld開源游戲服務器

運行以下的命令獲得GoWorld游戲服務器引擎。由於GoWorld依賴其他較多的外部庫,因此這個過程可能需要花費一點時間。

go get -u github.com/xiaonanln/goworld

編寫聊天服務端代碼

我們在%GOPATH%/src/github.com/xiaonanln/goworld/examples/chatroom_demo目錄中開發聊天室服務端。可以通過地址 https://github.com/xiaonanln/goworld/tree/master/examples/chatroom_demo 查看所有的服務端代碼。

聊天服務器主函數

使用GoWorld開發服務端需要開發者提供main函數入口。main函數一般會注冊幾個自定義的類型,然后調用goworld.Run進入游戲服務端主循環。

// serverDelegate 定義一些游戲服務器的回調函數(必須提供) type serverDelegate struct { game.GameDelegate } func main() { goworld.RegisterSpace(&MySpace{}) // 注冊自定義的Space類型(必須提供) // 注冊Account類型 goworld.RegisterEntity("Account", &Account{}, false, false) // 注冊Avatar類型,並定義屬性 goworld.RegisterEntity("Avatar", &Avatar{}, true, true).DefineAttrs(map[string][]string{ "name": {"Client", "Persistent"}, "chatroom": {"Client"}, }) // 運行游戲服務器 goworld.Run(&serverDelegate{}) }

如上所示,main函數的邏輯非常簡單。首先注冊一個自定義的場景對象類型MySpace,這個是GoWorld強制要求的,否則運行會報錯。然后main注冊了兩個實現聊天服務器邏輯的對象類型(Account和Avatar)。Account負責注冊和登錄流程,Avatar負責玩家聊天邏輯。這兩個類型的具體實現在下文繼續詳述。最后main調用goworld.Run運行游戲服務器的主循環。

自定義場景類型MySpace

GoWorld引擎要求我們必須在main里注冊一個自定義的場景類型。由於聊天服務器沒有任何場景邏輯,因此這個類型也沒有任何具體的代碼實現。場景可以幫助實現MMORPG游戲、或者開房間類型的游戲,但是對於一個簡單的聊天服務器來說並沒有什么作用。因此我們只定義並注冊一個空的場景類型即可。

// MySpace 是一個自定義的場景類型 // // 由於聊天服務器沒有任何場景邏輯,因此這個類型也沒有任何具體的代碼實現 type MySpace struct { entity.Space // 自定義的場景類型必須繼承一個引擎所提供的entity.Space類型 }

賬號對象類型Account

賬號類型定義如下。所有的自定義對象類型都必須繼承entity.Entity。當有玩家客戶端連接服務器的時候,服務器就會自動創建一個Account對象,並且將新的客戶端作為Account對象的客戶端。在GoWorld引擎中,每個對象都可以有最多一個客戶端對象。這樣,服務端對象就可以通過RPC調用客戶端對象的一些函數,並通過屬性機制更新客戶端對象的一些屬性。

// Account 是賬號對象類型,用於處理注冊、登錄邏輯。 type Account struct { entity.Entity // 自定義對象類型必須繼承entity.Entity logining bool }

賬號注冊

當客戶端點擊注冊的時候,就會給服務端發送一個注冊的RPC請求。Account對象需要定義一個函數(Register_Client)來接受這個RPC請求,如下所示。函數名末尾的_Client代表這是一個可以由客戶端調用的RPC函數。Register_Client函數使GoWorld引擎提供的方便的KVDB模塊進行賬號-密碼數據的存儲和讀取。當賬號密碼不存在的時候,就在KVDB中插入新的賬號和密碼。注冊過程在創建新賬號的同時,創建一個Avatar對象,然后立刻銷毀。這是為了在數據庫中生成新的Avatar對象的數據,並獲得其唯一的ID(avatarID)並將Avatar的ID也存入到KVDB中,和這個賬號進行綁定。

func (a *Account) Register_Client(username string, password string) { goworld.GetKVDB("password$"+username, func(val string, err error) { if val != "" { a.CallClient("ShowError", "這個賬號已經存在") return } goworld.PutKVDB("password$"+username, password, func(err error) { avatarID := goworld.CreateEntityLocally("Avatar") // 創建一個Avatar對象然后立刻銷毀,產生一次存盤 avatar := goworld.GetEntity(avatarID) avatar.Attrs.Set("name", username) avatar.Destroy() goworld.PutKVDB("avatarID$"+username, string(avatarID), func(err error) { a.CallClient("ShowInfo", "注冊成功,請點擊登錄") }) }) }) }

賬號登錄

Account對象使用Login_Client處理來自客戶端的登錄請求,如下所示。 首先,從KVDB中獲得正確的賬號和密碼並和玩家所提供的密碼進行比較。如果密碼正確,我們再次使用KVDB獲得賬號所對應的Avatar ID,並使用這個Avatar ID開始從數據庫里載入Avatar對象。

func (a *Account) Login_Client(username string, password string) { goworld.GetKVDB("password$"+username, func(correctPassword string, err error) { if correctPassword == "" { a.CallClient("ShowError", "賬號不存在") return } if password != correctPassword { a.CallClient("ShowError", "密碼錯誤") return } goworld.GetKVDB("avatarID$"+username, func(_avatarID string, err error) { avatarID := common.EntityID(_avatarID) goworld.LoadEntityAnywhere("Avatar", avatarID) a.Call(avatarID, "GetSpaceID", a.ID) }) }) }

這里我們使用goworld.LoadEntityAnywhere函數載入Avatar對象。在一個分布式服務器中,Avatar對象可能在任意一個服務端邏輯進程中創建。因此在這種情況下,Account向剛載入的Avatar對象發起一次GetSpaceID請求,試圖獲得Avatar對象所在的場景。Avatar對象需要定義GetSpaceID函數來處理請求,並把自己所在的場景ID發送給Account對象,代碼如下所示。和上面的_Client結尾函數不同的是,這里的RPC調用者和接受者都是服務端的對象,因此不需要提供_Client標記。

func (a *Avatar) GetSpaceID(callerID EntityID) { a.Call(callerID, "OnGetAvatarSpaceID", a.ID, a.Space.ID) }

Account對象在收到OnGetAvatarSpaceID回調之后,可以通過EnterSpace請求讓自己遷移到Avatar對象所在的進程,代碼如下所示。場景切換是GoWorld所提供的強大的對象操作功能,它使得服務端的對象可以在各個場景里方便的切換,大幅度簡化了開發者實現分布式服務的開發難度。

func (a *Account) OnGetAvatarSpaceID(avatarID common.EntityID, spaceID common.EntityID) { // 如果發現Avatar對象和Account對象在同一個服務器,則不需要進行場景切換 avatar := goworld.GetEntity(avatarID) if avatar != nil { a.onAvatarEntityFound(avatar) return } a.Attrs.Set("loginAvatarID", avatarID) a.EnterSpace(spaceID, entity.Position{}) }

Account對象在切換場景結束之后,再次在當前邏輯進程里尋找指定的Avatar對象。然后調用onAvatarEntityFound函數完成最后的登錄邏輯,也就是通過GiveClientTo函數把Account當前的客戶端連接移交給Avatar對象,然后Account對象因為失去客戶端而被銷毀。

func (a *Account) OnMigrateIn() { loginAvatarID := common.EntityID(a.Attrs.GetStr("loginAvatarID")) avatar := goworld.GetEntity(loginAvatarID) if avatar != nil { a.onAvatarEntityFound(avatar) } else { // failed a.CallClient("ShowError", "登錄失敗,請重試") a.logining = false } } func (a *Account) onAvatarEntityFound(avatar *entity.Entity) { a.GiveClientTo(avatar) } // OnClientDisconnected 會在對象失去客戶端的時候被調用 func (a *Account) OnClientDisconnected() { a.Destroy() }

Account對象在載入Avatar對象並完成登錄的過程似乎有些復雜,涉及到Avatar對象載入,兩次RPC調用以及一次對象遷移。不過GoWorld所提供的機制使得我們可以方便地將Avatar對象創建到各個不同的服務器進程中。

Avatar對象邏輯

Avatar對象代表一名已經登錄的聊天室玩家。和上述的Account對象一樣,我們首先需要定義一個Avatar類型。

定義與初始化

// Avatar 對象代表一名玩家 type Avatar struct { entity.Entity } // OnCreated 函數會在對象創建結束的時候調用 func (a *Avatar) OnCreated() { a.Entity.OnCreated() a.setDefaultAttrs() } func (a *Avatar) setDefaultAttrs() { a.Attrs.Set("chatroom", "1") a.SetFilterProp("chatroom", "1") }

當Avatar對象載入成功之后,我們會為它設置默認的聊天室。

通過使用a.Attrs.Set將Avatar對象的chatroom屬性設置為1。屬性機制是GoWorld所提供的一種存儲對象信息,並提供對象數據自動存盤、自動同步到客戶端的機制。因此服務端對象在設置chatroom屬性的同時,客戶端也會收到這個屬性的更新,並同步到UI界面上。

然后Avatar對象使用SetFilterProp函數設置自己的一個filter屬性:chatroom = 1。Filter屬性機制是GoWorld為了高效率地實現游戲里各種聊天室所提供的客戶端過濾和通知機制。服務端可以使用Filter屬性機制向所有滿足filter屬性要求的對象的客戶端發起廣播,這樣的效率要遠遠優於一個個對一個對象進行掃描並發送客戶端RPC。

說話和切換聊天室

Avatar對象提供SendChat_Client函數來處理來自客戶端的說話請求,如下所示。

func (a *Avatar) SendChat_Client(text string) { text = strings.TrimSpace(text) if text[0] == '/' { // this is a command cmd := spaceSep.Split(text[1:], -1) if cmd[0] == "join" { a.enterRoom(cmd[1]) } else { a.CallClient("ShowError", "無法識別的命令:"+cmd[0]) } } else { a.CallFitleredClients("chatroom", a.GetStr("chatroom"), "OnRecvChat", a.GetStr("name"), text) } }

SendChat_Client把以/開頭的內容當作一個命令,並進行特殊處理。其他內容則作為普通的說話內容,並通過調用引擎Filter屬性機制所提供的CallFitleredClients函數將說話人的名字和內容都發送到所有在當前聊天室的玩家客戶端。

如果玩家發了一個/join ...的命令,則會被看成一個切換聊天室的請求。切換聊天室的邏輯非常簡單,只需要將聊天室名字設置為新的Filter屬性值,並設置為玩家屬性從而更新到客戶端即可。

func (a *Avatar) enterRoom(name string) {
	a.SetFilterProp("chatroom", name)
	a.Attrs.Set("chatroom", name)
}

聊天室客戶端

聊天室客戶端的代碼都在:https://github.com/xiaonanln/goworld-chatroom-demo-client ,由Javascript編寫。客戶端代碼除了對服務端通信協議進行解析和封裝之外,其他界面邏輯非常簡單,因此這里不再詳述。另外在http://goworldgs.com/chatclient/上也有一個可運行的客戶端和服務端實現,有興趣的可以點開查看。

編譯運行服務端

一個完整的GoWorld服務器包含三個部分:中心分發器、網關服務器和邏輯服務器。我們剛才所編寫的代碼全是邏輯服務器的代碼,中心分發器和網關服務器是固定的程序,直接編譯運行即可。

編譯中心分發器dispatcher

cd %GOPATH%/src/github.com/xiaonanln/goworld/components/dispatcher
go build

編譯網關服務器

cd %GOPATH%/src/github.com/xiaonanln/goworld/components/gate
go build

編譯chatroomdemo游戲服務器

cd %GOPATH%/src/github.com/xiaonanln/goworld/examples/chatroom_demo
go build

設置GoWorld配置文件

我們使用goworld根目錄下的goworld.ini.sample作為游戲服務器的配置文件。

cd %GOPATH%/src/github.com/xiaonanln/goworld
cp goworld.ini.sample goworld.ini

配置文件設置了KVDB所使用的數據庫類型(默認為MongoDB)、Avatar對象數據庫所使用的數據庫類型(默認為MongoDB),以及dispatcher、gate、game所使用的各種配置。如果使用MongoDB作為KVDB和對象數據庫,請另外安裝和運行MongoDB 3.x。

運行服務器

cd %GOPATH%/src/github.com/xiaonanln/goworld
nohup components/dispatcher/dispatcher & nohup components/gate/gate -gid 1 & nohup examples/chatroom_demo/chatroom_demo -gid 1 & 

運行客戶端

在Cocos Creater中設置所有Scene中的GoWorld對象的地址為localhost,端口為網關服務器(gate1)的http端口,默認15012,然后運行客戶端即可連接到本地服務器。

總結

如上所述,在使用GoWorld所提供的分布式場景-對象框架和其他功能的情況下,我們可以輕松開發出一個分布式可擴展的聊天室服務端。不過GoWorld所提供的功能更適合開發分場景、分房間的游戲類型。另外GoWorld所提供的熱更新功能對於大型的游戲服務端項目來說也是必不可少的。

開源分布式游戲服務器引擎:https://github.com/xiaonanln/goworld,歡迎賞星,共同學習

對Go語言服務端開發感興趣的朋友歡迎加入QQ討論群:662182346


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM