This article is published by the Cloud + community

Author: Han Wei

preface

I haven’t written a technical article for almost a year, because I’ve been working on some specific game projects this year. These new projects are closer to the way indie games are developed. I felt that the company’s “ancestral” server framework technology was not a good fit, so I wrote a game server-side framework from scratch in order to achieve greater development efficiency and flexibility. Now the project is nearly online, I would like to summarize the design and implementation process of such a game server framework.

The basic running environment of this framework is Linux and written in C++. In order to run in a variety of environments and use, so the use of GCC 4.8, the “ancient” compiler, to the C99 specification development.

demand

Since “the more generic the code, the more useless the code”, FROM the beginning of the design, I decided to use a layered pattern to build the entire system. According to the general requirements of the game server, the most basic can be divided into two layers:

  1. Underlying basic functionality: includes communication, persistence, and other very general parts, focusing on performance, ease of use, scalability, and other metrics.
  2. High-level logic features: This includes specific game logic, which can be designed differently for different games.

I wanted to have a basically complete framework of “underlying basic features” that could be reused for multiple different games. Since the goal is to develop a game server framework suitable for indie game development. Therefore, the most basic demand analysis is as follows:

Functional requirements

  1. Concurrency: All server programs encounter the basic problem of how to handle concurrency. Generally speaking, there will be multi-threading, asynchronous two technologies. Multithreaded programming is more consistent with human thinking habits in coding, but it brings the problem of “locking”. The asynchronous non-blocking model, its program execution is relatively simple, and can make full use of the hardware performance, but the problem is that a lot of code needs to be written in the form of “callback”, for complex business logic, is very tedious, very poor readability. Although the two schemes have their own advantages and disadvantages, and some people combine the two technologies to make the best of each, I prefer to use the asynchronous, single-threaded, non-blocking scheduling mode as the foundation, because this scheme is the most clear and simple. To solve the “callback” problem, we can add other layers of abstraction on top of it, such as coroutines or techniques such as thread pools.
  2. Communication: Supports request-response mode as well as notification mode communication (broadcast as a multi-target notification). There are many features for logging in, buying and selling, opening backpacks and so on, all of which are explicitly requested and responded to. In a large number of online games, the location of multiple clients, HP and other things need to be synchronized through the network, which is actually a “proactive notification” communication mode.
  3. Persistence: Objects can be accessed. The format of the game archive is very complex, but its index needs to be read and written based on the player ID. On many game consoles such as the PlayStation, saves used to be stored on memory cards as “files”. So the most basic requirement for game persistence is a key-value access model. Of course, there are also more complex persistence requirements, such as leaderboards, auction houses, etc., that should be treated as extra requirements and should not be included in a basic generic low-level.
  4. Cache: Supports remote and distributed object caching. Game services are basically “stateful” services, because games require very demanding response latency and basically use server process memory to store process data. However, the faster the game changes, the lower the value of the data, such as experience, gold, HP, while the slower the change of levels, equipment, etc., the higher the value, which is very suitable for a cache model.
  5. Coroutines: you can write coroutine code in C++ without having to use a lot of callbacks to split code. This is a very useful feature for asynchronous code, which can greatly improve code readability and development efficiency. In particular, many low-level IO functions are provided with a coroutine API, which is as easy and comfortable to use as a synchronous API.
  6. Scripting: The initial idea is to support writing business logic in Lua. Game requirements are notoriously fast-changing, and writing business logic in a scripting language provides just that. In fact, scripting is widely used in the game industry. So supporting scripting is also an important capability for a game server framework.
  7. Other functions: including timers, object management on the server side, etc. These features are common enough that they need to be included in the framework, but there are plenty of mature solutions out there, so just pick a common, easy-to-understand model. For example, object management, I would use a component model similar to Unity.

