1を構築するためのチェスゲームサーバフレームワーク



ので、「より一般的なコード、それは、より多くの役に立たないコードで、」こんなに早くデザインで、私はそれが階層モデルを使用して、システム全体を構築することであるべきだと思います。一般の需要分割ゲームサーバとして、基本的な2つの層に分けることができる:

1.基礎となる特徴は、以下が挙げられる:等通信、永続性の非常に一般的な部分を、懸念は、性能、使いやすさ、拡張性、および他の指標です。

2.高レベルのロジック機能:別のゲームのための異なるデザインを持って、特定のゲームロジックを含みます。

基本的なニーズの分析:

機能要件

並行:並行タスクに対処する方法:すべてのサーバープログラムは、この基本的な質問が発生します。一般的に、マルチスレッド、非同期の二つの技術が存在します。マルチスレッド人間の思考の習慣に沿って、より多くのコーディングでのプログラミングが、問題の「ロック」をもたらしました。非同期モデルを非ブロック、状況はプログラムの実行が比較的簡単であるということですが、また、より完全なハードウェアの性能を活用するために、しかし、問題は、複雑なビジネスロジックのために、「コールバック」の形で多くのコードを記述する必要があり、それはそう非常に複雑な、非常に悪い可読性。両方のオプションが自分の長所と短所を持っているが、これらの二つの技術の組み合わせは、それぞれの長所を願っていますが、私は非同期に基づいて使用することを好むし、シングルスレッド、ノンブロッキング・このプログラムは、最も明確かつシンプルであるため、スケジューリングを。問題の「補正」を解決するために、我々は、コルーチンとして、別の抽象化層をその上に追加または改善するスレッドプールのような技術を追加することができます。

通信:サポートリクエストの応答モードと通知の通信モード(マルチターゲット考え放送通知)。ゲームは、ログオン、トランザクションをたくさん持っている、機能のバックパックなどを開いて、明確な要求と応答があります。そして、オンラインゲームの数が多い、複数のクライアントの位置、HPや他のものは、ネットワーク同期を通過する必要があり、実際には、それはコミュニケーションの「アクティブ通知」の一種です。

持続性:あなたがオブジェクトにアクセスすることができます。保存ゲーム形式は非常に複雑ですが、多くの場合、プレイヤーIDに基づくことができ、読み書きのそのインデックスを要求しています。プレイステーションなど、多くのゲーム機では、アーカイブする前に、すべての「ファイル」と同様であることができる方法は、メモリカードに保存されています。だから、永続的なゲームの最も基本的なニーズ、アクセスは、モデル値のキーです。もちろん、ゲームがこれらの追加要件はの基本的な共通基盤となるに含めるのに適していない、治療すべきであるなどのチャート、オークションハウス、などのより複雑なニーズを、持続しているだろう。

キャッシュは:リモート、分散オブジェクトキャッシュをサポートします。ゲームは非常に過酷な応答遅れを必要とするため、ゲームサービスは、サービス「状態、と」基本的に、基本的にはサーバプロセスのプロセスデータを格納するメモリを使用する必要があります。しかし、ゲームデータは、経験値、ゴールドコイン、HP、およびレベルの変化、設備およびその他の遅く、より高い値などの変化、低い値、しばしば高速で、この機能は、キャッシュモデルを使用することは非常に適しています処理。

コルーチン:コールバック関数の多数を回避するためにC ++コルーチン符号分割コードで書くことができます。これは、非同期の非常に便利な機能のためのコードで大幅にコードの開発効率と可読性を向上させることができます。具体的には、基礎となるIO機能の多くを関連するAPIのコルーチンを提供し、リラックス同期するために、同じAPIを使用するようになります。

スクリプト:最初のアイデアは、ビジネスロジックをサポートすることではLuaので書き込むことができます。ゲームは、名前の急速に変化するニーズのうち、この点でのサポートを提供できるようにするだけで、ビジネスロジックのスクリプト言語を記述しています。実際にはゲーム業界で使用されるスクリプトは非常に広いです。だから、スクリプトをサポートし、非常に重要な機能のゲームサーバーのフレームワークです。

