Thursday, April 21, 2016

How to implement nested protocols with boost::asio?

Leave a Comment

I'm trying to write a server that handles protocol A over protocol B.

Protocol A is HTTP or RTSP, and protocol B is a simple sequence of binary packets:

[packet length][...encrypted packet data...] 

So I want to use things like that:

boost::asio::async_read_until(socket, inputBuffer, "\r\n\r\n", read_handler); 

However, instead of socket use some pseudo-socket connected to Protocol B handlers.

I have some ideas:

  1. Forget about async_read, async_read_until, etc., and write two state machines for A and B.

  2. Hybrid approach: async_read_* for protocol B, state machine for A.

  3. Make internal proxy server.

I don't like (1) and (2) because

  • It's hard to decouple A from B (I want to be able to disable protocol B).

  • Ugly.

(3) just looks ugly :-)

So the question is: how do I implement this?

2 Answers

Answers 1

I won't go over boost::asio, since this seems more a design pattern than a networking one. I'd use the State Pattern. This way you could change protocol on the fly.

class net_protocol { protected:     socket sock;  public:     net_protocol(socket _sock) : sock(_sock) {}      virtual net_protocol* read(Result& r) = 0; };  class http_protocol : public net_protocol { public:     http_protocol(socket _sock) : net_protocol(_sock) {}      net_protocol* read(Result& r) {         boost::asio::async_read_until(socket, inputBuffer, "\r\n\r\n", read_handler);         // set result, or have read_handler set it         return this;     } };  class binary_protocol : public net_protocol { public:     binary_protocol(socket _sock) : net_protocol(_sock) {}      net_protocol* read(Result& r) {         // read 4 bytes as int size and then size bytes in a buffer. using boost::asio::async_read         // set result, or have read_handler set it          // change strategy example         //if (change_strategy)         //  return new http_strategy(sock);          return this;     } }; 

You'd initialize the starting protocol with

std::unique_ptr<net_protocol> proto(new http_protocol(sock)); 

then you'd read with:

//Result result; proto.reset(proto->read(result)); 

EDIT: the if() return new stragegy are, in fact, a state machine

if you are concerned about those async reads and thus can't decice which return policies, have the policy classes call a notify method in their read_handler

class caller {     std::unique_ptr<net_protocol> protocol;     boost::mutex io_mutex;  public:     void notify_new_strategy(const net_protocol* p) {          boost::unique_lock<boost::mutex> scoped_lock(mutex);         protocol.reset(p);     }      void notify_new_result(const Result r) { ... } }; 

If you don't need to change used protocol on the fly you would have no need of State, thus read() would return Result (or, void and call caller::notify_new_result(const Result) if async). Still you could use the same approach (2 concrete classes and a virtual one) and it would probably be something very close to Strategy Pattern

Answers 2

I have done something like your answer (2) in the past - using async_read calls to read the header first and then another async_read to read the length and forward the remaining things to a hand written state machine. But I wouldn't necessarily recommend that to you - You thereby might get zero-copy IO for protocol B but doing an IO call reading the 4-8 byte header is quite wasteful when you know there is always data coming behind it. And the problem is that your network abstraction for the 2 layers will be different - so the decoupling problem that you mention really exists.

Using a fixed length buffer, only calling async_read and then processing the data with 2 nested state machines (like you are basically proposing in answer (1)) works quite well. Your state machine for each would simple get pushed some new received data (from either directly the socket or from the lower state machine) and process that. This means A would not be coupled to B here, as you could directly push the data to the A state machine from asio, if the input/output data format matches.

Similar to this are the patterns that are used in the Netty and Facebook Wangle libraries, where you have handlers that get data pushed from a lower handler in the pipeline, perform their actions based on that input and output their decoded data to the next handler. These handlers can be state machines, but depending on the complexity of the protocol don't necessarily have to be. You can take some inspiration from that, e.g. look at some Wangle docs: https://github.com/facebook/wangle/blob/master/tutorial.md

If you don't want to push your data from one protocol handler to another but rather actively read it (most likely in an asynchronous fashion) you could also design yourself some interfaces (like ByteReader which implements an async_read(...) method or PacketReader which allows to read complete messages instead of bytes), implement them through your code (and ByteReader also through asio) and use them on the higher level. Thereby you are going from the push approach of data processing to a pull approach, which has some advantages and disadvantages.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment