自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

教你從頭寫游戲服務(wù)器框架

開發(fā) 架構(gòu) 服務(wù)器
由于“越通用的代碼,就是越?jīng)]用的代碼”,所以在設(shè)計(jì)之初,我就認(rèn)為應(yīng)該使用分層的模式來構(gòu)建整個系統(tǒng)。

 [[276402]]

由于“越通用的代碼,就是越?jīng)]用的代碼”,所以在設(shè)計(jì)之初,我就認(rèn)為應(yīng)該使用分層的模式來構(gòu)建整個系統(tǒng)。按照游戲服務(wù)器的一般需求劃分,最基本的可以分為兩層:

  1. 底層基礎(chǔ)功能:包括通信、持久化等非常通用的部分,關(guān)注的是性能、易用性、擴(kuò)展性等指標(biāo)。
  2. 高層邏輯功能:包括具體的游戲邏輯,針對不同的游戲會有不同的設(shè)計(jì)。 

我希望能有一個基本完整的“底層基礎(chǔ)功能”的框架,可以被復(fù)用于多個不同的游戲。由于目標(biāo)是開發(fā)一個 適合獨(dú)立游戲開發(fā) 的游戲服務(wù)器框架。所以最基本的需求分析為:

功能性需求

  1. 并發(fā):所有的服務(wù)器程序,都會碰到這個基本的問題:如何處理并發(fā)處理。一般來說,會有多線程、異步兩種技術(shù)。多線程編程在編碼上比較符合人類的思維習(xí)慣,但帶來了“鎖”這個問題。而異步非阻塞的模型,其程序執(zhí)行的情況是比較簡單的,而且也能比較充分的利用硬件性能,但是問題是很多代碼需要以“回調(diào)”的形式編寫,對于復(fù)雜的業(yè)務(wù)邏輯來說,顯得非常繁瑣,可讀性非常差。雖然這兩種方案各有利弊,也有人結(jié)合這兩種技術(shù)希望能各取所長,但是我更傾向于基礎(chǔ)是使用異步、單線程、非阻塞的調(diào)度方式,因?yàn)檫@個方案是最清晰簡單的。為了解決“回調(diào)”的問題,我們可以在其上再添加其他的抽象層,比如協(xié)程或者添加線程池之類的技術(shù)予以改善。
  2. 通信:支持 請求響應(yīng) 模式以及 通知 模式的通信(廣播視為一種多目標(biāo)的通知)。游戲有很多登錄、買賣、打開背包之類的功能,都是明確的有請求和響應(yīng)的。而大量的聯(lián)機(jī)游戲中,多個客戶端的位置、HP 等東西都需要經(jīng)過網(wǎng)絡(luò)同步,其實(shí)就是一種“主動通知”的通信方式。
  3. 持久化:可以存取 對象 。游戲存檔的格式非常復(fù)雜,但其索引的需求往往都是根據(jù)玩家 ID 來讀寫就可以。在很多游戲主機(jī)如 PlayStation 上,以前的存檔都是可以以類似“文件”的方式存放在記憶卡里的。所以游戲持久化最基本的需求,就是一個 key-value 存取模型。當(dāng)然,游戲中還會有更復(fù)雜的持久化需求,比如排行榜、拍賣行等,這些需求應(yīng)該額外對待,不適合包含在一個最基本的通用底層中。
  4. 緩存:支持遠(yuǎn)程、分布式的對象緩存。游戲服務(wù)基本上都是“帶狀態(tài)”的服務(wù),因?yàn)橛螒蛞箜憫?yīng)延遲非??量?,基本上都需要利用服務(wù)器進(jìn)程的內(nèi)存來存放過程數(shù)據(jù)。但是游戲的數(shù)據(jù),往往是變化越快的,價值越低,比如經(jīng)驗(yàn)值、金幣、HP,而等級、裝備等變化比較慢的,價值則越高,這種特征,非常適合用一個緩存模型來處理。
  5. 協(xié)程:可以用 C++ 來編寫協(xié)程代碼,避免大量回調(diào)函數(shù)分割代碼。這個是對于異步代碼非常有用的特性,能大大提高代碼的可讀性和開發(fā)效率。特別是把很多底層涉及IO的功能,都提供了協(xié)程化 API,使用起來就會像同步的 API 一樣輕松愜意。
  6. 腳本:初步設(shè)想是支持可以用 Lua 來編寫業(yè)務(wù)邏輯。游戲需求變化是出了名快的,用腳本語言編寫業(yè)務(wù)邏輯正好能提供這方面的支持。實(shí)際上腳本在游戲行業(yè)里的使用非常廣泛。所以支持腳本,也是一個游戲服務(wù)器框架很重要的能力。
  7. 其他功能:包括定時器、服務(wù)器端的對象管理等等。這些功能很常用,所以也需要包含在框架中,但已經(jīng)有很多成熟方案,所以只要選取常見易懂的模型即可。比如對象管理,我會采用類似 Unity 的組件模型來實(shí)現(xiàn)。