その他の機能:タイマー、サーバー側のオブジェクト管理を。これらの機能は共通しているので、それはあまりにも長い間、選択したモデルが一般的で理解しやすいことができるように、フレームワークに含まれる必要があるが、成熟した番組をたくさん持っています。このようなオブジェクト管理として、私は達成するためのコンポーネントモデルと同様のユニティが使用されます。

非機能要件

柔軟性:交換可能なサポート通信プロトコル、代替永続装置(データベースなど);(例えばmemcachedの/ Redisのような)別のキャッシュ装置、スタティックライブラリとヘッダファイルではなく、ユーザーとして公開あまりにも多くの要件を行うためのコード。特に、異なるプロジェクト間のより複雑なゲームの動作環境は、異なるデータベース、異なる通信プロトコルを使用することができます。しかし、ゲーム自体は設計するオブジェクト・モデルに基づいてビジネスロジックの多くはあるので、これらの機能のすべての基礎となる抽象モデルを「オブジェクト」に基づくものを持っていることができるはずです。別のゲームの数を可能にするように、それは根本的な開発のセットに基づいています。

展開のしやすさ:柔軟な構成ファイルのサポート、参照コマンドライン引数、環境変数、データベース、ミドルウェア、メッセージキューや他の施設に依存することなく、起動する別のプロセスをサポートしています。一般的なゲームは、開発環境、クローズドベータ環境、外部テストや動作環境などの動作環境の少なくとも3セットを、持っています。ゲームの更新バージョンは、多くの場合、複数の環境を更新する必要があります。だから、どのように我々は、展開が非常に重要な課題となっている簡素化しようとすることができます。私は良いサーバー側のフレームワークは、サーバー側のプログラム、設定、非依存の事情のない独立したスタートを許可し、開発、テスト、デモ環境の急速な展開を満たすためにすべきだと思います。そして、それは、非常に簡単な設定ファイル、または異なるパラメータで、コマンドラインもテストまたは外部の動作環境の下でクラスタ化で開始することができます。

パフォーマンス:多くのゲームサーバは、プログラミングに非同期ノンブロッキング・アプローチを使用します。非同期ノンブロッキングは、このように、マルチスレッドのロックのような複雑な問題を避けて、サーバーのスループットを向上させるために、制御コードの実行順序複数のユーザー同時タスクの下で非常に明確にすることができ、非常に良いことができますので。だから私は、このフレームワークは、基本的な同時実行モデルとして、非同期ノンブロッキングに基づいていることを願っています。これはまた別の利点は、あなたが手動で特定のプロセスを制御し、マルチコアCPUのサーバを利用することができるということです持っています。もちろん、非同期コードの可読性は、多くのコールバックするので、それが読みにくくなりますが、幸い、私たちはしてこの問題を改善することができます「コルーチン。」

スケーラビリティ:SOAのクラスタ管理に似たサーバプロセスの状態管理、の間の通信をサポートしています。自動災害復旧および自動拡張は、実際には、キーポイントは、サービスプロセスの同期と管理の状態です。私はあなたがもはや各プロジェクト間クラスタ通信の世話をすることができないように、共通の階には、あなたが他の問題に取り組む、全て管理するための単一の集中管理モデルを通じて、すべてのサーバ間の通話を置くことができることを願っています。

明確にダウン要求したら、基本的な階層構造を設計することができます。

チェスゲーム開発チュートリアルシリーズ:ゲームサーバの骨格構造(A) - チェスの展望



最後に、このようなモジュールの全体的なアーキテクチャ:

チェスゲーム開発チュートリアルシリーズ:ゲームサーバの骨格構造(A) - チェスの展望



通信模块

对于通信模块来说,需要有灵活的可替换协议的能力,就必须按一定的层次进行进一步的划分。对于游戏来说,最底层的通信协议,一般会使用TCP和UDP这两种,在服务器之间,也会使用消息队列中间件一类通信软件。框架必须要有能同事支持这几通信协议的能力。故此设计了一个层次为:Transport

在协议层面,最基本的需求有“分包”“分发”“对象序列化”等几种需求。如果要支持“请求-响应”模式,还需要在协议中带上“序列号”的数据,以便对应“请求”和“响应”。另外,游戏通常都是一种“会话”式的应用,也就是一系列的请求,会被视为一次“会话”,这就需要协众需要有类似Session ID这种数据。为了满足这些需求,设计一个层次为:Protocol