Nonfunctional requirements

  1. Flexibility: support for replaceable communication protocols; Replaceable persistence devices (such as databases); Replaceable cache devices (e.g. Memcached/Redis); Distribute as static libraries and header files without making too many demands on consumer code. The operating environment of a game is complex, especially between different projects, which may use different databases and different communication protocols. But a lot of the business logic of the game itself is designed based on the object model, so there should be a layer of model that abstracts all of these underlying functions based on “objects”. This allows many different games to be developed on the same underlying set.
  2. Deployment convenience: Supports flexible reference of configuration files, command line parameters, and environment variables. Support individual process startup without relying on database, message queue middleware, and other facilities. The average game will have at least three operating environments, including a development environment, an internal beta environment, and an external beta or operational environment. Updating a version of a game often requires updating multiple environments. So how to simplify deployment as much as possible becomes an important issue. In my opinion, a good server-side framework should be able to enable this server-side application to start independently, without configuration or dependencies, for rapid deployment in a development, test, or demo environment. It can be easily launched in clustered external test or operational environments by configuration files or command-line parameters.
  3. Performance: Many game servers are programmed in an asynchronous, non-blocking manner. Asynchronous non-blocking can greatly improve the throughput of the server, and can clearly control the code execution order of multiple user tasks at the same time, thus avoiding complex problems such as multi-threaded locking. So I also want the framework to use asynchronous non-blocking as the basic concurrency model. This has the added benefit of being able to manually control specific processes, taking full advantage of the performance of multi-core CPU servers. Of course, asynchronous code readability can be difficult to read due to the large number of callback functions, but fortunately we can also use “coroutines” to improve this problem.
  4. Scalability: Supports communication between servers, process state management, and soA-like cluster management. The key point of automatic Dr And automatic capacity expansion is status synchronization and management of service processes. I would like a common layer where all inter-server calls are managed through a unified centralized management model so that inter-cluster communication, addressing, and so on are no longer a concern for each project.

Once the requirements are defined, the basic hierarchy can also be designed:

level function The constraint
Logic layer Implement more specific business logic You can call all the underlying code, but you should rely primarily on the interface layer
Implementation layer Realization of various specific communication protocols, storage devices and other functions Satisfy the interface layer of the lower layer to do the implementation, prohibit the mutual call between the same layer
The interface layer The basic usage of each module is defined to isolate the specific implementation and design, thus providing interchangeable capabilities Code in this layer can call each other, but upper-layer code cannot be called
Tools layer Provides common C++ library functionality, such as log/json/ini/ date/time/string handling, etc Other layers of code should not be called, nor should other modules in the same layer be called
Third-party libraries Provides off-the-shelf functionality such as Redis/TCAPlus or other functions that are in the same position as a “tool layer” Other layers of code should not be called or even modified

Finally, the overall architectural modules are similar:

instructions communication The processor The cache persistence
Function implementation TcpUdpKcpTlvLine JsonHandlerObjectProcessor SessionLocalCacheRedisMapRamMapZooKeeperMap FileDataStoreRedisDataStroe
The interface definition TransferProtocol ServerClientProcessor DataMapSerializable DataStore
Tool library ConfigLOGJSONCoroutine

Communication module

For communication module, it needs the ability of flexible replacement protocol, so it must be further divided according to a certain level. For the game, the lowest level of communication protocols, generally use TCP and UDP, between servers, will also use message queue middleware and other communication software. The framework must have the capability to support these communication protocols. Therefore, a level is designed as Transport

At the protocol level, the most basic requirements include “subcontracting”, “distribution”, “object serialization” and so on. If you want to support the request-response mode, you also need to carry the data of the serial number in the protocol to correspond to the request and response. In addition, games are often a “Session” application, i.e. a series of requests that are treated as a “Session”, which requires co-sponsors to have data such as Session IDS. To meet these requirements, design a layer called: Protocol

With these two layers, it is possible to complete the most basic protocol-layer capabilities. However, we tend to expect business data packets to automatically become objects in programming, so we need an optional extra layer to convert byte arrays into objects when dealing with message bodies. Therefore, I designed a special processor, ObjectProcessor, to standardize the interface of object serialization and deserialization in the communication module.

The input level function The output
data Transport communication buffer
buffer Protocol The subcontract Message
Message Processor distribution object
object Processing module To deal with The business logic

Transport