非功能性需求

  1. 靈活性:支持可替換的通信協(xié)議;可替換的持久化設(shè)備(如數(shù)據(jù)庫);可替換的緩存設(shè)備(如 memcached/redis);以靜態(tài)庫和頭文件的方式發(fā)布,不對使用者代碼做過多的要求。游戲的運(yùn)營環(huán)境比較復(fù)雜,特別是在不同的項(xiàng)目之間,可能會使用不同的數(shù)據(jù)庫、不同的通信協(xié)議。但是游戲本身業(yè)務(wù)邏輯很多都是基于對象模型去設(shè)計(jì)的,所以應(yīng)該有一層能夠基于“對象”來抽象所有這些底層功能的模型。這樣才能讓多個不同的游戲,都基于一套底層進(jìn)行開發(fā)。
  2. 部署便利性:支持靈活的配置文件、命令行參數(shù)、環(huán)境變量的引用;支持單獨(dú)進(jìn)程啟動,而無須依賴數(shù)據(jù)庫、消息隊(duì)列中間件等設(shè)施。一般游戲都會有至少三套運(yùn)行環(huán)境,包括一個開發(fā)環(huán)境、一個內(nèi)測環(huán)境、一個外測或運(yùn)營環(huán)境。一個游戲的版本更新,往往需要更新多個環(huán)境。所以如何能盡量簡化部署就成為一個很重要的問題。我認(rèn)為一個好的服務(wù)器端框架,應(yīng)該能讓這個服務(wù)器端程序,在無配置、無依賴的情況下獨(dú)立啟動,以符合在開發(fā)、測試、演示環(huán)境下快速部署。并且能很簡單的通過配置文件、或者命令行參數(shù)的不同,在集群化下的外部測試或者運(yùn)營環(huán)境下啟動。
  3. 性能:很多游戲服務(wù)器,都會使用異步非阻塞的方式來編程。因?yàn)楫惒椒亲枞梢院芎玫奶岣叻?wù)器的吞吐量,而且可以很明確的控制多個用戶任務(wù)并發(fā)下的代碼執(zhí)行順序,從而避免多線程鎖之類的復(fù)雜問題。所以這個框架我也希望是以異步非阻塞作為基本的并發(fā)模型。這樣做還有另外一個好處,就是可以手工的控制具體的進(jìn)程,充分利用多核 CPU 服務(wù)器的性能。當(dāng)然異步代碼可讀性因?yàn)榇罅康幕卣{(diào)函數(shù),會變得很難閱讀,幸好我們還可以用“協(xié)程”來改善這個問題。
  4. 擴(kuò)展性:支持服務(wù)器之間的通信,進(jìn)程狀態(tài)管理,類似 SOA 的集群管理。自動容災(zāi)和自動擴(kuò)容,其實(shí)關(guān)鍵點(diǎn)是服務(wù)進(jìn)程的狀態(tài)同步和管理。我希望一個通用的底層,可以把所有的服務(wù)器間調(diào)用,都通過一個統(tǒng)一的集權(quán)管理模型管理起來,這樣就可以不再每個項(xiàng)目去關(guān)心集群間通信、尋址等問題。

一旦需求明確下來,基本的層級結(jié)構(gòu)也可以設(shè)計(jì)了: 

教你從頭寫游戲服務(wù)器框架

最后,整體的架構(gòu)模塊類似: 

教你從頭寫游戲服務(wù)器框架

通信模塊

對于通信模塊來說,需要有靈活的可替換協(xié)議的能力,就必須按一定的層次進(jìn)行進(jìn)一步的劃分。對于游戲來說,最底層的通信協(xié)議,一般會使用 TCP 和 UDP 這兩種,在服務(wù)器之間,也會使用消息隊(duì)列中間件一類通信軟件??蚣鼙仨氁心芡轮С诌@幾通信協(xié)議的能力。故此設(shè)計(jì)了一個層次為: Transport

在協(xié)議層面,最基本的需求有“分包”“分發(fā)”“對象序列化”等幾種需求。如果要支持“請求-響應(yīng)”模式,還需要在協(xié)議中帶上“序列號”的數(shù)據(jù),以便對應(yīng)“請求”和“響應(yīng)”。另外,游戲通常都是一種“會話”式的應(yīng)用,也就是一系列的請求,會被視為一次“會話”,這就需要協(xié)眾需要有類似 Session ID 這種數(shù)據(jù)。為了滿足這些需求,設(shè)計(jì)一個層次為: Protocol

擁有了以上兩個層次,是可以完成最基本的協(xié)議層能力了。但是,我們往往希望業(yè)務(wù)數(shù)據(jù)的協(xié)議包,能自動化的成為編程中的 對象,所以在處理消息體這里,需要一個可選的額外層次,用來把字節(jié)數(shù)組,轉(zhuǎn)換成對象。所以我設(shè)計(jì)了一個特別的處理器:ObjectProcessor ,去規(guī)范通信模塊中對象序列化、反序列化的接口。 

教你從頭寫游戲服務(wù)器框架

Transport

此層次是為了統(tǒng)一各種不同的底層傳輸協(xié)議而設(shè)置的,最基本應(yīng)該支持 TCP 和 UDP 這兩種協(xié)議。對于通信協(xié)議的抽象,其實(shí)在很多底層庫也做的非常好了,比如 Linux 的 socket 庫,其讀寫 API 甚至可以和文件的讀寫通用。C# 的 Socket 庫在 TCP 和 UDP 之間,其 api 也幾乎是完全一樣的。但是由于作用游戲服務(wù)器,很多適合還會接入一些特別的“接入層”,比如一些代理服務(wù)器,或者一些消息中間件,這些 API 可是五花八門的。另外,在 html5 游戲(比如微信小游戲)和一些頁游領(lǐng)域,還有用 HTTP 服務(wù)器作為游戲服務(wù)器的傳統(tǒng)(如使用 WebSocket 協(xié)議),這樣就需要一個完全不同的傳輸層了。

服務(wù)器傳輸層在異步模型下的基本使用序列,就是:

在主循環(huán)中,不斷嘗試讀取有什么數(shù)據(jù)可讀

如果上一步返回有數(shù)據(jù)到達(dá)了,則讀取數(shù)據(jù)

讀取數(shù)據(jù)處理后,需要發(fā)送數(shù)據(jù),則向網(wǎng)絡(luò)寫入數(shù)據(jù)

根據(jù)上面三個特點(diǎn),可以歸納出一個基本的接口:

  1. class Transport { 
  2. public:  
  3.  /** 
  4.  * 初始化Transport對象,輸入Config對象配置最大連接數(shù)等參數(shù),可以是一個新建的Config對象。 
  5.  */  
  6.  virtual int Init(Config* config) = 0; 
  7.  /** 
  8.  * 檢查是否有數(shù)據(jù)可以讀取,返回可讀的事件數(shù)。后續(xù)代碼應(yīng)該根據(jù)此返回值循環(huán)調(diào)用Read()提取數(shù)據(jù)。 
  9.  * 參數(shù)fds用于返回出現(xiàn)事件的所有fd列表,len表示這個列表的最大長度。如果可用事件大于這個數(shù)字,并不影響后續(xù)可以Read()的次數(shù)。 
  10.  * fds的內(nèi)容,如果出現(xiàn)負(fù)數(shù),表示有一個新的終端等待接入。 
  11.  */ 
  12.  virtual int Peek(int* fds, int len) = 0; 
  13.  /** 
  14.  * 讀取網(wǎng)絡(luò)管道中的數(shù)據(jù)。數(shù)據(jù)放在輸出參數(shù) peer 的緩沖區(qū)中。 
  15.  * @param peer 參數(shù)是產(chǎn)生事件的通信對端對象。 
  16.  * @return 返回值為可讀數(shù)據(jù)的長度,如果是 0 表示沒有數(shù)據(jù)可以讀,返回 -1 表示連接需要被關(guān)閉。 
  17.  */ 
  18.  virtual int Read( Peer* peer) = 0; 
  19.  /** 
  20.  * 寫入數(shù)據(jù),output_buf, buf_len為想要寫入的數(shù)據(jù)緩沖區(qū),output_peer為目標(biāo)隊(duì)端, 
  21.  * 返回值表示成功寫入了的數(shù)據(jù)長度。-1表示寫入出錯。 
  22.  */ 
  23.  virtual int Write(const char* output_buf, int buf_len, const Peer& output_peer) = 0; 
  24.  /** 
  25.  * 關(guān)閉一個對端的連接 
  26.  */ 
  27.  virtual void ClosePeer(const Peer& peer) = 0; 
  28.  /** 
  29.  * 關(guān)閉Transport對象。 
  30.  */ 
  31.  virtual void Close() = 0; 

在上面的定義中,可以看到需要有一個 Peer 類型。這個類型是為了代表通信的客戶端(對端)對象。在一般的 Linux 系統(tǒng)中,一般我們用 fd (File Description)來代表。但是因?yàn)樵诳蚣苤?,我們還需要為每個客戶端建立接收數(shù)據(jù)的緩存區(qū),以及記錄通信地址等功能,所以在 fd 的基礎(chǔ)上封裝了一個這樣的類型。這樣也有利于把 UDP 通信以不同客戶端的模型,進(jìn)行封裝。

  1. ///@brief 此類型負(fù)責(zé)存放連接過來的客戶端信息和數(shù)據(jù)緩沖區(qū) 
  2. class Peer { 
  3. public:  
  4.  int buf_size_; ///< 緩沖區(qū)長度 
  5.  char* const buffer_;///< 緩沖區(qū)起始地址 
  6.  int produced_pos_; ///< 填入了數(shù)據(jù)的長度 
  7.  int consumed_pos_; ///< 消耗了數(shù)據(jù)的長度 
  8.  int GetFd() const; 
  9.  void SetFd(int fd); /// 獲得本地地址 
  10.  const struct sockaddr_in& GetLocalAddr() const; 
  11.  void SetLocalAddr(const struct sockaddr_in& localAddr); /// 獲得遠(yuǎn)程地址 
  12.  const struct sockaddr_in& GetRemoteAddr() const; 
  13.  void SetRemoteAddr(const struct sockaddr_in& remoteAddr); 
  14. private: 
  15.  int fd_; ///< 收發(fā)數(shù)據(jù)用的fd 
  16.  struct sockaddr_in remote_addr_; ///< 對端地址 
  17.  struct sockaddr_in local_addr_; ///< 本端地址 
  18. }; 

游戲使用 UDP 協(xié)議的特點(diǎn):一般來說 UDP 是無連接的,但是對于游戲來說,是肯定需要有明確的客戶端的,所以就不能簡單用一個 UDP socket 的fd 來代表客戶端,這就造成了上層的代碼無法簡單在 UDP 和 TCP 之間保持一致。因此這里使用 Peer 這個抽象層,正好可以接近這個問題。這也可以用于那些使用某種消息隊(duì)列中間件的情況,因?yàn)榭赡苓@些中間件,也是多路復(fù)用一個 fd 的,甚至可能就不是通過使用 fd 的 API 來開發(fā)的。