拥有了以上两个层次,是可以完成最基本的协议层能力了。但是,我们往往希望业务数据的协议包,能自动化的成为编程中的对象,所以在处理消息体这里,需要一个可选的额外层次,用来把字节数组,转换成对象。所以我设计了一个特别的处理器:ObjectProcessor,去规范通信模块中对象序列化、反序列化的接口。

チェスゲーム開発チュートリアルシリーズ:ゲームサーバの骨格構造(A) - チェスの展望



Transport

此层次是为了统一各种不同的底层传输协议而设置的,最基本应该支持TCP和UDP这两种协议。对于通信协议的抽象,其实在很多底层库也做的非常好了,比如Linux的socket库,其读写API甚至可以和文件的读写通用。C#的Socket库在TCP和UDP之间,其api也几乎是完全一样的。但是由于作用游戏服务器,很多时候还会接入一些特别的“接入层”,比如一些代理服务器,或者一些消息中间件,这些API可是五花八门的。另外,在html5游戏(比如微信小游戏)和一些页游领域,还有用HTTP服务器作为游戏服务器的传统(如使用WebSocket协议),这样就需要一个完全不同的传输层了。

服务器传输层在异步模型下的基本使用序列,就是:

1.在主循环中,不断尝试读取有什么数据可读

2.如果上一步返回有数据到达了,则读取数据

3.读取数据处理后,需要发送数据,则向网络写入数据

根据上面三个特点,可以归纳出一个基本的接口:

  1. class Transport {
  2. public:
  3.    /**
  4.     * 初始化Transport对象,输入Config对象配置最大连接数等参数,可以是一个新建的Config对象。
  5.     */
  6.    virtual int Init(Config* config) = 0;
  7.    /**
  8.     * 检查是否有数据可以读取,返回可读的事件数。后续代码应该根据此返回值循环调用Read()提取数据。
  9.     * 参数fds用于返回出现事件的所有fd列表,len表示这个列表的最大长度。如果可用事件大于这个数字,并不影响后续可以Read()的次数。
  10.     * fds的内容,如果出现负数,表示有一个新的终端等待接入。
  11.     */
  12.    virtual int Peek(int* fds, int len) = 0;
  13.    /**
  14.     * 读取网络管道中的数据。数据放在输出参数 peer 的缓冲区中。
  15.     * @param peer 参数是产生事件的通信对端对象。
  16.     * @return 返回值为可读数据的长度,如果是 0 表示没有数据可以读,返回 -1 表示连接需要被关闭。
  17.     */
  18.    virtual int Read( Peer* peer) = 0;
  19.    /**
  20.     * 写入数据,output_buf, buf_len为想要写入的数据缓冲区,output_peer为目标队端,
  21.     * 返回值表示成功写入了的数据长度。-1表示写入出错。
  22.     */
  23.    virtual int Write(const char* output_buf, int buf_len, const Peer& output_peer) = 0;
  24.    /**
  25.     * 关闭一个对端的连接
  26.     */
  27.    virtual void ClosePeer(const Peer& peer) = 0;
  28.    /**
  29.     * 关闭Transport对象。
  30.     */
  31.    virtual void Close() = 0;
  32. }

复制代码



在上面的定义中,可以看到需要有一个Peer类型。这个类型是为了代表通信的客户端(对端)对象。在一般的Linux系统中,一般我们用fd(File Description)来代表。但是因为在框架中,我们还需要为每个客户端建立接收数据的缓存区,以及记录通信地址等功能,所以在fd的基础上封装了一个这样的类型。这样也有利于把UDP通信以不同客户端的模型,进行封装。

  1. ///@brief 此类型负责存放连接过来的客户端信息和数据缓冲区
  2. class Peer {
  3. public:
  4.     int buf_size_;      ///< 缓冲区长度
  5.     char* const buffer_;///< 缓冲区起始地址
  6.     int produced_pos_;  ///< 填入了数据的长度
  7.     int consumed_pos_;  ///< 消耗了数据的长度
  8.     int GetFd() const;
  9.     void SetFd(int fd);    /// 获得本地地址
  10.     const struct sockaddr_in& GetLocalAddr() const;
  11.     void SetLocalAddr(const struct sockaddr_in& localAddr);    /// 获得远程地址
  12.     const struct sockaddr_in& GetRemoteAddr() const;
  13.     void SetRemoteAddr(const struct sockaddr_in& remoteAddr);
  14. private:
  15.     int fd_;                            ///< 收发数据用的fd
  16.     struct sockaddr_in remote_addr_;    ///< 对端地址
  17.     struct sockaddr_in local_addr_;     ///< 本端地址
  18. };