This layer is designed to unify different underlying transport protocols. TCP and UDP are supported. For the communication protocol abstraction, in fact, in many low-level libraries also do very well, such as Linux socket library, its read and write API can even be common with the file read and write. C#’s Socket library falls between TCP and UDP, and its API is almost identical. However, because of the role of game servers, many applications also have access to special “access layers”, such as proxy servers, or message-oriented middleware, with various apis. In addition, in HTML5 games (such as wechat mini games) and some page games, there is also a tradition of using HTTP servers as game servers (such as using the WebSocket protocol), which requires a completely different transport layer.

The basic usage sequence of the server transport layer under the asynchronous model is:

  1. In the main loop, what data is there to read by constantly trying to read
  2. If any data arrived from the previous step, the data is read
  3. After data processing, data needs to be sent and written to the network

According to the above three characteristics, a basic interface can be concluded:

class Transport {
public:   
   /** * To initialize the Transport object, enter the Config object to configure the maximum number of connections and other parameters, which can be a new Config object. * /   
   virtual int Init(Config* config) = 0;

   /** * Check if any data can be read, return the number of events that can be read. Subsequent code should loop through Read() to extract the data based on this return value. * The FDS argument returns a list of all FD occurrences, len being the maximum length of the list. If the number of available events is greater than this, it does not affect the number of subsequent reads that can be made (). * The content of FDS, if there is a negative number, it indicates that a new terminal is waiting for access. * /
   virtual int Peek(int* fds, int len) = 0;

   /** * Read the data in the network pipe. The data is placed in the buffer of the output parameter peer. The @param peer argument is the communication peer object that generates the event. * @return Returns the length of the data that can be read. If 0 means there is no data to read and -1 means the connection needs to be closed. * /
   virtual int Read( Peer* peer) = 0;

   /** * Write data, output_buf, buf_len is the data buffer to be written, output_peer is the target queue end, * returns the length of data successfully written. -1 indicates a write error. * /
   virtual int Write(const char* output_buf, int buf_len, const Peer& output_peer) = 0;

   /** * closes a peer connection */
   virtual void ClosePeer(const Peer& peer) = 0;

   /** * Close the Transport object. * /
   virtual void Close() = 0;

}
Copy the code

In the above definition, you can see that there needs to be a Peer type. This type is intended to represent the communicating client (peer) object. In common Linux systems, we usually use fd (File Description) to represent. But because in the framework, we also need to establish for each client to receive data cache, as well as record communication address and other functions, so on the basis of FD encapsulated a type of such. This also helps encapsulate UDP traffic in different client models.

///@brief This type stores connected client information and data buffers
class Peer {
public:	
    int buf_size_;      ///< buffer length
    char* const buffer_;///< buffer start address
    int produced_pos_;  ///< fills in the length of the data
    int consumed_pos_;  ///< consumes the length of data

    int GetFd() const;
    void SetFd(int fd);    /// get the local address
    const struct sockaddr_in& GetLocalAddr() const;
    void SetLocalAddr(const struct sockaddr_in& localAddr);    // get the remote address

    const struct sockaddr_in& GetRemoteAddr() const;
    void SetRemoteAddr(const struct sockaddr_in& remoteAddr);

private:
    int fd_;                            ///< fd for sending and receiving data
    struct sockaddr_in remote_addr_;    ///< peer address
    struct sockaddr_in local_addr_;     ///< local address
};
Copy the code

The game uses UDP protocol features: generally speaking, UDP is connect-free, but for the game, is definitely need to have a clear client, so can not simply use a UDP socket FD to represent the client, which causes the upper code can not simply keep consistent between UDP and TCP. Therefore, the Peer abstraction layer is used here to approach the problem. This can also be used in cases where some kind of message queue middleware is used, because it is possible that the middleware, too, is multiplexing a FD, and may not even have been developed using the FD API.

The above definition of Transport is very easy for TCP implementers to implement. However, UDP implementers need to consider how to exploit Peer, especially the peer-.fd_ data. When I implement, I use a set of virtual FD mechanism, through the IPv4 address of a client to the corresponding Map of INT, to provide the function of differentiating clients for the upper layer. On Linux, these IO can be implemented using the epoll library by reading the IO events in the Peek() function and filling in the socket call with Read()/Write().

In addition, to enable communication between servers, a type of Connector corresponding to Tansport needs to be designed: Connector. This abstract base class is used to make requests to the server in the client model. The design is similar to Transport. In addition to Connecotr for Linux, I also implemented code in C# so that clients developed in Unity can easily use it. Since.NET itself supports the asynchronous model, it doesn’t take much effort to implement.

/** * @Brief Indicates the connector class used by the client, which represents the transport protocol, such as TCP or UDP */
class Connector {

public:    virtual ~Connector() {}    
 
    /** * @brief Initializes and establishes connections * @param Config Requires configuration * @return 0 indicates success */
    virtual int Init(Config* config) = 0;

    /** * @brief close */
    virtual void Close() = 0;

    /** * @brief Read whether network data is coming * Read whether data is coming. The return value is the number of readable events, usually 1 * If 0, there is no data to read. * If -1 is returned, a network error occurs and the connection needs to be closed. * If -2 is returned, the connection is successful. * @return Network data */
    virtual int Peek() = 0;

    If buffer_length is 0, it also returns 0. If buffer_length is 0, it also returns 0. * @return -1, the connection needs to be closed
    virtual int Read(char* ouput_buffer, int buffer_length) = 0;

    /** * @brief Writes the data in the input_buffer to the network connection. Returns the number of bytes written. * @return If -1 is returned, the write error occurs and the connection needs to be closed. * /
   virtual int Write(const char* input_buffer, int buffer_length) = 0;

protected:
    Connector(){}
};
Copy the code

Protocol

For communication “protocols”, there are many meanings. Among the many requirements, the protocol layer I have defined is expected to accomplish only four basic capabilities:

  1. Subcontracting: The ability to slice individual data units from the streaming layer, or to assemble multiple “fragments” of data into a complete data unit. This is usually resolved by adding a “length” field to the protocol header.
  2. Request response correspondence: This is an important feature for asynchronous, non-blocking communication. Many requests may be made at once, and responses will arrive in no particular order. A unique sequence number field in the protocol header corresponds to which response belongs to which request.
  3. Session persistence: Since the underlying network of the game may use non-long-link transport such as UDP or HTTP, logically maintaining a session cannot rely solely on the transport layer. Coupled with the fact that we all want our programs to be resistant to network jitter and reconnection, maintaining a session becomes a common requirement. Referring to the Session function in the field of Web services, I designed a Session function, adding data such as Session ID in the protocol, it can be relatively simple to maintain the Session.
  4. Distribution: The game server must contain many different business logic and therefore requires multiple protocol packages in different data formats in order to forward the corresponding format of data.

In addition to the above three functions, in fact, I hope to deal with many capabilities in the protocol layer, the most typical is the function of object serialization, as well as compression, encryption and so on. The reason I didn’t include the ability to serialize objects in Protocol is that “object” in object serialization is itself a very business-logical concept. In C++, there is no complete “object” model, and there is no native reflection support, so it is not easy to separate code layers by the abstract concept of “object”. But I also designed an ObjectProcessor that incorporated object serialization support into the framework at a higher level. This Processor is a method for customizing object serialization so that developers can choose any “encode and decode” capabilities without relying on the underlying support.

As for functions such as compression and encryption, it is true that they can be implemented in the Protocol layer, or even added to the Protocol as a layer of abstraction. Perhaps a Protocol layer is not enough to support such rich functions. Design a “call chain” model. But for the sake of simplicity, I think it would be nice to add additional Protocol implementation classes where needed, such as a “TLV Protocol type with compression” or something like that.

The Message itself is abstracted into a type called Message, which has two header fields, “service name” and “session ID”, for “dispatch” and “session persistence”. The body of the message is placed in a byte array and the length of the byte array is recorded.

enum MessageType {
    TypeError.///< wrong protocol
    TypeRequest, ///< request type, sent from client to server
    TypeResponse, ///< Response type, the server returns after receiving the request
    TypeNotice  ///< notification type, the server actively notifies the client
};

///@brief Base class of the communication message body
/// is basically a char[] buffer
struct Message {
public:
    static int MAX_MAESSAGE_LENGTH;
    static int MAX_HEADER_LENGTH;
  
    MessageType type;  ///< MessageType information of this message body

    virtual ~Message();    virtual Message& operator=(const Message& right);

    /** * @brief copies the data into the package buffer */
    void SetData(const char* input_ptr, int input_length);