對于上面的 Transport 定義,對于 TCP 的實(shí)現(xiàn)者來說,是非常容易能完成的。但是對于 UDP 的實(shí)現(xiàn)者來說,則需要考慮如何寵妃利用 Peer ,特別是 Peer.fd_ 這個數(shù)據(jù)。我在實(shí)現(xiàn)的時候,使用了一套虛擬的 fd 機(jī)制,通過一個客戶端的 IPv4 地址到 int 的對應(yīng) Map ,來對上層提供區(qū)分客戶端的功能。在 Linux 上,這些 IO 都可以使用 epoll 庫來實(shí)現(xiàn),在 Peek() 函數(shù)中讀取 IO 事件,在 Read()/Write() 填上 socket 的調(diào)用就可以了。

另外,為了實(shí)現(xiàn)服務(wù)器之間的通信,還需要設(shè)計(jì)和 Tansport 對應(yīng)的一個類型:Connector 。這個抽象基類,用于以客戶端模型對服務(wù)器發(fā)起請求。其設(shè)計(jì)和 Transport 大同小異。除了 Linux 環(huán)境下的 Connecotr ,我還實(shí)現(xiàn)了在 C# 下的代碼,以便用 Unity 開發(fā)的客戶端可以方便的使用。由于 .NET 本身就支持異步模型,所以其實(shí)現(xiàn)也不費(fèi)太多功夫。

  1. /** 
  2.  * @brief 客戶端使用的連接器類,代表傳輸協(xié)議,如 TCP 或 UDP 
  3.  */ 
  4. class Connector { 
  5. public: virtual ~Connector() {}  
  6.   
  7.  /** 
  8.  * @brief 初始化建立連接等 
  9.  * @param config 需要的配置 
  10.  * @return 0 為成功 
  11.  */ 
  12.  virtual int Init(Config* config) = 0; 
  13.  /** 
  14.  * @brief 關(guān)閉 
  15.  */ 
  16.  virtual void Close() = 0; 
  17.  /** 
  18.  * @brief 讀取是否有網(wǎng)絡(luò)數(shù)據(jù)到來 
  19.  * 讀取有無數(shù)據(jù)到來,返回值為可讀事件的數(shù)量,通常為1 
  20.  * 如果為0表示沒有數(shù)據(jù)可以讀取。 
  21.  * 如果返回 -1 表示出現(xiàn)網(wǎng)絡(luò)錯誤,需要關(guān)閉此連接。 
  22.  * 如果返回 -2 表示此連接成功連上對端。 
  23.  * @return 網(wǎng)絡(luò)數(shù)據(jù)的情況 
  24.  */ 
  25.  virtual int Peek() = 0; 
  26.  /** 
  27.  * @brief 讀取網(wǎng)絡(luò)數(shù)  
  28.  * 讀取連接里面的數(shù)據(jù),返回讀取到的字節(jié)數(shù),如果返回0表示沒有數(shù)據(jù), 
  29.  * 如果buffer_length是0, 也會返回0, 
  30.  * @return 返回-1表示連接需要關(guān)閉(各種出錯也返回0) 
  31.  */ 
  32.  virtual int Read(char* ouput_buffer, int buffer_length) = 0; 
  33.  /** 
  34.  * @brief 把input_buffer里的數(shù)據(jù)寫入網(wǎng)絡(luò)連接,返回寫入的字節(jié)數(shù)。 
  35.  * @return 如果返回-1表示寫入出錯,需要關(guān)閉此連接。 
  36.  */ 
  37.  virtual int Write(const char* input_buffer, int buffer_length) = 0; 
  38. protected: 
  39.  Connector(){} 
  40. }; 

