Use and principle analysis of FFRPC and Server

  • 2020-06-19 11:09:57
  • OfStack

Abstract:

Ffrpc has been refactored and streamlined to make the code cleaner and cleaner, almost as perfect as I expected. I'm going to write several articles to show you what ffrpc can do. To summarize the features of ffrpc briefly:

Ffrpc is c++ network communication library Full asynchronous + callback mechanism Support common 2 base protocol, protobuf, thrift Design based on Broker pattern Clever design, small amount of code, core ffrpc only 1000 lines of code The performance monitoring of the interface is integrated, and the user can automatically obtain the performance data of the interface to facilitate the optimization of the interface

An example of a common base 2 protocol

Ffrpc implements one of the most basic serialization methods in binary system. The basic principle is that if it is a fixed length, copy it directly; if it is a string, copy the length before copying the content. Therefore, it only supports backward extension fields, which is not convenient for other languages, but it is very convenient and efficient if c++ only transmits messages between languages. For example, the communication of each process in the network game server can use the simplest 2 base protocol. A utility class ffmsg_t is defined in Ffrpc to define binary messages.

Message definition:


struct echo_t
{
  struct in_t: public ffmsg_t<in_t>
  {
    void encode()
    {
      encoder() << data;
    }
    void decode()
    {
      decoder() >> data;
    }
    string data;
  };
  struct out_t: public ffmsg_t<out_t>
  {
    void encode()
    {
      encoder() << data;
    }
    void decode()
    {
      decoder() >> data;
    }
    string data;
  };
};

As you can see, streaming serialization is provided in ffmsg_t, making serialization easy. When designing server messages, the following points should be noted:

When designing the server interface, each interface takes a message as a parameter and returns a message after processing, which is the most traditional rpc pattern. Ffrpc USES this design philosophy to simplify and standardize interface design. If you use ffmsg_t to define messages, my recommended definition style is similar to the code above. The input and output messages for the echo interface are defined above, but both are defined within the echo_t structure to make it clear that these are 1-pair interface messages. The traditional server interface defines one cmd for each interface, and then deserializes the specific interface through cmd. ffrpc omits the definition of cmd, but directly adopts the message name as cmd. For example, the interface registered in ffrpc accepts messages from echo_t, so the message received from echo_t naturally calls this interface Interface definition must specify both input and output messages Ffmsg_t supports normal type, string type, stl type.

echo service implementation code:


struct echo_service_t
{
  //! echo Interface, which returns the requested sent message ffreq_t Two template parameters can be provided, the 1 Six messages representing input (sent by the requester) 
  //!  The first 2 The template parameters represent the type of result message to be returned by the interface 
  void echo(ffreq_t<echo_t::in_t, echo_t::out_t>& req_)
  {
    echo_t::out_t out;
    out.data = req_.msg.data;
    LOGINFO(("XX", "foo_t::echo: recv %s", req_.msg.data.c_str()));

    req_.response(out);
  }
};
echo_service_t foo;
  //! broker Client, can be registered to broker , and register the service and interface, which can also be remotely invoked on other nodes 
  ffrpc_t ffrpc_service("echo");
  ffrpc_service.reg(&echo_service_t::echo, &foo);

  if (ffrpc_service.open(arg_helper))
  {
    return -1;
}

This defines the echo service, which provides an interface that accepts echo_t::in_t messages and returns echo_t::out_t messages. It follows that the steps to define a service using ffrpc are:

l defines messages and interfaces

In the example of registering an interface to ffrpc, ffpc provides the reg template method, which will automatically analyze what input message is used by the registered interface, thus ensuring that the corresponding interface will be called if the echo_t::in_t message arrives

At the heart of Ffrpc's work is broker, and a brief description of broker's role is to forward messages. client and server of Ffrpc are not connected directly, but communicate through broker forwarding messages.

The advantage is that the location of server is completely transparent to client, which is the essence of the broker model. So ffrpc was born scalability. For example, to invoke the interface of echo service, you don't need to know the location or configuration of serverr at all, just the name of the echo service. One might worry that full broker forwarding could be costly.

Broker ensures optimal message forwarding. If client or server and broker are in the same process, then messages are passed directly between memory without even serialization. This is also due to the broker mode, which is characterized by good scalability. In this way, ffrpc is perfectly capable of designing either a single-process server or a multi-process distributed set of services.
Example of client calling the echo service:


