2

I want to make a buffer of characters, write to it using sprintf, then pass it to multiple calls of async_write() (i.e. distribute it to a set of clients). My question is what is the best data structure to use for this? If there are compromises then I guess the priorities for defining "best" would be:

  1. fewer CPU cycles
  2. code clarity
  3. less memory usage

Here is what I have currently, that appears to work:

function broadcast(){
  char buf[512];
  sprintf(buf,"Hello %s","World!");
  boost::shared_ptr<std::string> msg(new std::string(buf));
  msg->append(1,0);   //NUL byte at the end

  for(std::vector< boost::shared_ptr<client_session> >::iterator i=clients.begin();
    i!=clients.end();++i) i->write(buf);
}

Then:

void client_session::write(boost::shared_ptr<std::string> msg){
  if(!socket->is_open())return;
  boost::asio::async_write(*socket,
    boost::asio::buffer(*msg),
    boost::bind(&client_session::handle_write, shared_from_this(),_1,_2,msg)
    );
}

NOTES:

  • Typical message size is going to be less than 64 bytes; the 512 buffer size is just paranoia.
  • I pass a NUL byte to mark the end of each message; this is part of the protocol.
  • msg has to out-live my first code snippet (an asio requirement), hence the use of a shared pointer.

I think I can do better than this on all my criteria. I wondered about using boost::shared_array? Or creating an asio::buffer (wrapped in a smart pointer) directly from my char buf[512]? But reading the docs on these and other choices left me overwhelmed with all the possibilities.

Also, in my current code I pass msg as a parameter to handle_write(), to ensure the smart pointer is not released until handle_write() is reached. That is required isn't it?

UPDATE: If you can argue that it is better overall, I'm open to replacing sprintf with a std::stringstream or similar. The point of the question is that I need to compose a message and then broadcast it, and I want to do this efficiently.

UPDATE #2 (Feb 26 2012): I appreciate the trouble people have gone to post answers, but I feel none of them has really answered the question. No-one has posted code showing a better way, nor given any numbers to support them. In fact I'm getting the impression that people think the current approach is as good as it gets.

Darren Cook
  • 27,837
  • 13
  • 117
  • 217

3 Answers3

3

First of all, note that you are passing your raw buffer instead of your message to the write function, I think you do not meant to do that?

If you're planning to send plain-text messages, you could simply use std::string and std::stringstream to begin with, no need to pass fixed-size arrays.

If you need to do some more binary/bytewise formatting I would certainly start with replacing that fixed-size array by a vector of chars. In this case I also wouldn't take the roundtrip of converting it to a string first but construct the asio buffer directly from the byte vector. If you do not have to work with a predefined protocol, an even better solution is to use something like Protocol Buffers or Thrift or any viable alternative. This way you do not have to worry about things like endianness, repetition, variable-length items, backwards compatibility, ... .

The shared_ptr trick is indeed necessary, you do need to store the data that is referenced by the buffer somewhere until the buffer is consumed. Do not forget there are alternatives that could be more clear, like storing it simply in the client_session object itself. However, if this is feasible depends a bit on how your messaging objects are constructed ;).

KillianDS
  • 16,936
  • 4
  • 61
  • 70
  • Thanks @KillianDS. Regarding your first point, the code I show works, and is also how they do it in the tutorial: http://www.boost.org/doc/libs/1_39_0/doc/html/boost_asio/tutorial/tutdaytime3.html (and, regarding your last point, they store the std::string in the class object; but they can do that because read and writes cleanly alternate. That is not a choice for me.) – Darren Cook Feb 20 '12 at 02:42
1

You could store a std::list<boost::shared_ptr<std::string> > in your client_session object, and have client_session::write() do a push_back() on it. I think that is cleverly avoiding the functionality of boost.asio, though.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
bdow
  • 181
  • 4
  • Thanks. This is the approach used in the final class in this tutorial: http://www.gamedev.net/blog/950/entry-2249317-a-guide-to-getting-started-with-boostasio?pg=10 See `std::list< std::vector< uint8_t > > m_pending_sends` in the Connection class then the function bodies of `Send()`, `DispatchSend()`, `HandleSend()` and `StartSend()` – Darren Cook Feb 21 '12 at 00:21
  • What do you think are the pro/cons of this approach? I still copy by `char buf[512]` into a `std::string`; what is the advantage over what I do currently? Also, what do you mean by _cleverly avoiding the functionality of boost.asio_? – Darren Cook Feb 21 '12 at 00:24
  • Well, with the caveat that I haven't actually used boost.asio in my code, the boost.asio "rationale" page says: `Boost.Asio provides the tools to manage these long running operations, without requiring programs to use concurrency models based on threads and explicit locking.` To safely manage a message queue the way I outlined, and the way the tutorial you mentioned seems to, you should protect against concurrent access/modification (to prevent race conditions when one thread is writing to the queue and another is consuming elements). – bdow Feb 21 '12 at 01:28
  • It seems that the right way to do this is to use a shared_ptr to a string or vector, or even a shared_array, and pass that into the `client_session::write()` call (so that when it goes out of scope for all client sessions, it gets cleaned up correctly). It's much like what you have, actually. – bdow Feb 21 '12 at 01:54
1

As I got you need to send the same messages to many clients. The implementation would be a bit more complicated.

I would recommend to prepare a message as a boost::shared_ptr<std::string> (as @KillianDS recommended) to avoid additional memory usage and copying from your char buf[512]; (it's not safe in any case, you cannot be sure how your program will evolve in the future and if this capacity will be sufficient in all cases).

Then push this message to each client internal std::queue. If the queue is empty and no writings are pending (for this particular client, use boolean flag to check this) - pop the message from queue and async_write it to socket passing shared_ptr as a parameter to a completion handler (a functor that you pass to async_write). Once the completion handler is called you can take the next message from the queue. shared_ptr reference counter will keep the message alive until the last client suffesfully sent it to socket.

In addition I would recommend to limit maximum queue size to slow down message creation on insufficient network speed.

EDIT

Usually sprintf is more efficient in cost of safety. If performance is criticical and std::stringstream is a bottleneck you still can use sprintf with std::string:

std::string buf(512, '\0');
sprintf(&buf[0],"Hello %s","World!");

Please note, std::string is not guaranteed to store data in contiguous memory block, as opposite to std::vector (please correct me if this changed for C++11). Practically, all popular implementations of std::string does use contiguous memory. Alternatively, you can use std::vector in the example above.

Andriy Tylychko
  • 15,967
  • 6
  • 64
  • 112
  • Thanks Andy. "prepare a message as a ... std::string". By this you mean use stringstream? sprintf does not work with std::string. Is using stringstream, then calling str() on it, more efficient than using sprintf into a char buf[512] then creating a std::string from it? (I've never measured it, but my hunch is that the sprintf approach is either more efficient, or they are the same.) – Darren Cook Feb 26 '12 at 10:36
  • Thanks Andy; that is an interesting approach. It is legal in C++11 (see http://stackoverflow.com/questions/7518732/why-are-stdvectordata-and-stdstringdata-different ) It should be quicker (*) than my example code, at the expense of memory (the 512 in my code didn't matter as I create a std::string of exactly the right size; here you always use that much). *: Ignoring cache behaviour due to using more memory! – Darren Cook Feb 28 '12 at 04:46