Protocol

對于通信“協(xié)議”來說,其實(shí)包含了許許多多的含義。在眾多的需求中,我所定義的這個協(xié)議層,只希望完成四個最基本的能力:

  1. 分包:從流式傳輸層切分出一個個單獨(dú)的數(shù)據(jù)單元,或者把多個“碎片”數(shù)據(jù)拼合成一個完整的數(shù)據(jù)單元的能力。一般解決這個問題,需要在協(xié)議頭部添加一個“長度”字段。
  2. 請求響應(yīng)對應(yīng):這對于異步非阻塞的通信模式下,是非常重要的功能。因?yàn)榭赡茉谝凰查g發(fā)出了很多個請求,而回應(yīng)則會不分先后的到達(dá)。協(xié)議頭部如果有一個不重復(fù)的“序列號”字段,就可以對應(yīng)起哪個回應(yīng)是屬于哪個請求的。
  3. 會話保持:由于游戲的底層網(wǎng)絡(luò),可能會使用 UDP 或者 HTTP 這種非長連接的傳輸方式,所以要在邏輯上保持一個會話,就不能單純的依靠傳輸層。加上我們都希望程序有抗網(wǎng)絡(luò)抖動、斷線重連的能力,所以保持會話成為一個常見的需求。我參考在 Web 服務(wù)領(lǐng)域的會話功能,設(shè)計(jì)了一個 Session 功能,在協(xié)議中加上 Session ID 這樣的數(shù)據(jù),就能比較簡單的保持會話。
  4. 分發(fā):游戲服務(wù)器必定會包含多個不同的業(yè)務(wù)邏輯,因此需要多種不同數(shù)據(jù)格式的協(xié)議包,為了把對應(yīng)格式的數(shù)據(jù)轉(zhuǎn)發(fā)。

除了以上三個功能,實(shí)際上希望在協(xié)議層處理的能力,還有很多,最典型的就是對象序列化的功能,還有壓縮、加密功能等等。我之所以沒有把對象序列化的能力放在 Protocol 中,原因是對象序列化中的“對象”本身是一個業(yè)務(wù)邏輯關(guān)聯(lián)性非常強(qiáng)的概念。在 C++ 中,并沒有完整的“對象”模型,也缺乏原生的反射支持,所以無法很簡單的把代碼層次通過“對象”這個抽象概念劃分開來。但是我也設(shè)計(jì)了一個 ObjectProcessor ,把對象序列化的支持,以更上層的形式結(jié)合到框架中。這個 Processor 是可以自定義對象序列化的方法,這樣開發(fā)者就可以自己選擇任何“編碼、解碼”的能力,而不需要依靠底層的支持。