复制代码



游戏使用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本身就支持异步模型,所以其实现也不费太多功夫。

  1. /**
  2. * @brief 客户端使用的连接器类,代表传输协议,如 TCP 或 UDP
  3. */
  4. class Connector {
  5. public:    virtual ~Connector() {}
  6.     /**
  7.      * @brief 初始化建立连接等
  8.      * @param config 需要的配置
  9.      * @return 0 为成功
  10.      */
  11.     virtual int Init(Config* config) = 0;
  12.     /**
  13.      * @brief 关闭
  14.      */
  15.     virtual void Close() = 0;
  16.     /**
  17.      * @brief 读取是否有网络数据到来
  18.      * 读取有无数据到来,返回值为可读事件的数量,通常为1
  19.      * 如果为0表示没有数据可以读取。
  20.      * 如果返回 -1 表示出现网络错误,需要关闭此连接。
  21.      * 如果返回 -2 表示此连接成功连上对端。
  22.      * @return 网络数据的情况
  23.      */
  24.     virtual int Peek() = 0;
  25.     /**
  26.      * @brief 读取网络数
  27.      * 读取连接里面的数据,返回读取到的字节数,如果返回0表示没有数据,
  28.      * 如果buffer_length是0, 也会返回0,
  29.      * @return 返回-1表示连接需要关闭(各种出错也返回0)
  30.      */
  31.     virtual int Read(char* ouput_buffer, int buffer_length) = 0;
  32.     /**
  33.      * @brief 把input_buffer里的数据写入网络连接,返回写入的字节数。
  34.      * @return 如果返回-1表示写入出错,需要关闭此连接。
  35.      */
  36.    virtual int Write(const char* input_buffer, int buffer_length) = 0;
  37. protected:
  38.     Connector(){}
  39. };

复制代码



Protocol

对于通信“协议”来说,其实包含了许许多多的含义。在众多的需求中,我所定义的这个协议层,只希望完成四个最基本的能力:

分包:从流式传输层切分出一个个单独的数据单元,或者把多个“碎片”数据拼合成一个完整的数据单元的能力。一般解决这个问题,需要在协议头部添加一个“长度”字段。

请求响应对应:这对于异步非阻塞的通信模式下,是非常重要的功能。因为可能在一瞬间发出了很多个请求,而回应则会不分先后的到达。协议头部如果有一个不重复的“序列号”字段,就可以对应起哪个回应是属于哪个请求的。

会话保持:由于游戏的底层网络,可能会使用UDP或者HTTP这种非长连接的传输方式,所以要在逻辑上保持一个会话,就不能单纯的依靠传输层。加上我们都希望程序有抗网络抖动、断线重连的能力,所以保持会话成为一个常见的需求。我参考在Web服务领域的会话功能,设计了一个Session功能,在协议中加上Session ID这样的数据,就能比较简单的保持会话。

分发:游戏服务器必定会包含多个不同的业务逻辑,因此需要多种不同数据格式的协议包,为了把对应格式的数据转发。

除了以上三个功能,实际上希望在协议层处理的能力,还有很多,最典型的就是对象序列化的功能,还有压缩、加密功能等等。我之所以没有把对象序列化的能力放在Protocol中,原因是对象序列化中的“对象”本身是一个业务逻辑关联性非常强的概念。在C++中,并没有完整的“对象”模型,也缺乏原生的反射支持,所以无法很简单的把代码层次通过“对象”这个抽象概念划分开来。但是我也设计了一个ObjectProcessor,把对象序列化的支持,以更上层的形式结合到框架中。这个Processor是可以自定义对象序列化的方法,这样开发者就可以自己选择任何“编码、解码”的能力,而不需要依靠底层的支持。