    ///@brief gets the data pointer
    inline char* GetData() const{
        return data_;
    }

     ///@brief to get the length of the data
    inline int GetDataLen() const{
        return data_len_;
    }
 
    char* GetHeader() const;
    int GetHeaderLen() const;

protected:
    Message();
    Message(const Message& message);
 
private:
    char* data_;                  // Package body content buffer
    int data_len_;                // Package length

};
Copy the code

According to the two communication modes of “request response” and “notification” previously designed, three Message types need to be designed to inherit from Message, which are:

  • Request Request packet
  • The Response Response packet
  • Notice Notice package

Both the Request and Response classes have seq_id, but Notice does not. The Protocol class is a subclass that converts an array of buffer bytes into Message. Therefore, corresponding Encode()/Decode() methods need to be implemented for all three Message subtypes.

class Protocol {

public:
    virtual ~Protocol() {
    }

    /** * @brief encodes the request message to binary *, encodes MSG to buF, returns how long the data was written, if it exceeds len, returns -1 indicating an error. * If 0 is returned, no encoding is required and the framework will read the data directly from the MSG buffer to send. * @param buf Target data buffer * @param offset Target offset * @param len Target data length * @param MSG Input message object * @return Number of bytes used to complete encoding. If < 0, error */
    virtual int Encode(char* buf, int offset, int len, const Request& msg) = 0;

    /** * code MSG to buF and return how long data was written. If len is exceeded, return -1 indicating an error. * If 0 is returned, no encoding is required and the framework will read the data directly from the MSG buffer to send. * @param buf Target data buffer * @param offset Target offset * @param len Target data length * @param MSG Input message object * @return Number of bytes used to complete encoding. If < 0, error */
    virtual int Encode(char* buf, int offset, int len, const Response& msg) = 0;

    /** * code MSG to buF and return how long data was written. If len is exceeded, return -1 indicating an error. * If 0 is returned, no encoding is required and the framework will read the data directly from the MSG buffer to send. * @param buf Target data buffer * @param offset Target offset * @param len Target data length * @param MSG Input message object * @return Number of bytes used to complete encoding. If < 0, error */
    virtual int Encode(char* buf, int offset, int len, const Notice& msg) = 0;

