0

I'm attempting to write an RFC 2812 compliant C++ IRC library. I am having some trouble with the design of the client itself. From what I have read IRC communication tends to be asynchronous.

I am using boost::asio::async_read and boost::asio::async_write. From reading the documentation I have gathered that you cannot perform multiple async_write requests before one is completed. You therefore end up with rather nested callbacks. Doesn't this defeat the purpose of doing async calls? Wouldn't it just be better to use synchronous calls to prevent the nesting? If not, why?

Secondly, if I am not mistaken, each boost::asio::async_write should be followed up by a boost::asio::async_read to receive the server's response to the commands sent. My client's functions, therefore, would need to take a callback parameter so a user of the class may do something after the client receives a response (ex. send another message...).

If I were to continue implementing this with async, should I keep a std::deque<std::tuple<message, callback>> and each time a boost::asio::async_write is finished, and there is a tuple in the queue, dequeue and send the message then raise the callback? Would this be the optimal way to implement this system?

I'm thinking since messages are sent all the time I'm going to have to implement some kind of listener loop that queues up responses, but how would you associate these responses with the specific command that triggered them? Or in the case that the response is just a message to the channel from another user?

Community
  • 1
  • 1
Francisco Aguilera
  • 3,099
  • 6
  • 31
  • 57

2 Answers2

2

The IRC protocol is a full-duplex protocol. As such, one should always be listening to the server connection expecting commands to process. It could be argued that one should primarily use the messages received from the server to update state, rather than correlating request and responses, as the server may not respond to a command or may respond much later than expected. For example, one may issue a WHOIS command, but receive multiple PRIVMSG commands before receiving a response to WHOIS. For a chat client, a user would likely expect being able to receive chat messages while waiting for a response to WHOIS. Hence, having a async_write() to async_read() call chain may not be ideal in handling the protocol.

For a given socket, the Asio documentation does recommend not initiating additional read operations if there is an outstanding composed read operation and not initiating additional write operations if there is an outstanding composed write operation. Queuing up messages and having an asynchronous call chains process from the queue is a great way to fulfill this recommendation. Consider reading this answer for a nice solution using a queue and an asynchronous call chain.

Also, be aware that the server may send a PING command even on an active connection. When the client is responding with a PONG command, it may be necessary to insert the PONG command near the front of the outbound queue so that it gets sent out as soon as possible.

Community
  • 1
  • 1
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • So you're saying I should have both a `std::deque>`, and a `std::deque>`? Also I should either spawn a thread to block and read constantly and fill up the reply queue? Or alternatively, recursively call async_read? What do you mean by update state - could you give an example? As for the whois or ping commands, I could have "hooked" routines that send specific messages when receiving specific replies such as these. – Francisco Aguilera Sep 26 '15 at 22:45
  • @FranciscoAguilera The IRC protocol is asynchronous by nature, so I would not recommend having blocking calls. The protocol is also not specific as to when replies to commands need to be sent, and some sever implementations may not send a response. I think it would be far more robust to handle server commands via events (e.g. call `on_join` when a `JOIN` command is received), rather than trying to manage callbacks so close to the protocol layer (the client may receive `JOIN` commands that are not in reply to a `JOIN` command sent by the client). – Tanner Sansbury Sep 27 '15 at 01:20
  • Right, but how do you propose I receive these commands? I need to be calling async_read, no? Should I just loop call async_read from it's callbacks checking for data to read throughout the life of the client? – Francisco Aguilera Sep 27 '15 at 01:26
  • @FranciscoAguilera An async-call chain loop invoking `async_read_until` would be appropriate where another `async_read_until` operation was initiated in the completion handler (this read operation will make handling the message boundary easier). Also, I just wanted to stress that the first paragraph in my answer was trying to accentuate concerns on handling a full-duplex protocol, such as IRC, as-if it was a half-duplex protocol. – Tanner Sansbury Sep 27 '15 at 01:45
  • Sounds great, one last question - (RFC 2812)[https://tools.ietf.org/html/rfc2812#section-3.1.2] mentions the max message size to be 510 bytes, is this the size of messages I should be reading in my async_read? What happens if a message is smaller or larger would that affect consequent read messages? – Francisco Aguilera Sep 27 '15 at 02:04
  • 1
    @FranciscoAguilera When implementing a protocol, it often requires being exceptional pedantic when reading the spec. That RFC mentions that the max message size is 512 bytes (510 bytes for command + parameters; 2 bytes for the CR-LF message boundary). As mentioned above, use the [`async_read_until()`](http://www.boost.org/doc/libs/1_59_0/doc/html/boost_asio/reference/async_read_until/overload2.html) to handle the message boundary. – Tanner Sansbury Sep 27 '15 at 04:12
1

Doesn't this defeat the purpose of doing async calls?

The usual solution is to use strands:

Why do I need strand per connection when using boost::asio?

You are free to queue multiple asynchronous operations on the same io objects using an (implicit) strand¹.

Using a strand ensures that the completion handlers are invoked on that same logical thread.


On the Protocol

You could indeed keep a queue of commands and await responses for each command before sending the next.

You might be a little bit smarter about this if you can spot the correlation due the different type of reply, but then you'd need to keep queues per type of command. I'd consider that premature optimization.

Community
  • 1
  • 1
sehe
  • 374,641
  • 47
  • 450
  • 633