至于压缩和加密这一类功能,确实是可以放在Protocol层中实现,甚至可以作为一个抽象层次加入Protocol,可能只有一个Protocol层不足以支持这么丰富的功能,需要好像Apache Mina这样,设计一个“调用链”的模型。但是为了简单起见,我觉得在具体需要用到的地方,再额外添加Protocol的实现类就好,比如添加一个“带压缩功能的TLV Protocol类型”之类的。

消息本身被抽象成一个叫Message的类型,它拥有“服务名字”“会话ID”两个消息头字段,用以完成“分发”和“会话保持”功能。而消息体则被放在一个字节数组中,并记录下字节数组的长度。

  1. enum MessageType {
  2.     TypeError, ///< 错误的协议
  3.     TypeRequest, ///< 请求类型,从客户端发往服务器
  4.     TypeResponse, ///< 响应类型,服务器收到请求后返回
  5.     TypeNotice  ///< 通知类型,服务器主动通知客户端
  6. };
  7. ///@brief 通信消息体的基类
  8. ///基本上是一个 char[] 缓冲区
  9. struct Message {
  10. public:
  11.     static int MAX_MAESSAGE_LENGTH;
  12.     static int MAX_HEADER_LENGTH;
  13.     MessageType type;  ///< 此消息体的类型(MessageType)信息
  14.     virtual ~Message();    virtual Message& operator=(const Message& right);
  15.     /**
  16.      * @brief 把数据拷贝进此包体缓冲区
  17.      */
  18.     void SetData(const char* input_ptr, int input_length);
  19.     ///@brief 获得数据指针
  20.     inline char* GetData() const{
  21.         return data_;
  22.     }
  23.      ///@brief 获得数据长度
  24.     inline int GetDataLen() const{
  25.         return data_len_;
  26.     }
  27.     char* GetHeader() const;
  28.     int GetHeaderLen() const;
  29. protected:
  30.     Message();
  31.     Message(const Message& message);
  32. private:
  33.     char* data_;                  // 包体内容缓冲区
  34.     int data_len_;                // 包体长度
  35. };

复制代码



根据之前设计的“请求响应”和“通知”两种通信模式,需要设计出三种消息类型继承于Message,他们是:

  • Request请求包
  • Response响应包
  • Notice通知包