struct echo_client_t
{
  //!  Remote call interface, which can specify callback functions (or be left blank), also used ffreq_t Specifies the type of input message that can be used lambda Binding parameters 
  void echo_callback(ffreq_t<echo_t::out_t>& req_, int index, ffrpc_t* ffrpc_client)
  {
    if (req_.error())
    {
      LOGERROR(("XX", "error_msg <%s>", req_.error_msg()));
      return;
    }
    else if (index < 10)
    {
      echo_t::in_t in;
      in.data = "helloworld";
      LOGINFO(("XX", "%s %s index=%d callback...", __FUNCTION__, req_.msg.data.c_str(), index));
      sleep(1);
      ffrpc_client->call("echo", in, ffrpc_ops_t::gen_callback(&echo_client_t::echo_callback, this, ++index, ffrpc_client));
    }
    else
    {
      LOGINFO(("XX", "%s %s %d callback end", __FUNCTION__, req_.msg.data.c_str(), index));
    }
  }
};
ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
  }
  
  echo_t::in_t in;
  in.data = "helloworld";
  echo_client_t client;
  ffrpc_client.call("echo", in, ffrpc_ops_t::gen_callback(&echo_client_t::echo_callback, &client, 1, &ffrpc_client));

Using ffrpc call remote interface, only need to develop the service name and the input message, broker automatic positioning echo service position, in this example as ffrpc client and server with 1 process, then automatically passed through memory, if server and broker with 1 process, while client in other processes or physical machine, broker and server between transferring memory, broker and client send messages to tcp, which means that you write an tcp's server to receive the interface to send the message to service, and then send the message to client via tcp. It must be noted, however, that ffrpc completely simplifies the tcp server definition, and is even more scalability, and can be used entirely for multithreaded intra-process communication.

It should be noted that ffrpc has good fault tolerance. If the service does not exist or the interface does not exist or an exception occurs, the callback function will still be called, and the error message will be returned, making the error handling easier. For example, if client is logged in to gate in the game server but scene may not have started yet, this will be handled very well and the callback function will check for errors. For callbacks, familiar to developers who often use multithreading and task queues, support for lambda should be a plus, making asynchronous code more understandable.

Startup mode of Broker:


int main(int argc, char* argv[])
  {
    //!  Beautiful log component, shell The output is color drops!! 
    LOG.start("-log_path ./log -log_filename log -log_class XX,BROKER,FFRPC -log_print_screen true -log_print_file false -log_level 3");
  
    if (argc == 1)
    {
      printf("usage: %s -broker tcp://127.0.0.1:10241\n", argv[0]);
      return 1;
    }
    arg_helper_t arg_helper(argc, argv);
  
    //!  Start the broker , responsible for network-related operations, such as message forwarding, node registration, reconnection, etc 
  
    ffbroker_t ffbroker;
    if (ffbroker.open(arg_helper))
    {
      return -1;
    }
  
    sleep(1);
    if (arg_helper.is_enable_option("-echo_test"))
    {
      run_echo_test(arg_helper);    
    }
    else if (arg_helper.is_enable_option("-protobuf_test"))
    {
      run_protobuf_test(arg_helper);
    }
    else
    {
      printf("usage %s -broker tcp://127.0.0.1:10241 -echo_test\n", argv[0]);
      return -1;
    }
  
    ffbroker.close();
    return 0;
  }

Two key components in Ffrpc, broker and rpc, are responsible for forwarding and registering the server, and rpc represents the communication node, which could be client or server. Even with multiple servers, only broker1 ports are needed for listening, and other services only need to provide different service names.

Protobuf protocol example

The good design of Ffrpc decouples the protocol and adds 10 or so lines of code to support protobuf. Of course, this is also because the messages generated by protobuf inherit from the message base class. When I implemented thrift, things got a little trickier. The code generated by thrift was cleaner, but the generated messages were not integrated with the base class, so I had to copy and paste some code.

Protobuf definition file:


package ff;

message pb_echo_in_t {
 required string data = 1;
}
message pb_echo_out_t {
 required string data = 1;
}

We still design an echo service to define the echo interface message, based on the ffrpc design concept, each interface has an input message and an output message.

Echo service implementation code:


struct protobuf_service_t
{
  //! echo Interface, which returns the requested sent message ffreq_t Two template parameters can be provided, the 1 Six messages representing input (sent by the requester) 
  //!  The first 2 The template parameters represent the type of result message to be returned by the interface 
  void echo(ffreq_t<pb_echo_in_t, pb_echo_out_t>& req_)
  {
    LOGINFO(("XX", "foo_t::echo: recv data=%s", req_.msg.data()));
    pb_echo_out_t out;
    out.set_data("123456");
    req_.response(out);
  }
};
protobuf_service_t foo;
  //! broker Client, can be registered to broker , and register the service and interface, which can also be remotely invoked on other nodes 
  ffrpc_t ffrpc_service("echo");
  ffrpc_service.reg(&protobuf_service_t::echo, &foo);

  if (ffrpc_service.open(arg_helper))
  {
    return -1;
  }