至于壓縮和加密這一類功能,確實(shí)是可以放在 Protocol 層中實(shí)現(xiàn),甚至可以作為一個抽象層次加入 Protocol ,可能只有一個 Protocol 層不足以支持這么豐富的功能,需要好像 Apache Mina 這樣,設(shè)計(jì)一個“調(diào)用鏈”的模型。但是為了簡單起見,我覺得在具體需要用到的地方,再額外添加 Protocol 的實(shí)現(xiàn)類就好,比如添加一個“帶壓縮功能的 TLV Protocol 類型”之類的。

消息本身被抽象成一個叫 Message 的類型,它擁有“服務(wù)名字”“會話ID”兩個消息頭字段,用以完成“分發(fā)”和“會話保持”功能。而消息體則被放在一個字節(jié)數(shù)組中,并記錄下字節(jié)數(shù)組的長度。

  1. enum MessageType { 
  2.  TypeError, ///< 錯誤的協(xié)議 
  3.  TypeRequest, ///< 請求類型,從客戶端發(fā)往服務(wù)器 
  4.  TypeResponse, ///< 響應(yīng)類型,服務(wù)器收到請求后返回 
  5.  TypeNotice ///< 通知類型,服務(wù)器主動通知客戶端 
  6. }; 
  7. ///@brief 通信消息體的基類 
  8. ///基本上是一個 char[] 緩沖區(qū) 
  9. struct Message { 
  10. public
  11.  static int MAX_MAESSAGE_LENGTH; 
  12.  static int MAX_HEADER_LENGTH; 
  13.   
  14.  MessageType type; ///< 此消息體的類型(MessageType)信息 
  15.  virtual ~Message(); virtual Message& operator=(const Message& right); 
  16.  /** 
  17.  * @brief 把數(shù)據(jù)拷貝進(jìn)此包體緩沖區(qū) 
  18.  */ 
  19.  void SetData(const char* input_ptr, int input_length); 
  20.  ///@brief 獲得數(shù)據(jù)指針 
  21.  inline char* GetData() const{ 
  22.  return data_; 
  23.  } 
  24.  ///@brief 獲得數(shù)據(jù)長度 
  25.  inline int GetDataLen() const{ 
  26.  return data_len_; 
  27.  } 
  28.   
  29.  char* GetHeader() const; 
  30.  int GetHeaderLen() const; 
  31. protected: 
  32.  Message(); 
  33.  Message(const Message& message); 
  34.   
  35. private: 
  36.  char* data_; // 包體內(nèi)容緩沖區(qū) 
  37.  int data_len_; // 包體長度 
  38. }; 

根據(jù)之前設(shè)計(jì)的“請求響應(yīng)”和“通知”兩種通信模式,需要設(shè)計(jì)出三種消息類型繼承于 Message,他們是:

  • Request 請求包
  • Response 響應(yīng)包
  • Notice 通知包

Request 和 Response 兩個類,都有記錄序列號的 seq_id 字段,但 Notice 沒有。Protocol 類就是負(fù)責(zé)把一段 buffer 字節(jié)數(shù)組,轉(zhuǎn)換成 Message 的子類對象。所以需要針對三種 Message 的子類型都實(shí)現(xiàn)對應(yīng)的 Encode() / Decode() 方法。

  1. class Protocol { 
  2. public
  3.  virtual ~Protocol() { 
  4.  } 
  5.  /** 
  6.  * @brief 把請求消息編碼成二進(jìn)制數(shù)據(jù) 
  7.  * 編碼,把msg編碼到buf里面,返回寫入了多長的數(shù)據(jù),如果超過了 len,則返回-1表示錯誤。 
  8.  * 如果返回 0 ,表示不需要編碼,框架會直接從 msg 的緩沖區(qū)讀取數(shù)據(jù)發(fā)送。 
  9.  * @param buf 目標(biāo)數(shù)據(jù)緩沖區(qū) 
  10.  * @param offset 目標(biāo)偏移量 
  11.  * @param len 目標(biāo)數(shù)據(jù)長度 
  12.  * @param msg 輸入消息對象 
  13.  * @return 編碼完成所用的字節(jié)數(shù),如果 < 0 表示出錯 
  14.  */ 
  15.  virtual int Encode(char* buf, int offset, int len, const Request& msg) = 0; 
  16.  /** 
  17.  * 編碼,把msg編碼到buf里面,返回寫入了多長的數(shù)據(jù),如果超過了 len,則返回-1表示錯誤。 
  18.  * 如果返回 0 ,表示不需要編碼,框架會直接從 msg 的緩沖區(qū)讀取數(shù)據(jù)發(fā)送。 
  19.  * @param buf 目標(biāo)數(shù)據(jù)緩沖區(qū) 
  20.  * @param offset 目標(biāo)偏移量 
  21.  * @param len 目標(biāo)數(shù)據(jù)長度 
  22.  * @param msg 輸入消息對象 
  23.  * @return 編碼完成所用的字節(jié)數(shù),如果 < 0 表示出錯 
  24.  */ 
  25.  virtual int Encode(char* buf, int offset, int len, const Response& msg) = 0; 
  26.  /** 
  27.  * 編碼,把msg編碼到buf里面,返回寫入了多長的數(shù)據(jù),如果超過了 len,則返回-1表示錯誤。 
  28.  * 如果返回 0 ,表示不需要編碼,框架會直接從 msg 的緩沖區(qū)讀取數(shù)據(jù)發(fā)送。 
  29.  * @param buf 目標(biāo)數(shù)據(jù)緩沖區(qū) 
  30.  * @param offset 目標(biāo)偏移量 
  31.  * @param len 目標(biāo)數(shù)據(jù)長度 
  32.  * @param msg 輸入消息對象 
  33.  * @return 編碼完成所用的字節(jié)數(shù),如果 < 0 表示出錯 
  34.  */ 
  35.  virtual int Encode(char* buf, int offset, int len, const Notice& msg) = 0; 
  36.  /** 
  37.  * 開始編碼,會返回即將解碼出來的消息類型,以便使用者構(gòu)造合適的對象。 
  38.  * 實(shí)際操作是在進(jìn)行“分包”操作。 
  39.  * @param buf 輸入緩沖區(qū) 
  40.  * @param offset 輸入偏移量 
  41.  * @param len 緩沖區(qū)長度 
  42.  * @param msg_type 輸出參數(shù),表示下一個消息的類型,只在返回值 > 0 的情況下有效,否則都是 TypeError 
  43.  * @return 如果返回0表示分包未完成,需要繼續(xù)分包。如果返回-1表示協(xié)議包頭解析出錯。其他返回值表示這個消息包占用的長度。 
  44.  */ 
  45.  virtual int DecodeBegin(const char* buf, int offset, int len, 
  46.  MessageType* msg_type) = 0; 
  47.  /** 
  48.  * 解碼,把之前DecodeBegin()的buf數(shù)據(jù)解碼成具體消息對象。 
  49.  * @param request 輸出參數(shù),解碼對象會寫入此指針 
  50.  * @return 返回0表示成功,-1表示失敗。 
  51.  */ 
  52.  virtual int Decode(Request* request) = 0; 
  53.  /** 
  54.  * 解碼,把之前DecodeBegin()的buf數(shù)據(jù)解碼成具體消息對象。 
  55.  * @param request 輸出參數(shù),解碼對象會寫入此指針 
  56.  * @return 返回0表示成功,-1表示失敗。 
  57.  */ 
  58.  virtual int Decode(Response* response) = 0; 
  59.  /** 
  60.  * 解碼,把之前DecodeBegin()的buf數(shù)據(jù)解碼成具體消息對象。 
  61.  * @param request 輸出參數(shù),解碼對象會寫入此指針 
  62.  * @return 返回0表示成功,-1表示失敗。 
  63.  */ 
  64.  virtual int Decode(Notice* notice) = 0;protected: 
  65.  Protocol() { 
  66.  } 
  67. }; 