Request和Response两个类,都有记录序列号的seq_id字段,但Notice没有。Protocol类就是负责把一段buffer字节数组,转换成Message的子类对象。所以需要针对三种Message的子类型都实现对应的Encode()/Decode()方法。

  1. class Protocol {
  2. public:
  3.     virtual ~Protocol() {
  4.     }
  5.     /**
  6.      * @brief 把请求消息编码成二进制数据
  7.      * 编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了 len,则返回-1表示错误。
  8.      * 如果返回 0 ,表示不需要编码,框架会直接从 msg 的缓冲区读取数据发送。
  9.      * @param buf 目标数据缓冲区
  10.      * @param offset 目标偏移量
  11.      * @param len 目标数据长度
  12.      * @param msg 输入消息对象
  13.      * @return 编码完成所用的字节数,如果 < 0 表示出错
  14.      */
  15.     virtual int Encode(char* buf, int offset, int len, const Request& msg) = 0;
  16.     /**
  17.      * 编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了 len,则返回-1表示错误。
  18.      * 如果返回 0 ,表示不需要编码,框架会直接从 msg 的缓冲区读取数据发送。
  19.      * @param buf 目标数据缓冲区
  20.      * @param offset 目标偏移量
  21.      * @param len 目标数据长度
  22.      * @param msg 输入消息对象
  23.      * @return 编码完成所用的字节数,如果 < 0 表示出错
  24.      */
  25.     virtual int Encode(char* buf, int offset, int len, const Response& msg) = 0;
  26.     /**
  27.      * 编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了 len,则返回-1表示错误。
  28.      * 如果返回 0 ,表示不需要编码,框架会直接从 msg 的缓冲区读取数据发送。
  29.      * @param buf 目标数据缓冲区
  30.      * @param offset 目标偏移量
  31.      * @param len 目标数据长度
  32.      * @param msg 输入消息对象
  33.      * @return 编码完成所用的字节数,如果 < 0 表示出错
  34.      */
  35.     virtual int Encode(char* buf, int offset, int len, const Notice& msg) = 0;
  36.     /**
  37.      * 开始编码,会返回即将解码出来的消息类型,以便使用者构造合适的对象。
  38.      * 实际操作是在进行“分包”操作。
  39.      * @param buf 输入缓冲区
  40.      * @param offset 输入偏移量
  41.      * @param len 缓冲区长度
  42.      * @param msg_type 输出参数,表示下一个消息的类型,只在返回值 > 0 的情况下有效,否则都是 TypeError
  43.      * @return 如果返回0表示分包未完成,需要继续分包。如果返回-1表示协议包头解析出错。其他返回值表示这个消息包占用的长度。
  44.      */
  45.     virtual int DecodeBegin(const char* buf, int offset, int len,
  46.                             MessageType* msg_type) = 0;
  47.     /**
  48.      * 解码,把之前DecodeBegin()的buf数据解码成具体消息对象。
  49.      * @param request 输出参数,解码对象会写入此指针
  50.      * @return 返回0表示成功,-1表示失败。
  51.      */
  52.     virtual int Decode(Request* request) = 0;
  53.     /**
  54.      * 解码,把之前DecodeBegin()的buf数据解码成具体消息对象。
  55.      * @param request 输出参数,解码对象会写入此指针
  56.      * @return 返回0表示成功,-1表示失败。
  57.      */
  58.     virtual int Decode(Response* response) = 0;
  59.     /**
  60.      * 解码,把之前DecodeBegin()的buf数据解码成具体消息对象。
  61.      * @param request 输出参数,解码对象会写入此指针
  62.      * @return 返回0表示成功,-1表示失败。
  63.      */
  64.     virtual int Decode(Notice* notice) = 0;protected:
  65.     Protocol() {
  66.     }
  67. };

复制代码



这里有一点需要注意,由于C++没有内存垃圾搜集和反射的能力,在解释数据的时候,并不能一步就把一个char[]转换成某个子类对象,而必须分成两步处理。

先通过DecodeBegin()来返回,将要解码的数据是属于哪个子类型的。同时完成分包的工作,通过返回值来告知调用者,是否已经完整的收到一个包。

调用对应类型为参数的Decode()来具体把数据写入对应的输出变量。

对于Protocol的具体实现子类,我首先实现了一个LineProtocol,是一个非常不严谨的,基于文本ASCII编码的,用空格分隔字段,用回车分包的协议。用来测试这个框架是否可行。因为这样可以直接通过telnet工具,来测试协议的编解码。然后我按照TLV(Type Length Value)的方法设计了一个二进制的协议。大概的定义如下:

协议分包:[消息类型:int:2][消息长度:int:4][消息内容:bytes:消息长度]

消息类型取值:

  • 0x00 Error
  • 0x01 Request
  • 0x02 Response
  • 0x03 Notice



チェスゲーム開発チュートリアルシリーズ:ゲームサーバの骨格構造(A) - チェスの展望



一个名为TlvProtocol的类型完成对这个协议的实现。

Processor

处理器层是我设计用来对接具体业务逻辑的抽象层,它主要通过输入参数Request和Peer来获得客户端的输入数据,然后通过Server类的Reply()/Inform()来返回Response和Notice消息。实际上Transport和Protocol的子类们,都属于net模块,而各种Processor和Server/Client这些功能类型,属于另外一个processor模块。这样设计的原因,是希望所有processor模块的代码单向的依赖net模块的代码,但反过来不成立。

Processor基类非常简单,就是一个处理函数回调函数入口Process():

  1. ///@brief 处理器基类,提供业务逻辑回调接口
  2. class Processor {
  3. public:
  4.     Processor();
  5.     virtual ~Processor();
  6.     /**
  7.      * 初始化一个处理器,参数server为业务逻辑提供了基本的能力接口。
  8.      */
  9.     virtual int Init(Server* server, Config* config = NULL);
  10.     /**
  11.      * 处理请求-响应类型包实现此方法,返回值是0表示成功,否则会被记录在错误日志中。
  12.      * 参数peer表示发来请求的对端情况。其中 Server 对象的指针,可以用来调用 Reply(),
  13.      * Inform() 等方法。如果是监听多个服务器,server 参数则会是不同的对象。
  14.      */
  15.     virtual int Process(const Request& request, const Peer& peer,
  16.                         Server* server);
  17.     /**
  18.      * 关闭清理处理器所占用的资源
  19.      */
  20.     virtual int Close();
  21. };