In almost the same way as using ffmsg_t, the msg field of ffreq_t is the input message.

Sample code for client calling the echo server


struct protobuf_client_t
{
  //!  Remote call interface, which can specify callback functions (or be left blank), also used ffreq_t Specifies the type of input message that can be used lambda Binding parameters 
  void echo_callback(ffreq_t<pb_echo_out_t>& req_, int index, ffrpc_t* ffrpc_client)
  {
    if (req_.error())
    {
      LOGERROR(("XX", "error_msg <%s>", req_.error_msg()));
      return;
    }
    else if (index < 10)
    {
      pb_echo_in_t in;
      in.set_data("Ohnice");
      LOGINFO(("XX", "%s data=%s index=%d callback...", __FUNCTION__, req_.msg.data(), index));
      sleep(1);
      ffrpc_client->call("echo", in, ffrpc_ops_t::gen_callback(&protobuf_client_t::echo_callback, this, ++index, ffrpc_client));
    }
    else
    {
      LOGINFO(("XX", "%s %d callback end", __FUNCTION__, index));
    }
  }
};

  ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
  }
  
  protobuf_client_t client;
  pb_echo_in_t in;
  in.set_data("Ohnice");

  ffrpc_client.call("echo", in, ffrpc_ops_t::gen_callback(&protobuf_client_t::echo_callback, &client, 1, &ffrpc_client));

The advantages of Protobuf are:

Support version, which makes it easier to add fields

Protobuf is multilingual so you can communicate with other languages as well

Examples of the Thrift protocol

Thrift definition file:


namespace cpp ff 

struct echo_thrift_in_t {   
 1: string data
}

struct echo_thrift_out_t {   
 1: string data
}

Thrift server implementation code:


struct thrift_service_t
{
  //! echo Interface, which returns the requested sent message ffreq_t Two template parameters can be provided, the 1 Six messages representing input (sent by the requester) 
  //!  The first 2 The template parameters represent the type of result message to be returned by the interface 
  void echo(ffreq_thrift_t<echo_thrift_in_t, echo_thrift_out_t>& req_)
  {
    LOGINFO(("XX", "foo_t::echo: recv data=%s", req_.msg.data));
    echo_thrift_out_t out;
    out.data = "123456";
    req_.response(out);
  }
};
thrift_service_t foo;
  //! broker Client, can be registered to broker , and register the service and interface, which can also be remotely invoked on other nodes 
  ffrpc_t ffrpc_service("echo");
  ffrpc_service.reg(&thrift_service_t::echo, &foo);

  if (ffrpc_service.open(arg_helper))
  {
    return -1;
  }
  
  ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
}

An example of client calling echo:


struct thrift_client_t
{
  //!  Remote call interface, which can specify callback functions (or be left blank), also used ffreq_t Specifies the type of input message that can be used lambda Binding parameters 
  void echo_callback(ffreq_thrift_t<echo_thrift_out_t>& req_, int index, ffrpc_t* ffrpc_client)
  {
    if (req_.error())
    {
      LOGERROR(("XX", "error_msg <%s>", req_.error_msg()));
      return;
    }
    else if (index < 10)
    {
      echo_thrift_in_t in;
      in.data = "Ohnice";
      LOGINFO(("XX", "%s data=%s index=%d callback...", __FUNCTION__, req_.msg.data, index));
      sleep(1);
      ffrpc_client->call("echo", in, ffrpc_ops_t::gen_callback(&thrift_client_t::echo_callback, this, ++index, ffrpc_client));
    }
    else
    {
      LOGINFO(("XX", "%s %d callback end", __FUNCTION__, index));
    }
  }
};
ffrpc_t ffrpc_client;
  if (ffrpc_client.open(arg_helper))
  {
    return -1;
  }
  
  thrift_client_t client;
  echo_thrift_in_t in;
  in.data = "Ohnice";

ffrpc_client.call("echo", in, ffrpc_ops_t::gen_callback(&thrift_client_t::echo_callback, &client, 1, &ffrpc_client));

Advantages and disadvantages of Thrift:

Thrift is more flexible, supports list and map, and can be nested N languages supported The official version relies on boost, from which ffrpc extracts a basic version of c++, with only header files, not boost

conclusion

Ffrpc is a network communication library based on c++. scalability and ease of use are the biggest advantages Using ffrpc for interprocess communication is very easy, just define the service and interface, you can use google protobuf and facebook thrift in addition to the most traditional message definition of ffmsg_t. Ffrpc is fully asynchronous, and asynchronous logic can be easily manipulated by means of the callback function +lambda. More examples of Ffrpc will follow, and the advantages of ffrpc will become more obvious when the system is complex. Github address: https: / / github com/fanchy/FFRPC

Related articles: