本文由雲+社區發表做者:韓偉javascript
大概已經有差很少一年沒寫技術文章了,緣由是今年投入了一些具體遊戲項目的開發。這些新的遊戲項目,比較接近獨立遊戲的開發方式。我以爲公司的「祖傳」服務器框架技術不太適合,因此從頭寫了一個遊戲服務器端的框架,以便得到更好的開發效率和靈活性。如今項目將近上線,有時間就想總結一下,這樣一個遊戲服務器框架的設計和實現過程。html
這個框架的基本運行環境是 Linux ,採用 C++ 編寫。爲了能在各類環境上運行和使用,因此採用了 gcc 4.8 這個「古老」的編譯器,以 C99 規範開發。html5
因爲「越通用的代碼,就是越沒用的代碼」,因此在設計之初,我就認爲應該使用分層的模式來構建整個系統。按照遊戲服務器的通常需求劃分,最基本的能夠分爲兩層:java
我但願能有一個基本完整的「底層基礎功能」的框架,能夠被複用於多個不一樣的遊戲。因爲目標是開發一個 適合獨立遊戲開發 的遊戲服務器框架。因此最基本的需求分析爲:redis
功能性需求數據庫
非功能性需求編程
一旦需求明確下來,基本的層級結構也能夠設計了:json
層次 | 功能 | 約束 |
---|---|---|
邏輯層 | 實現更具體的業務邏輯 | 能調用全部下層代碼,但應主要依賴接口層 |
實現層 | 對各類具體的通訊協議、存儲設備等功能的實現 | 知足下層的接口層來作實現,禁止同層間互相調用 |
接口層 | 定義了各模塊的基本使用方式,用以隔離具體的實現和設計,從而提供互相替換的能力 | 本層之間代碼能夠互相調用,但禁止調用上層代碼 |
工具層 | 提供通用的 C++ 工具庫功能,如 log/json/ini/日期時間/字符串處理 等等 | 不該該調用其餘層代碼,也不該該調用同層其餘模塊 |
第三方庫 | 提供諸如 redis/tcaplus 或者其餘現成功能,其地位和「工具層」同樣 | 不該該調用其餘層代碼,甚至不該該修改其源碼 |
最後,總體的架構模塊相似:api
說明 | 通訊 | 處理器 | 緩存 | 持久化 |
---|---|---|---|---|
功能實現 | TcpUdpKcpTlvLine | JsonHandlerObjectProcessor | SessionLocalCacheRedisMapRamMapZooKeeperMap | FileDataStoreRedisDataStroe |
接口定義 | TransferProtocol | ServerClientProcessor | DataMapSerializable | DataStore |
工具類庫 | ConfigLOGJSONCoroutine |
對於通訊模塊來講,須要有靈活的可替換協議的能力,就必須按必定的層次進行進一步的劃分。對於遊戲來講,最底層的通訊協議,通常會使用 TCP 和 UDP 這兩種,在服務器之間,也會使用消息隊列中間件一類通訊軟件。框架必需要有能同事支持這幾通訊協議的能力。故此設計了一個層次爲: Transport數組
在協議層面,最基本的需求有「分包」「分發」「對象序列化」等幾種需求。若是要支持「請求-響應」模式,還須要在協議中帶上「序列號」的數據,以便對應「請求」和「響應」。另外,遊戲一般都是一種「會話」式的應用,也就是一系列的請求,會被視爲一次「會話」,這就須要協衆須要有相似 Session ID
這種數據。爲了知足這些需求,設計一個層次爲: Protocol
擁有了以上兩個層次,是能夠完成最基本的協議層能力了。可是,咱們每每但願業務數據的協議包,能自動化的成爲編程中的 對象,因此在處理消息體這裏,須要一個可選的額外層次,用來把字節數組,轉換成對象。因此我設計了一個特別的處理器:ObjectProcessor ,去規範通訊模塊中對象序列化、反序列化的接口。
輸入 | 層次 | 功能 | 輸出 |
---|---|---|---|
data | Transport | 通訊 | buffer |
buffer | Protocol | 分包 | Message |
Message | Processor | 分發 | object |
object | 處理模塊 | 處理 | 業務邏輯 |
此層次是爲了統一各類不一樣的底層傳輸協議而設置的,最基本應該支持 TCP 和 UDP 這兩種協議。對於通訊協議的抽象,其實在不少底層庫也作的很是好了,好比 Linux 的 socket 庫,其讀寫 API 甚至能夠和文件的讀寫通用。C# 的 Socket 庫在 TCP 和 UDP 之間,其 api 也幾乎是徹底同樣的。可是因爲做用遊戲服務器,不少適合還會接入一些特別的「接入層」,好比一些代理服務器,或者一些消息中間件,這些 API 但是五花八門的。另外,在 html5 遊戲(好比微信小遊戲)和一些頁遊領域,還有用 HTTP 服務器做爲遊戲服務器的傳統(如使用 WebSocket 協議),這樣就須要一個徹底不一樣的傳輸層了。
服務器傳輸層在異步模型下的基本使用序列,就是:
根據上面三個特色,能夠概括出一個基本的接口:
class Transport { public: /** * 初始化Transport對象,輸入Config對象配置最大鏈接數等參數,能夠是一個新建的Config對象。 */ virtual int Init(Config* config) = 0; /** * 檢查是否有數據能夠讀取,返回可讀的事件數。後續代碼應該根據此返回值循環調用Read()提取數據。 * 參數fds用於返回出現事件的全部fd列表,len表示這個列表的最大長度。若是可用事件大於這個數字,並不影響後續能夠Read()的次數。 * fds的內容,若是出現負數,表示有一個新的終端等待接入。 */ virtual int Peek(int* fds, int len) = 0; /** * 讀取網絡管道中的數據。數據放在輸出參數 peer 的緩衝區中。 * @param peer 參數是產生事件的通訊對端對象。 * @return 返回值爲可讀數據的長度,若是是 0 表示沒有數據能夠讀,返回 -1 表示鏈接須要被關閉。 */ virtual int Read( Peer* peer) = 0; /** * 寫入數據,output_buf, buf_len爲想要寫入的數據緩衝區,output_peer爲目標隊端, * 返回值表示成功寫入了的數據長度。-1表示寫入出錯。 */ virtual int Write(const char* output_buf, int buf_len, const Peer& output_peer) = 0; /** * 關閉一個對端的鏈接 */ virtual void ClosePeer(const Peer& peer) = 0; /** * 關閉Transport對象。 */ virtual void Close() = 0; }
在上面的定義中,能夠看到須要有一個 Peer 類型。這個類型是爲了表明通訊的客戶端(對端)對象。在通常的 Linux 系統中,通常咱們用 fd (File Description)來表明。可是由於在框架中,咱們還須要爲每一個客戶端創建接收數據的緩存區,以及記錄通訊地址等功能,因此在 fd 的基礎上封裝了一個這樣的類型。這樣也有利於把 UDP 通訊以不一樣客戶端的模型,進行封裝。
///@brief 此類型負責存放鏈接過來的客戶端信息和數據緩衝區 class Peer { public: int buf_size_; ///< 緩衝區長度 char* const buffer_;///< 緩衝區起始地址 int produced_pos_; ///< 填入了數據的長度 int consumed_pos_; ///< 消耗了數據的長度 int GetFd() const; void SetFd(int fd); /// 得到本地地址 const struct sockaddr_in& GetLocalAddr() const; void SetLocalAddr(const struct sockaddr_in& localAddr); /// 得到遠程地址 const struct sockaddr_in& GetRemoteAddr() const; void SetRemoteAddr(const struct sockaddr_in& remoteAddr); private: int fd_; ///< 收發數據用的fd struct sockaddr_in remote_addr_; ///< 對端地址 struct sockaddr_in local_addr_; ///< 本端地址 };
遊戲使用 UDP 協議的特色:通常來講 UDP 是無鏈接的,可是對於遊戲來講,是確定須要有明確的客戶端的,因此就不能簡單用一個 UDP socket 的fd 來表明客戶端,這就形成了上層的代碼沒法簡單在 UDP 和 TCP 之間保持一致。所以這裏使用 Peer 這個抽象層,正好能夠接近這個問題。這也能夠用於那些使用某種消息隊列中間件的狀況,由於可能這些中間件,也是多路複用一個 fd 的,甚至可能就不是經過使用 fd 的 API 來開發的。
對於上面的 Transport 定義,對於 TCP 的實現者來講,是很是容易能完成的。可是對於 UDP 的實現者來講,則須要考慮如何寵妃利用 Peer ,特別是 Peer.fd_ 這個數據。我在實現的時候,使用了一套虛擬的 fd 機制,經過一個客戶端的 IPv4 地址到 int 的對應 Map ,來對上層提供區分客戶端的功能。在 Linux 上,這些 IO 均可以使用 epoll 庫來實現,在 Peek() 函數中讀取 IO 事件,在 Read()/Write() 填上 socket 的調用就能夠了。
另外,爲了實現服務器之間的通訊,還須要設計和 Tansport 對應的一個類型:Connector 。這個抽象基類,用於以客戶端模型對服務器發起請求。其設計和 Transport 大同小異。除了 Linux 環境下的 Connecotr ,我還實現了在 C# 下的代碼,以便用 Unity 開發的客戶端能夠方便的使用。因爲 .NET 自己就支持異步模型,因此其實現也不費太多功夫。
/** * @brief 客戶端使用的鏈接器類,表明傳輸協議,如 TCP 或 UDP */ class Connector { public: virtual ~Connector() {} /** * @brief 初始化創建鏈接等 * @param config 須要的配置 * @return 0 爲成功 */ virtual int Init(Config* config) = 0; /** * @brief 關閉 */ virtual void Close() = 0; /** * @brief 讀取是否有網絡數據到來 * 讀取有無數據到來,返回值爲可讀事件的數量,一般爲1 * 若是爲0表示沒有數據能夠讀取。 * 若是返回 -1 表示出現網絡錯誤,須要關閉此鏈接。 * 若是返回 -2 表示此鏈接成功連上對端。 * @return 網絡數據的狀況 */ virtual int Peek() = 0; /** * @brief 讀取網絡數 * 讀取鏈接裏面的數據,返回讀取到的字節數,若是返回0表示沒有數據, * 若是buffer_length是0, 也會返回0, * @return 返回-1表示鏈接須要關閉(各類出錯也返回0) */ virtual int Read(char* ouput_buffer, int buffer_length) = 0; /** * @brief 把input_buffer裏的數據寫入網絡鏈接,返回寫入的字節數。 * @return 若是返回-1表示寫入出錯,須要關閉此鏈接。 */ virtual int Write(const char* input_buffer, int buffer_length) = 0; protected: Connector(){} };
對於通訊「協議」來講,其實包含了許許多多的含義。在衆多的需求中,我所定義的這個協議層,只但願完成四個最基本的能力:
除了以上三個功能,實際上但願在協議層處理的能力,還有不少,最典型的就是對象序列化的功能,還有壓縮、加密功能等等。我之因此沒有把對象序列化的能力放在 Protocol 中,緣由是對象序列化中的「對象」自己是一個業務邏輯關聯性很是強的概念。在 C++ 中,並無完整的「對象」模型,也缺少原生的反射支持,因此沒法很簡單的把代碼層次經過「對象」這個抽象概念劃分開來。可是我也設計了一個 ObjectProcessor ,把對象序列化的支持,以更上層的形式結合到框架中。這個 Processor 是能夠自定義對象序列化的方法,這樣開發者就能夠本身選擇任何「編碼、解碼」的能力,而不須要依靠底層的支持。
至於壓縮和加密這一類功能,確實是能夠放在 Protocol 層中實現,甚至能夠做爲一個抽象層次加入 Protocol ,可能只有一個 Protocol 層不足以支持這麼豐富的功能,須要好像 Apache Mina 這樣,設計一個「調用鏈」的模型。可是爲了簡單起見,我以爲在具體須要用到的地方,再額外添加 Protocol 的實現類就好,好比添加一個「帶壓縮功能的 TLV Protocol 類型」之類的。
消息自己被抽象成一個叫 Message 的類型,它擁有「服務名字」「會話ID」兩個消息頭字段,用以完成「分發」和「會話保持」功能。而消息體則被放在一個字節數組中,並記錄下字節數組的長度。
enum MessageType { TypeError, ///< 錯誤的協議 TypeRequest, ///< 請求類型,從客戶端發往服務器 TypeResponse, ///< 響應類型,服務器收到請求後返回 TypeNotice ///< 通知類型,服務器主動通知客戶端 }; ///@brief 通訊消息體的基類 ///基本上是一個 char[] 緩衝區 struct Message { public: static int MAX_MAESSAGE_LENGTH; static int MAX_HEADER_LENGTH; MessageType type; ///< 此消息體的類型(MessageType)信息 virtual ~Message(); virtual Message& operator=(const Message& right); /** * @brief 把數據拷貝進此包體緩衝區 */ void SetData(const char* input_ptr, int input_length); ///@brief 得到數據指針 inline char* GetData() const{ return data_; } ///@brief 得到數據長度 inline int GetDataLen() const{ return data_len_; } char* GetHeader() const; int GetHeaderLen() const; protected: Message(); Message(const Message& message); private: char* data_; // 包體內容緩衝區 int data_len_; // 包體長度 };
根據以前設計的「請求響應」和「通知」兩種通訊模式,須要設計出三種消息類型繼承於 Message,他們是:
Request 和 Response 兩個類,都有記錄序列號的 seq_id 字段,但 Notice 沒有。Protocol 類就是負責把一段 buffer 字節數組,轉換成 Message 的子類對象。因此須要針對三種 Message 的子類型都實現對應的 Encode() / Decode() 方法。
class Protocol { public: virtual ~Protocol() { } /** * @brief 把請求消息編碼成二進制數據 * 編碼,把msg編碼到buf裏面,返回寫入了多長的數據,若是超過了 len,則返回-1表示錯誤。 * 若是返回 0 ,表示不須要編碼,框架會直接從 msg 的緩衝區讀取數據發送。 * @param buf 目標數據緩衝區 * @param offset 目標偏移量 * @param len 目標數據長度 * @param msg 輸入消息對象 * @return 編碼完成所用的字節數,若是 < 0 表示出錯 */ virtual int Encode(char* buf, int offset, int len, const Request& msg) = 0; /** * 編碼,把msg編碼到buf裏面,返回寫入了多長的數據,若是超過了 len,則返回-1表示錯誤。 * 若是返回 0 ,表示不須要編碼,框架會直接從 msg 的緩衝區讀取數據發送。 * @param buf 目標數據緩衝區 * @param offset 目標偏移量 * @param len 目標數據長度 * @param msg 輸入消息對象 * @return 編碼完成所用的字節數,若是 < 0 表示出錯 */ virtual int Encode(char* buf, int offset, int len, const Response& msg) = 0; /** * 編碼,把msg編碼到buf裏面,返回寫入了多長的數據,若是超過了 len,則返回-1表示錯誤。 * 若是返回 0 ,表示不須要編碼,框架會直接從 msg 的緩衝區讀取數據發送。 * @param buf 目標數據緩衝區 * @param offset 目標偏移量 * @param len 目標數據長度 * @param msg 輸入消息對象 * @return 編碼完成所用的字節數,若是 < 0 表示出錯 */ virtual int Encode(char* buf, int offset, int len, const Notice& msg) = 0; /** * 開始編碼,會返回即將解碼出來的消息類型,以便使用者構造合適的對象。 * 實際操做是在進行「分包」操做。 * @param buf 輸入緩衝區 * @param offset 輸入偏移量 * @param len 緩衝區長度 * @param msg_type 輸出參數,表示下一個消息的類型,只在返回值 > 0 的狀況下有效,不然都是 TypeError * @return 若是返回0表示分包未完成,須要繼續分包。若是返回-1表示協議包頭解析出錯。其餘返回值表示這個消息包占用的長度。 */ virtual int DecodeBegin(const char* buf, int offset, int len, MessageType* msg_type) = 0; /** * 解碼,把以前DecodeBegin()的buf數據解碼成具體消息對象。 * @param request 輸出參數,解碼對象會寫入此指針 * @return 返回0表示成功,-1表示失敗。 */ virtual int Decode(Request* request) = 0; /** * 解碼,把以前DecodeBegin()的buf數據解碼成具體消息對象。 * @param request 輸出參數,解碼對象會寫入此指針 * @return 返回0表示成功,-1表示失敗。 */ virtual int Decode(Response* response) = 0; /** * 解碼,把以前DecodeBegin()的buf數據解碼成具體消息對象。 * @param request 輸出參數,解碼對象會寫入此指針 * @return 返回0表示成功,-1表示失敗。 */ virtual int Decode(Notice* notice) = 0;protected: Protocol() { } };
這裏有一點須要注意,因爲 C++ 沒有內存垃圾蒐集和反射的能力,在解釋數據的時候,並不能一步就把一個 char[] 轉換成某個子類對象,而必須分紅兩步處理。
對於 Protocol 的具體實現子類,我首先實現了一個 LineProtocol ,是一個很是不嚴謹的,基於文本ASCII編碼的,用空格分隔字段,用回車分包的協議。用來測試這個框架是否可行。由於這樣能夠直接經過 telnet 工具,來測試協議的編解碼。而後我按照 TLV (Type Length Value)的方法設計了一個二進制的協議。大概的定義以下:
協議分包: [消息類型:int:2] [消息長度:int:4] [消息內容:bytes:消息長度]
消息類型取值:
包類型 | 字段 | 編碼細節 |
---|---|---|
Request | 服務名 | 字段:int:2[字符串內容:chars:消息長度] |
序列號 | 字段:int:2 | |
會話ID | 字段:int:2 | |
消息體 | 字段:int:2[字符串內容:chars:消息長度] | |
Response | 服務名 | 字段:int:2[字符串內容:chars:消息長度] |
序列號 | 字段:int:2 | |
會話ID | 字段:int:2 | |
消息體 | 字段:int:2[字符串內容:chars:消息長度] | |
Notice | 服務名 | 字段:int:2[字符串內容:chars:消息長度] |
消息體 | 字段:int:2[字符串內容:chars:消息長度] |
一個名爲 TlvProtocol 的類型完成對這個協議的實現。
處理器層是我設計用來對接具體業務邏輯的抽象層,它主要經過輸入參數 Request 和 Peer 來得到客戶端的輸入數據,而後經過 Server 類的 Reply()/Inform() 來返回 Response 和 Notice 消息。實際上 Transport 和 Protocol 的子類們,都屬於 net 模塊,而各類 Processor 和 Server/Client 這些功能類型,屬於另一個 processor 模塊。這樣設計的緣由,是但願全部 processor 模塊的代碼單向的依賴 net 模塊的代碼,但反過來不成立。
Processor 基類很是簡單,就是一個處理函數回調函數入口 Process()
:
///@brief 處理器基類,提供業務邏輯回調接口 class Processor { public: Processor(); virtual ~Processor(); /** * 初始化一個處理器,參數server爲業務邏輯提供了基本的能力接口。 */ virtual int Init(Server* server, Config* config = NULL); /** * 處理請求-響應類型包實現此方法,返回值是0表示成功,不然會被記錄在錯誤日誌中。 * 參數peer表示發來請求的對端狀況。其中 Server 對象的指針,能夠用來調用 Reply(), * Inform() 等方法。若是是監聽多個服務器,server 參數則會是不一樣的對象。 */ virtual int Process(const Request& request, const Peer& peer, Server* server); /** * 關閉清理處理器所佔用的資源 */ virtual int Close(); };
設計完 Transport/Protocol/Processor 三個通訊處理層次後,就須要一個組合這三個層次的代碼,那就是 Server 類。這個類在 Init() 的時候,須要上面三個類型的子類做爲參數,以組合成不一樣功能的服務器,如:
TlvProtocol tlv_protocol; // Type Length Value 格式分包協議,須要和客戶端一致 TcpTransport tcp_transport; // 使用 TCP 的通訊協議,默認監聽 0.0.0.0:6666 EchoProcessor echo_processor; // 業務邏輯處理器 Server server; // DenOS 的網絡服務器主對象 server.Init(&tcp_transport, &tlv_protocol, &echo_processor); // 組裝一個遊戲服務器對象:TLV 編碼、TCP 通訊和迴音服務
Server 類型還須要一個 Update() 函數,讓用戶進程的「主循環」不停的調用,用來驅動整個程序的運行。這個 Update() 函數的內容很是明確:
另外,Server 還須要處理一些額外的功能,好比維護一個會話緩存池(Session),提供發送 Response 和 Notice 消息的接口。當這些工做都完成後,整套系統已經能夠用來做爲一個比較「通用」的網絡消息服務器框架存在了。剩下的就是添加各類 Transport/Protocol/Processor 子類的工做。
class Server { public: Server(); virtual ~Server(); /** * 初始化服務器,須要選擇組裝你的通訊協議鏈 */ int Init(Transport* transport, Protocol* protocol, Processor* processor, Config* config = NULL); /** * 阻塞方法,進入主循環。 */ void Start(); /** * 須要循環調用驅動的方法。若是返回值是0表示空閒。其餘返回值表示處理過的任務數。 */ virtual int Update(); void ClosePeer(Peer* peer, bool is_clear = false); //關閉當個鏈接,is_clear 表示是否最終總體清理 /** * 關閉服務器 */ void Close(); /** * 對某個客戶端發送通知消息, * 參數peer表明要通知的對端。 */ int Inform(const Notice& notice, const Peer& peer); /** * 對某個 Session ID 對應的客戶端發送通知消息,返回 0 表示能夠發送,其餘值爲發送失敗。 * 此接口能支持斷線重連,只要客戶端已經成功鏈接,並使用舊的 Session ID,一樣有效。 */ int Inform(const Notice& notice, const std::string& session_id); /** * 對某個客戶端發來的Request發回迴應消息。 * 參數response的成員seqid必須正確填寫,才能正確迴應。 * 返回0成功,其它值(-1)表示失敗。 */ int Reply(Response* response, const Peer& peer); /** * 對某個 Session ID 對應的客戶端發送迴應消息。 * 參數 response 的 seqid 成員系統會自動填寫會話中記錄的數值。 * 此接口能支持斷線重連,只要客戶端已經成功鏈接,並使用舊的 Session ID,一樣有效。 * 返回0成功,其它值(-1)表示失敗。 */ int Reply(Response* response, const std::string& session_id); /** * 會話功能 */ Session* GetSession(const std::string& session_id = "", bool use_this_id = false); Session* GetSessionByNumId(int session_id = 0); bool IsExist(const std::string& session_id); };
有了 Server 類型,確定也須要有 Client 類型。而 Client 類型的設計和 Server 相似,但就不是使用 Transport 接口做爲傳輸層,而是 Connector 接口。不過 Protocol 的抽象層是徹底重用的。Client 並不須要 Processor 這種形式的回調,而是直接傳入接受數據消息就發起回調的接口對象 ClientCallback。
class ClientCallback { public: ClientCallback() { } virtual ~ClientCallback() { // Do nothing } /** * 當鏈接創建成功時回調此方法。 * @return 返回 -1 表示不接受這個鏈接,須要關閉掉此鏈接。 */ virtual int OnConnected() { return 0; } /** * 當網絡鏈接被關閉的時候,調用此方法 */ virtual void OnDisconnected() { // Do nothing } /** * 收到響應,或者請求超時,此方法會被調用。 * @param response 從服務器發來的迴應 * @return 若是返回非0值,服務器會打印一行錯誤日誌。 */ virtual int Callback(const Response& response) { return 0; } /** * 當請求發生錯誤,好比超時的時候,返回這個錯誤 * @param err_code 錯誤碼 */ virtual void OnError(int err_code){ WARN_LOG("The request is timeout, err_code: %d", err_code); } /** * 收到通知消息時,此方法會被調用 */ virtual int Callback(const Notice& notice) { return 0; } /** * 返回此對象是否應該被刪除。此方法會被在 Callback() 調用前調用。 * @return 若是返回 true,則會調用 delete 此對象的指針。 */ virtual bool ShouldBeRemoved() { return false; } }; class Client : public Updateable { public: Client(); virtual ~Client(); /** * 鏈接服務器 * @param connector 傳輸協議,如 TCP, UDP ... * @param protocol 分包協議,如 TLV, Line, TDR ... * @param notice_callback 收到通知後觸發的回調對象,若是傳輸協議有「鏈接概念」(如TCP/TCONND),創建、關閉鏈接時也會調用。 * @param config 配置文件對象,將讀取如下配置項目:MAX_TRANSACTIONS_OF_CLIENT 客戶端最大併發鏈接數; BUFFER_LENGTH_OF_CLIENT客戶端收包緩存;CLIENT_RESPONSE_TIMEOUT 客戶端響應等待超時時間。 * @return 返回 0 表示成功,其餘表示失敗 */ int Init(Connector* connector, Protocol* protocol, ClientCallback* notice_callback = NULL, Config* config = NULL); /** * callback 參數能夠爲 NULL,表示不須要回應,只是單純的發包便可。 */ virtual int SendRequest(Request* request, ClientCallback* callback = NULL); /** * 返回值表示有多少數據須要處理,返回-1爲出錯,須要關閉鏈接。返回0表示沒有數據須要處理。 */ virtual int Update(); virtual void OnExit(); void Close(); Connector* connector() ; ClientCallback* notice_callback() ; Protocol* protocol() ; };
至此,客戶端和服務器端基本設計完成,能夠直接經過編寫測試代碼,來檢查是否運行正常。
此文已由騰訊雲+社區在各渠道發佈,一切權利歸做者全部
獲取更多新鮮技術乾貨,能夠關注咱們騰訊雲技術社區-雲加社區官方號及知乎機構號