复制代码



设计完Transport/Protocol/Processor三个通信处理层次后,就需要一个组合这三个层次的代码,那就是Server类。这个类在Init()的时候,需要上面三个类型的子类作为参数,以组合成不同功能的服务器,如:

  1. TlvProtocol tlv_protocol;   //  Type Length Value 格式分包协议,需要和客户端一致
  2. TcpTransport tcp_transport; // 使用 TCP 的通信协议,默认监听 0.0.0.0:6666
  3. EchoProcessor echo_processor;   // 业务逻辑处理器
  4. Server server;  // DenOS 的网络服务器主对象
  5. server.Init(&tcp_transport, &tlv_protocol, &echo_processor);    // 组装一个游戏服务器对象:TLV 编码、TCP 通信和回音服务

复制代码



Server类型还需要一个Update()函数,让用户进程的“主循环”不停的调用,用来驱动整个程序的运行。这个Update()函数的内容非常明确:

1.检查网络是否有数据需要处理(通过Transport对象)
2.有数据的话就进行解码处理(通过Protocol对象)
3.解码成功后进行业务逻辑的分发调用(通过Processor对象)

另外,Server还需要处理一些额外的功能,比如维护一个会话缓存池(Session),提供发送Response和Notice消息的接口。当这些工作都完成后,整套系统已经可以用来作为一个比较“通用”的网络消息服务器框架存在了。剩下的就是添加各种Transport/Protocol/Processor子类的工作。

  1. class Server {
  2. public:
  3.     Server();
  4.     virtual ~Server();
  5.     /**
  6.      * 初始化服务器,需要选择组装你的通信协议链
  7.      */
  8.     int Init(Transport* transport, Protocol* protocol, Processor* processor, Config* config = NULL);
  9.     /**
  10.      * 阻塞方法,进入主循环。
  11.      */
  12.     void Start();
  13.     /**
  14.      * 需要循环调用驱动的方法。如果返回值是0表示空闲。其他返回值表示处理过的任务数。
  15.      */
  16.     virtual int Update();
  17.     void ClosePeer(Peer* peer, bool is_clear = false); //关闭当个连接,is_clear 表示是否最终整体清理
  18.     /**
  19.      * 关闭服务器
  20.      */
  21.     void Close();
  22.     /**
  23.      * 对某个客户端发送通知消息,
  24.      * 参数peer代表要通知的对端。
  25.      */
  26.     int Inform(const Notice& notice, const Peer& peer);
  27.     /**
  28.      * 对某个  Session ID 对应的客户端发送通知消息,返回 0 表示可以发送,其他值为发送失败。
  29.      * 此接口能支持断线重连,只要客户端已经成功连接,并使用旧的 Session ID,同样有效。
  30.      */
  31.     int Inform(const Notice& notice, const std::string& session_id);
  32.     /**
  33.      * 对某个客户端发来的Request发回回应消息。
  34.      * 参数response的成员seqid必须正确填写,才能正确回应。
  35.      * 返回0成功,其它值(-1)表示失败。
  36.      */
  37.     int Reply(Response* response, const Peer& peer);
  38.     /**
  39.      * 对某个 Session ID 对应的客户端发送回应消息。
  40.      * 参数 response 的 seqid 成员系统会自动填写会话中记录的数值。
  41.      * 此接口能支持断线重连,只要客户端已经成功连接,并使用旧的 Session ID,同样有效。
  42.      * 返回0成功,其它值(-1)表示失败。
  43.      */
  44.     int Reply(Response* response, const std::string& session_id);
  45.     /**
  46.      * 会话功能
  47.      */
  48.     Session* GetSession(const std::string& session_id = “”, bool use_this_id = false);
  49.     Session* GetSessionByNumId(int session_id = 0);
  50.     bool IsExist(const std::string& session_id);
  51. };