這里有一點(diǎn)需要注意,由于 C++ 沒有內(nèi)存垃圾搜集和反射的能力,在解釋數(shù)據(jù)的時候,并不能一步就把一個 char[] 轉(zhuǎn)換成某個子類對象,而必須分成兩步處理。

  • 先通過 DecodeBegin() 來返回,將要解碼的數(shù)據(jù)是屬于哪個子類型的。同時完成分包的工作,通過返回值來告知調(diào)用者,是否已經(jīng)完整的收到一個包。
  • 調(diào)用對應(yīng)類型為參數(shù)的 Decode() 來具體把數(shù)據(jù)寫入對應(yīng)的輸出變量。

對于 Protocol 的具體實(shí)現(xiàn)子類,我首先實(shí)現(xiàn)了一個 LineProtocol ,是一個非常不嚴(yán)謹(jǐn)?shù)?,基于文本ASCII編碼的,用空格分隔字段,用回車分包的協(xié)議。用來測試這個框架是否可行。因?yàn)檫@樣可以直接通過 telnet 工具,來測試協(xié)議的編解碼。然后我按照 TLV (Type Length Value)的方法設(shè)計(jì)了一個二進(jìn)制的協(xié)議。大概的定義如下:

協(xié)議分包: [消息類型:int:2] [消息長度:int:4] [消息內(nèi)容:bytes:消息長度]

消息類型取值:

  • 0x00 Error
  • 0x01 Request
  • 0x02 Response
  • 0x03 Notice 
教你從頭寫游戲服務(wù)器框架

一個名為 TlvProtocol 的類型完成對這個協(xié)議的實(shí)現(xiàn)。

Processor

處理器層是我設(shè)計(jì)用來對接具體業(yè)務(wù)邏輯的抽象層,它主要通過輸入?yún)?shù) Request 和 Peer 來獲得客戶端的輸入數(shù)據(jù),然后通過 Server 類的 Reply()/Inform() 來返回 Response 和 Notice 消息。實(shí)際上 Transport 和 Protocol 的子類們,都屬于 net 模塊,而各種 Processor 和 Server/Client 這些功能類型,屬于另外一個 processor 模塊。這樣設(shè)計(jì)的原因,是希望所有 processor 模塊的代碼單向的依賴 net 模塊的代碼,但反過來不成立。

Processor 基類非常簡單,就是一個處理函數(shù)回調(diào)函數(shù)入口 Process():

  1. ///@brief 處理器基類,提供業(yè)務(wù)邏輯回調(diào)接口 
  2. class Processor { 
  3. public
  4.  Processor(); 
  5.  virtual ~Processor(); 
  6.   
  7.  /** 
  8.  * 初始化一個處理器,參數(shù)server為業(yè)務(wù)邏輯提供了基本的能力接口。 
  9.  */ 
  10.  virtual int Init(Server* server, Config* config = NULL); 
  11.  /** 
  12.  * 處理請求-響應(yīng)類型包實(shí)現(xiàn)此方法,返回值是0表示成功,否則會被記錄在錯誤日志中。 
  13.  * 參數(shù)peer表示發(fā)來請求的對端情況。其中 Server 對象的指針,可以用來調(diào)用 Reply(), 
  14.  * Inform() 等方法。如果是監(jiān)聽多個服務(wù)器,server 參數(shù)則會是不同的對象。 
  15.  */ 
  16.  virtual int Process(const Request& request, const Peer& peer, 
  17.  Server* server); 
  18.  /** 
  19.  * 關(guān)閉清理處理器所占用的資源 
  20.  */ 
  21.  virtual int Close(); 
  22. }; 

