棋牌游戲服務器內(nèi)部架構(gòu)基本功能解析和需要注意的東西
-
數(shù)據(jù)共享如何實現(xiàn)
由于棋牌類不分區(qū)不分服,一般都會按世界服的思路來設計,也就是說,服務器是一個由多臺物理機組成的集群。當用戶登錄服務器后創(chuàng)建房間時,會根據(jù)負載均衡算法分布到任何一臺服務器上。所以,不管用戶登錄的是哪一臺服務器都是可以獲取玩家的數(shù)據(jù),一般都會采用Redis來做數(shù)據(jù)共享。
-
如何進入房間
在同一局游戲中,會要求所有人都在同一個房間中,可以規(guī)定在同一間房間中的用戶必須登錄到同一臺物理機上。在創(chuàng)建房間完成之后,其他人根據(jù)房間號查找房間的時候,可以根據(jù)房間號換取房間所在服務器的IP和端口,判斷當前登錄用戶的服務器的IP的端口是否和所在服務器是否一致,若相同則不做切換,若不同則需連接到房間所在的服務器上。
-
如何保證房間操作的順序
創(chuàng)建房間成功之后接下來的操作都要保證它的順序性,所以房間需要有一個自己的消息隊列,可以把每個房間到達服務器的消息封裝為一個任務,將這個任務放到消息隊列中,然后有一個任務執(zhí)行者去按順序執(zhí)行這些任務。
-
登錄
登錄都需要接入第三方,所以這塊是HTTP操作,需統(tǒng)一提供一個Web服務,用來做登錄驗證。因為在登錄時調(diào)用第三方的HTTP服務的過程會很慢。如果放在邏輯服去做的話可能會卡住業(yè)務邏輯任務。因為不同的玩家業(yè)務請求可能在一個線程中,如果有任務卡那么這個任務之后新來的請求都會被卡住,進而會導致消息延遲。
-
公告通知
公告一般會在登錄時向服務器獲取一次,將它放在Web服務器與業(yè)務邏輯分離,當業(yè)務邏輯服維護或更新時不會影響用戶的登錄。
對于世界消息通知,統(tǒng)一由網(wǎng)關處理并發(fā)送給本網(wǎng)關中所有的用戶。對于管理后臺發(fā)送過來的通知,如玩家充值、自動廣播等,可直接發(fā)送到網(wǎng)關提供的HTTP服務接口,由網(wǎng)管統(tǒng)一發(fā)送給網(wǎng)管中的所有用戶。對于游戲內(nèi)的通知,如結(jié)算通知等,可直接使用游戲中的通知模塊處理之后,發(fā)送到網(wǎng)管在發(fā)送給各個客戶端。
-
用戶ID
由于棋牌類游戲是世界服無分區(qū),所以用戶的ID必須是全局唯一的,可以利用Redis的incr()
方法原子的遞增,如果不想被別人根據(jù)遞增的ID推算出有多少注冊用戶,遞增的梯度可以隨機,比如每次遞增的值都是從1到1024中隨機的一個。
-
創(chuàng)建房間
當房主房間房間時,房間ID需要在任何一臺服務器都可以查詢到,所以創(chuàng)建房間成功后,房間ID要存儲在貢獻內(nèi)容Redis中,每個房間ID對應一個房間所在的IP或服務器ID。當有用戶進入房間查詢房間ID時,可以判斷房間是否和自己登陸的游戲服是否相同。
-
查找并加入房間
根據(jù)房間ID查找到房間后,獲取房間所在服務器的ID或IP,如果和所登錄的服務器相同,就可以直接加入房間。如果不一樣,則要把房間所在服務器的ID或IP返回給客戶端,讓客戶端重新與房間所在服務器建立連接,并使用登錄時的token驗證用戶。
-
游戲腳本調(diào)用
在驗證游戲是否合法時,客戶端和服務器都需要驗證,驗證的算法是一樣的,使用Lua腳本編寫一份腳本,在服務器和客戶端都可以同時使用。同一個算法使用同一個腳本,在開發(fā)新的同類游戲時,只需要替換下腳本,也就不用再重復開發(fā)。
-
后臺管理系統(tǒng)
棋牌類后臺管理系統(tǒng)會根據(jù)運營需求開發(fā),需求不一。不過后臺管理系統(tǒng)是要和游戲服務器通信的,這種通信方式最好是采用Redis的訂閱發(fā)布機制,這樣可以把某個消息事件同時發(fā)送到所有業(yè)務服務器上,并根據(jù)用戶所在服務器進行處理。
-
玩家同屏
玩家同屏是棋牌類游戲的一個重點,對應大型ARPG或MMO游戲來說,這并不是什么難事兒。因為同屏就是服務器對客戶端的消息進行轉(zhuǎn)發(fā)。由于棋牌游戲同步數(shù)據(jù)量比較小,常見的同步方式有兩種:
-
客戶端主動拉取
客戶端定時主動向服務器請求一個用戶的消息隊列,當一個玩家有操作需要同步到其他玩家時,在服務器端先把消息放到用戶的消息隊列中,等待客戶端的拉去操作。這種做法的好處是不需要考慮網(wǎng)絡閃斷或弱網(wǎng)環(huán)境,消息都是同步獲取的。缺點是定時拉取的時間間隔很短,可能會不到一秒就要拉取一次。
-
服務器主動推送
當一個玩家出牌的消息需要同步給其他玩家時,服務器會獲得這個玩家和服務器建立的socket連接,然后服務器使用socket主動向客戶端發(fā)送消息。這種方式要考慮網(wǎng)絡閃斷造成消息丟失的問題。因為服務器推送的消息客戶端可能會收不到,所以客戶端需要根據(jù)心跳來判斷網(wǎng)絡是否斷開連接,如果斷開就需要重新從服務器拉取整個房間狀態(tài),或者是根據(jù)服務器發(fā)送的消息號,當客戶端接收到的服務器消息號有跳號時,比如應該收到10卻收到20說明中間有消息丟失,這個時候需要重新拉整個房間的狀態(tài)信息。這種做法的缺點是開發(fā)復雜,需要考慮網(wǎng)絡問題。優(yōu)點是只有在有消息時才會推送,沒有的話不推送,不占用帶寬等網(wǎng)絡資源,可以增加用戶同時在線數(shù)量,也就增加了服務器的承載量。
-
共享內(nèi)存
共享內(nèi)存在系統(tǒng)中的地位看上去很像是數(shù)據(jù)庫前端的緩存,和《天龍八部》的ShareMemory類似,sharedb也采用定長的數(shù)據(jù)結(jié)構(gòu),通過共享內(nèi)存來實現(xiàn)進程間的數(shù)據(jù)共享。sharedb的存在使得游戲邏輯處理和數(shù)據(jù)保存邏輯得到很好的隔離,游戲邏輯不必關心后端的數(shù)據(jù)是如何保存的,只要sharedb掛上定期存盤的服務,在接口定義明確的情況下,后端到底采用什么樣的數(shù)據(jù)庫變得不是那么重要,從而降低了系統(tǒng)的耦合度。
-
數(shù)據(jù)同步和持久化
由于棋牌類游戲數(shù)據(jù)量少,計算量也很小,所以完全可以不使用內(nèi)存緩存而直接使用Redis做共享內(nèi)存,用戶的所有數(shù)據(jù)都緩存到Redis中,更新也同步更新到Redis中,這樣不管一個用戶登錄哪一臺業(yè)務服務器都能獲得自己的最新數(shù)據(jù)。
在更新數(shù)據(jù)庫時由于數(shù)據(jù)第一緩存是Redis,所以活躍的用戶數(shù)據(jù)都可以從Redis中直接獲得,而不用查詢數(shù)據(jù)庫,所以數(shù)據(jù)庫的更新可以采取異步更新,而不會產(chǎn)生數(shù)據(jù)延遲。但需要注意的是數(shù)據(jù)的異步更新必須是有序性的,那么這就產(chǎn)生了一個問題,如何保證用戶的更新不會亂呢?
因為業(yè)務服務器是由多個的,用戶可能連接到其中的任意一臺,如果說登錄的是服務器A加入的房間時服務器B,那么連接就會切換,為了保證數(shù)據(jù)更新的順序,可以做一個數(shù)據(jù)持久化服務,將需要更新數(shù)據(jù)庫的任務實時發(fā)送到這臺服務器上,由數(shù)據(jù)庫持久化服務執(zhí)行對數(shù)據(jù)庫的更新,這樣不管用戶連接到那臺業(yè)務服務器它的更新都是由順序的。
由于棋牌類的業(yè)務少數(shù)據(jù)更新少,所以查詢可以有Redis緩存以減少數(shù)據(jù)庫查詢的壓力,而更新實行實時更新到數(shù)據(jù)庫,前期可以不需要開發(fā)數(shù)據(jù)持久化服務,等用戶積累到一定數(shù)量后,發(fā)現(xiàn)更新數(shù)據(jù)庫比較緩慢的時候再單獨做一個數(shù)據(jù)庫持久化服務。
Redis負責緩存,由于緩存的數(shù)據(jù)是最新的,通常會比數(shù)據(jù)庫中的要新。例如在玩家登錄游戲后,玩家所有的數(shù)據(jù)都以Redis中為準,Redis中的數(shù)據(jù)會定時同步到數(shù)據(jù)庫中,同步的有游戲服務器中的模塊同步,另外一般5分鐘同步一次所有變更的玩家數(shù)據(jù)。如果Redis啟動了RDB快照,則會自動定時同步數(shù)據(jù)到磁盤。
-
存儲方案