复制代码



有了Server类型,肯定也需要有Client类型。而Client类型的设计和Server类似,但就不是使用Transport接口作为传输层,而是Connector接口。不过Protocol的抽象层是完全重用的。Client并不需要Processor这种形式的回调,而是直接传入接受数据消息就发起回调的接口对象ClientCallback。

  1. class ClientCallback {
  2. public:
  3.     ClientCallback() {
  4.     }
  5.     virtual ~ClientCallback() {
  6.          // Do nothing
  7.     }
  8.     /**
  9.      *  当连接建立成功时回调此方法。
  10.      * @return 返回 -1 表示不接受这个连接,需要关闭掉此连接。
  11.      */
  12.     virtual int OnConnected() {
  13.         return 0;
  14.     }
  15.     /**
  16.      * 当网络连接被关闭的时候,调用此方法
  17.      */
  18.     virtual void OnDisconnected() {        // Do nothing
  19.     }
  20.     /**
  21.      * 收到响应,或者请求超时,此方法会被调用。
  22.      * @param response 从服务器发来的回应
  23.      * @return 如果返回非0值,服务器会打印一行错误日志。
  24.      */
  25.     virtual int Callback(const Response& response) {
  26.         return 0;
  27.     }
  28.     /**
  29.      * 当请求发生错误,比如超时的时候,返回这个错误
  30.      * @param err_code 错误码
  31.      */
  32.     virtual void OnError(int err_code){
  33.         WARN_LOG(“The request is timeout, err_code: %d”, err_code);
  34.     }
  35.     /**
  36.      * 收到通知消息时,此方法会被调用
  37.      */
  38.     virtual int Callback(const Notice& notice) {
  39.         return 0;
  40.     }
  41.     /**
  42.      * 返回此对象是否应该被删除。此方法会被在 Callback() 调用前调用。
  43.      * @return 如果返回 true,则会调用 delete 此对象的指针。
  44.      */
  45.     virtual bool ShouldBeRemoved() {
  46.         return false;
  47.     }
  48. };
  49. class Client : public Updateable {
  50. public:
  51.     Client();    virtual ~Client();
  52.      /**
  53.      * 连接服务器
  54.      * @param connector 传输协议,如 TCP, UDP …
  55.      * @param protocol 分包协议,如 TLV, Line, TDR …
  56.      *コールバックオブジェクト@paramはnotice_callback(例えばTCP / TCONNDとして)「接続コンセプト」のトランスポート・プロトコルは、確立が、接続をクローズする際に呼び出された場合、通知を受信した後にトリガ。
  57.      * @Param設定プロファイルオブジェクトは、次の設定項目を読み出す:同時クライアント接続のMAX_TRANSACTIONS_OF_CLIENT最大数; BUFFER_LENGTH_OF_CLIENTクライアントは、パケットバッファを受信するステップと、タイムアウトを待機CLIENT_RESPONSE_TIMEOUTクライアント応答。
  58.      * @Returnは、障害のために、成功のために0を返します。他の
  59.      * /
  60.     int型のInit(コネクタ*コネクタ、プロトコル*プロトコル、
  61.              ClientCallback * notice_callback = NULL、コンフィグ*設定= NULL);
  62.     / **
  63.      *コールバックパラメータはNULLにすることができ、それは何の応答は、単純な契約はしないことを示します。
  64.      * /
  65.     仮想int型のsendRequest(リクエスト*リクエスト、ClientCallback *コールバック= NULL);
  66.     / **
  67.      *戻り値はエラーが-1、接続を閉じる必要があり、処理すべきデータの数を示します。戻り値0はデータが処理されていないことを示しています。
  68.      * /
  69.     仮想int型の更新();
  70.     仮想空フォーカス喪失時();
  71.     空クローズ();
  72.     コネクタ*コネクタ();
  73.     ClientCallback * notice_callback();
  74.     プロトコル*プロトコル();
  75. }。

コードをコピー



これまでのところ、ボードゲームを開発し、クライアントとサーバー側の基本設計が完了すると、あなたが直接、通常の動作を確認するためにテストコードを書くことができます。

おすすめ

転載: www.cnblogs.com/upupup02/p/10979045.html