    /** * begins encoding, which returns the type of message to be decoded so that the user can construct an appropriate object. * The actual operation is a "subcontracting" operation. * @param buf input buffer * @param offset Input offset * @param len Buffer length * @param msg_type Output parameter, indicating the type of the next message, valid only if the return value is > 0, Otherwise, it's TypeError * @return. If 0 is returned, subcontracting is incomplete and you need to continue subcontracting. If -1 is returned, protocol header resolution fails. Other return values indicate the length occupied by this message packet. * /
    virtual int DecodeBegin(const char* buf, int offset, int len,
                            MessageType* msg_type) = 0;

    /** * Decodes the buF data previously DecodeBegin() into a concrete message object. * @param request outputs the argument to which the decoded object will write * @return returns 0 for success and -1 for failure. * /
    virtual int Decode(Request* request) = 0;

    /** * Decodes the buF data previously DecodeBegin() into a concrete message object. * @param request outputs the argument to which the decoded object will write * @return returns 0 for success and -1 for failure. * /
    virtual int Decode(Response* response) = 0;

    /** * Decodes the buF data previously DecodeBegin() into a concrete message object. * @param request outputs the argument to which the decoded object will write * @return returns 0 for success and -1 for failure. * /
    virtual int Decode(Notice* notice) = 0; protected: Protocol() { } };Copy the code

It is important to note that since C++ has no memory garbage collection and reflection capabilities, it is not possible to convert a char[] into a subclass object in one step when interpreting data, but must be processed in two steps.

  1. DecodeBegin() returns the subtype of the data to be decoded. At the same time, the subcontracted work is completed, and a return value is used to tell the caller whether a package has been received in its entirety.
  2. Decode() is called with the corresponding type of parameter to write data to the corresponding output variable.

For the specific subclass of Protocol, I first implemented a LineProtocol, which is a very loose, text-based ASCII encoding, separated by Spaces, subcontracted by carriage return Protocol. To test whether the framework works. In this way, the codec of the protocol can be tested directly through Telnet. Then I designed a binary protocol according to the TLV (Type Length Value) method. The general definition is as follows:

Subcontract: [message type :int:2] [Message length :int:4] [Message content :bytes: message length]

The message type can be:

  • 0x00 Error
  • 0x01 Request
  • 0x02 Response
  • 0x03 Notice
Package type field Coding details
Request The service name [field :int:2][Length :int:2]
The serial number [int:2][int:4]
The session ID [int:2][int:4]
The message body [field :int:2][Length :int:2]
Response The service name [field :int:2][Length :int:2]
The serial number [int:2][int:4]
The session ID [int:2][int:4]
The message body [field :int:2][Length :int:2]
Notice The service name [field :int:2][Length :int:2]
The message body [field :int:2][Length :int:2]

A type called TlvProtocol implements this protocol.

Processor

The processor layer is the abstract layer designed by me to interconnect with the specific business logic. It mainly obtains the input data of the client through input parameters Request and Peer, and then returns Response and Notice messages through Reply()/Inform() of the Server class. In fact, the Transport and Protocol subclasses belong to the NET module, while the various Processor and Server/Client function types belong to a separate Processor module. The reason for this design is that all the processor code is expected to be unidirectionally dependent on the NET code, but the reverse is not true.

The Processor base class is very simple. It is just a function that handles the callback entry Process() :

///@brief Processor base class, which provides the business logic callback interface

class Processor {

public:
    Processor();
    virtual ~Processor();
 
    /** * Initializes a processor. The server argument provides the basic capability interface for the business logic. * /
    virtual int Init(Server* server, Config* config = NULL);

    /** * This method is implemented with a return value of 0 indicating success, otherwise it is logged in the error log. * The peer parameter indicates the peer that sends the request. The pointer to the Server object can be used to call methods like Reply(), * Inform(), etc. If you are listening on multiple servers, the server parameter will be a different object. * /
    virtual int Process(const Request& request, const Peer& peer,
                        Server* server);

    /** * Close the resource used to clean up the processor */
    virtual int Close();
};
Copy the code

Design the Transport/Protocol/Processor after three communication process level, requires a combination of the three levels of code, that is the Server class. This class requires subclasses of the above three types as arguments when Init() to compose servers with different functions, such as:

TlvProtocol tlv_protocol;   // Type Length Value Specifies a subcontract in the same format as the client
TcpTransport tcp_transport; // Use TCP communication protocol, default listening 0.0.0.0:6666
EchoProcessor echo_processor;   // Business logic processor
Server server;  // The network server main object of DenOS
server.Init(&tcp_transport, &tlv_protocol, &echo_processor);    // Assemble a game server object: TLV encoding, TCP communication, and echo service
Copy the code

The Server type also requires an Update() function, which keeps the “main loop” of the user process running, driving the entire program. The Update() function is very explicit:

  1. Check whether the network has data to process (via Transport objects)
  2. Decode if data is present (via the Protocol object)
  3. Dispatch calls to business logic after successful decoding (via Processor object)

In addition, the Server needs to handle some additional functions, such as maintaining a Session cache pool and providing an interface for sending Response and Notice messages. When all the work is done, the whole system can be used as a relatively “general” network message server framework. That is left is to add a variety of Transport/Protocol/Processor subclasses of the work.

class Server {

public:
    Server();
    virtual ~Server();
 
    /** * To initialize the server, choose to assemble your communication protocol chain */
    int Init(Transport* transport, Protocol* protocol, Processor* processor, Config* config = NULL);