設(shè)計(jì)完 Transport/Protocol/Processor 三個通信處理層次后,就需要一個組合這三個層次的代碼,那就是 Server 類。這個類在 Init() 的時候,需要上面三個類型的子類作為參數(shù),以組合成不同功能的服務(wù)器,如:

  1. TlvProtocol tlv_protocol; // Type Length Value 格式分包協(xié)議,需要和客戶端一致 
  2. TcpTransport tcp_transport; // 使用 TCP 的通信協(xié)議,默認(rèn)監(jiān)聽 0.0.0.0:6666 
  3. EchoProcessor echo_processor; // 業(yè)務(wù)邏輯處理器 
  4. Server server; // DenOS 的網(wǎng)絡(luò)服務(wù)器主對象 
  5. server.Init(&tcp_transport, &tlv_protocol, &echo_processor); // 組裝一個游戲服務(wù)器對象:TLV 編碼、TCP 通信和回音服務(wù) 

Server 類型還需要一個 Update() 函數(shù),讓用戶進(jìn)程的“主循環(huán)”不停的調(diào)用,用來驅(qū)動整個程序的運(yùn)行。這個 Update() 函數(shù)的內(nèi)容非常明確:

檢查網(wǎng)絡(luò)是否有數(shù)據(jù)需要處理(通過 Transport 對象)

有數(shù)據(jù)的話就進(jìn)行解碼處理(通過 Protocol 對象)

解碼成功后進(jìn)行業(yè)務(wù)邏輯的分發(fā)調(diào)用(通過 Processor 對象)

另外,Server 還需要處理一些額外的功能,比如維護(hù)一個會話緩存池(Session),提供發(fā)送 Response 和 Notice 消息的接口。當(dāng)這些工作都完成后,整套系統(tǒng)已經(jīng)可以用來作為一個比較“通用”的網(wǎng)絡(luò)消息服務(wù)器框架存在了。剩下的就是添加各種 Transport/Protocol/Processor 子類的工作。

  1. class Server { 
  2. public
  3.  Server(); 
  4.  virtual ~Server(); 
  5.   
  6.  /** 
  7.  * 初始化服務(wù)器,需要選擇組裝你的通信協(xié)議鏈 
  8.  */ 
  9.  int Init(Transport* transport, Protocol* protocol, Processor* processor, Config* config = NULL); 
  10.  /** 
  11.  * 阻塞方法,進(jìn)入主循環(huán)。 
  12.  */ 
  13.  void Start(); 
  14.  /** 
  15.  * 需要循環(huán)調(diào)用驅(qū)動的方法。如果返回值是0表示空閑。其他返回值表示處理過的任務(wù)數(shù)。 
  16.  */ 
  17.  virtual int Update(); 
  18.  void ClosePeer(Peer* peer, bool is_clear = false); //關(guān)閉當(dāng)個連接,is_clear 表示是否最終整體清理 
  19.  /** 
  20.  * 關(guān)閉服務(wù)器 
  21.  */ 
  22.  void Close(); 
  23.  /** 
  24.  * 對某個客戶端發(fā)送通知消息, 
  25.  * 參數(shù)peer代表要通知的對端。 
  26.  */ 
  27.  int Inform(const Notice& notice, const Peer& peer); 
  28.  /** 
  29.  * 對某個 Session ID 對應(yīng)的客戶端發(fā)送通知消息,返回 0 表示可以發(fā)送,其他值為發(fā)送失敗。 
  30.  * 此接口能支持?jǐn)嗑€重連,只要客戶端已經(jīng)成功連接,并使用舊的 Session ID,同樣有效。 
  31.  */ 
  32.  int Inform(const Notice& notice, const std::string& session_id); 
  33.  /** 
  34.  * 對某個客戶端發(fā)來的Request發(fā)回回應(yīng)消息。 
  35.  * 參數(shù)response的成員seqid必須正確填寫,才能正確回應(yīng)。 
  36.  * 返回0成功,其它值(-1)表示失敗。 
  37.  */ 
  38.  int Reply(Response* response, const Peer& peer); 
  39.  /** 
  40.  * 對某個 Session ID 對應(yīng)的客戶端發(fā)送回應(yīng)消息。 
  41.  * 參數(shù) response 的 seqid 成員系統(tǒng)會自動填寫會話中記錄的數(shù)值。 
  42.  * 此接口能支持?jǐn)嗑€重連,只要客戶端已經(jīng)成功連接,并使用舊的 Session ID,同樣有效。 
  43.  * 返回0成功,其它值(-1)表示失敗。 
  44.  */ 
  45.  int Reply(Response* response, const std::string& session_id); 
  46.  /** 
  47.  * 會話功能 
  48.  */ 
  49.  Session* GetSession(const std::string& session_id = "", bool use_this_id = false); 
  50.  Session* GetSessionByNumId(int session_id = 0); 
  51.  bool IsExist(const std::string& session_id); 
  52.   
  53. }; 