    /** * block the method to enter the main loop. * /
    void Start();

    /** * requires a loop to call the driven method. If the return value is 0, it is idle. Other return values indicate the number of tasks processed. * /
    virtual int Update();
    void ClosePeer(Peer* peer, bool is_clear = false); // Close the connection when is_clear indicates whether the whole is finally cleaned

    /** * Shut down the server */
    void Close();

    /** * Sends a notification message to a client. The * parameter peer indicates the peer to be notified. * /
    int Inform(const Notice& notice, const Peer& peer);

    /** * Sends a notification message to the client corresponding to a Session ID. If 0 is returned, the notification can be sent. If other values are returned, the notification fails to be sent. * This interface can support disconnection and reconnection, as long as the client has successfully connected and uses the old Session ID. * /
    int Inform(const Notice& notice, const std::string& session_id);

    /** * Returns a response message to a Request sent by a client. * The seqID member of the response parameter must be filled in correctly to respond correctly. * Returns 0 success, other values (-1) indicating failure. * /
    int Reply(Response* response, const Peer& peer);

    /** * Sends a response message to the client corresponding to a Session ID. * The seqID member of the response parameter is automatically filled in with the value recorded in the session. * This interface can support disconnection and reconnection, as long as the client has successfully connected and uses the old Session ID. * Returns 0 success, other values (-1) indicating failure. * /
    int Reply(Response* response, const std::string& session_id);

    /** * Session function */
    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);
   
};
Copy the code

With a Server type, you definitely need a Client type as well. The Client type design is similar to that of the Server, but instead of using the Transport interface as the Transport layer, it uses the Connector interface. However, the Protocol abstraction layer is completely reusable. The Client does not require a callback in the form of a Processor, but rather passes in the interface object ClientCallback that receives the data message and initiates the callback.

Class ClientCallback {public: ClientCallback() {} virtual ~ClientCallback() {// Do nothing} /** * Callback this method when the connection is successfully established. * @return Returns -1, indicating that the connection is not accepted and needs to be closed. */ virtual int OnConnected() { return 0; } /** * This method will be called when the network connection is closed */ virtual void OnDisconnected() {// Do nothing} /** * This method will be called when a response is received or the request times out. * @param Response Response sent from the server * @return If a non-zero value is returned, the server prints an error log. */ virtual int Callback(const Response& response) { return 0; } /** * When the request has an error, such as a timeout, * @param err_code Error code */ virtual void OnError(int err_code){WARN_LOG("The request is timeout, err_code: %d", err_code); } /** * this method is called when a notification message is received */ virtual int Callback(const Notice& Notice) {return 0; } /** * returns whether this object should be deleted. This method is called before Callback() is called. * @return If true, delete the pointer to this object is called. */ virtual bool ShouldBeRemoved() { return false; }}; class Client : public Updateable { public: Client(); virtual ~Client(); /** * @param connector transport protocol, such as TCP, UDP... * @param protocol Subcontracting protocols such as TLV, Line, TDR... * The callback object triggered by @param notice_callback when notified, which is also called when a connection is established or closed if the transport protocol has a "connection concept" (e.g. TCP/TCONND). * @param config config file object that reads the following configuration items: MAX_TRANSACTIONS_OF_CLIENT Maximum number of concurrent client connections; BUFFER_LENGTH_OF_CLIENT Client packet collection cache; CLIENT_RESPONSE_TIMEOUT Timeout time for waiting for a client response. * @return returns 0 indicating success, */ int Init(Connector* Connector, Protocol* Protocol, ClientCallback* notice_callback = NULL, Config* config = NULL); /** * callback can be NULL, indicating that no response is required. */ virtual int SendRequest(Request* request, ClientCallback* callback = NULL); /** * the return value indicates how much data needs to be processed, and returns -1 for error and need to close the connection. Returning 0 means there is no data to process. */ virtual int Update(); virtual void OnExit(); void Close(); Connector* connector() ; ClientCallback* notice_callback() ; Protocol* protocol() ; };Copy the code

At this point, the basic design of the client and server is complete, and you can directly write test code to check whether it runs normally.

This article has been published by Tencent Cloud + community in various channels, all rights belong to the author

For more fresh technology dry goods, you can follow usTencent Cloud technology community – Cloud Plus community official number and Zhihu organization number