有了 Server 類型,肯定也需要有 Client 類型。而 Client 類型的設(shè)計(jì)和 Server 類似,但就不是使用 Transport 接口作為傳輸層,而是 Connector 接口。不過 Protocol 的抽象層是完全重用的。Client 并不需要 Processor 這種形式的回調(diào),而是直接傳入接受數(shù)據(jù)消息就發(fā)起回調(diào)的接口對象 ClientCallback。

  1. class ClientCallback { 
  2. public
  3.   
  4.  ClientCallback() { 
  5.  } 
  6.  virtual ~ClientCallback() { 
  7.  // Do nothing 
  8.  } 
  9.  /** 
  10.  * 當(dāng)連接建立成功時回調(diào)此方法。 
  11.  * @return 返回 -1 表示不接受這個連接,需要關(guān)閉掉此連接。 
  12.  */ 
  13.  virtual int OnConnected() { 
  14.  return 0; 
  15.  } 
  16.  /** 
  17.  * 當(dāng)網(wǎng)絡(luò)連接被關(guān)閉的時候,調(diào)用此方法 
  18.  */ 
  19.  virtual void OnDisconnected() { // Do nothing 
  20.  } 
  21.  /** 
  22.  * 收到響應(yīng),或者請求超時,此方法會被調(diào)用。 
  23.  * @param response 從服務(wù)器發(fā)來的回應(yīng) 
  24.  * @return 如果返回非0值,服務(wù)器會打印一行錯誤日志。 
  25.  */ 
  26.  virtual int Callback(const Response& response) { 
  27.  return 0; 
  28.  } 
  29.  /** 
  30.  * 當(dāng)請求發(fā)生錯誤,比如超時的時候,返回這個錯誤 
  31.  * @param err_code 錯誤碼 
  32.  */ 
  33.  virtual void OnError(int err_code){ 
  34.  WARN_LOG("The request is timeout, err_code: %d", err_code); 
  35.  } 
  36.  /** 
  37.  * 收到通知消息時,此方法會被調(diào)用 
  38.  */ 
  39.  virtual int Callback(const Notice& notice) { 
  40.  return 0; 
  41.  } 
  42.  /** 
  43.  * 返回此對象是否應(yīng)該被刪除。此方法會被在 Callback() 調(diào)用前調(diào)用。 
  44.  * @return 如果返回 true,則會調(diào)用 delete 此對象的指針。 
  45.  */ 
  46.  virtual bool ShouldBeRemoved() { 
  47.  return false
  48.  } 
  49. }; 
  50. class Client : public Updateable { 
  51.   
  52. public
  53.  Client(); virtual ~Client(); 
  54.  /** 
  55.  * 連接服務(wù)器 
  56.  * @param connector 傳輸協(xié)議,如 TCP, UDP ... 
  57.  * @param protocol 分包協(xié)議,如 TLV, Line, TDR ... 
  58.  * @param notice_callback 收到通知后觸發(fā)的回調(diào)對象,如果傳輸協(xié)議有“連接概念”(如TCP/TCONND),建立、關(guān)閉連接時也會調(diào)用。 
  59.  * @param config 配置文件對象,將讀取以下配置項(xiàng)目:MAX_TRANSACTIONS_OF_CLIENT 客戶端最大并發(fā)連接數(shù); BUFFER_LENGTH_OF_CLIENT客戶端收包緩存;CLIENT_RESPONSE_TIMEOUT 客戶端響應(yīng)等待超時時間。 
  60.  * @return 返回 0 表示成功,其他表示失敗 
  61.  */ 
  62.  int Init(Connector* connector, Protocol* protocol, 
  63.  ClientCallback* notice_callback = NULL, Config* config = NULL); 
  64.  /** 
  65.  * callback 參數(shù)可以為 NULL,表示不需要回應(yīng),只是單純的發(fā)包即可。 
  66.  */ 
  67.  virtual int SendRequest(Request* request, ClientCallback* callback = NULL); 
  68.  /** 
  69.  * 返回值表示有多少數(shù)據(jù)需要處理,返回-1為出錯,需要關(guān)閉連接。返回0表示沒有數(shù)據(jù)需要處理。 
  70.  */ 
  71.  virtual int Update(); 
  72.  virtual void OnExit(); 
  73.  void Close(); 
  74.  Connector* connector() ; 
  75.  ClientCallback* notice_callback() ; 
  76.  Protocol* protocol() ; 
  77. }; 

至此,客戶端和服務(wù)器端基本設(shè)計(jì)完成,可以直接通過編寫測試代碼,來檢查是否運(yùn)行正常。

 

責(zé)任編輯:武曉燕 來源: 今日頭條
相關(guān)推薦

2019-02-20 13:57:48

游戲服務(wù)器框架

2018-06-04 10:30:47

游戲服務(wù)器框架

2019-09-16 15:30:51

2016-08-09 19:36:48

2017-07-19 08:30:31

2020-03-02 17:49:40

大型游戲服務(wù)器

2014-12-22 13:59:05

FireflyGFirefly框架

2014-04-10 09:51:36

2017-07-20 10:35:51

2018-09-19 09:17:13

2017-07-19 16:17:53

2016-01-08 10:24:32

Docker容器容器技術(shù)

2018-05-18 10:22:39

沖突游戲服務(wù)器

2018-11-28 09:53:50

游戲服務(wù)器線程

2013-03-07 10:08:04

模擬城市游戲服務(wù)器服務(wù)器忙

2018-12-17 10:59:50

游戲服務(wù)器結(jié)構(gòu)

2009-02-10 15:35:00

網(wǎng)絡(luò)游戲服務(wù)器網(wǎng)絡(luò)游戲

2012-11-21 09:47:42

Pomelo游戲服務(wù)端框架開源

2019-08-01 15:25:17

Http服務(wù)器協(xié)議

2017-11-30 12:39:06

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號