Friday, November 12, 2010

Creating a std::iostream socket class (Part 2)

Last time I outlined how the tcpstream class should work. Having written socket code before, the biggest unknown was "how does I makes a stream of my owns"? The answer is "you implement a custom std::streambuf object." Once you have a stream buffer reading from your source, you simply create a std::iostream object and set it to use that stream buffer, et voila!

The standard documentation for streambuf is rather terse and more than a little ambiguous, but happily, C++ Annotations Version 6.2.3 chapter 20 begins by detailing the implementation of a custom std::streambuf object.

In short, all that's required to create your own stream buffer is to implement some or all of the following methods:

// implement for std::streambuf read functionality
virtual int underflow();

// implement for std::streambuf write functionality
virtual int overflow(int c = EOF);

// optional but may improve performance
virtual std::streamsize xsgetn(char_type *s, std::streamsize n);
virtual std::streamsize xsputn(char_type *s, std::streamsize n);

The first of these, underflow(), is called when the owner of the stream buffer runs out of data while reading. The general form that this method should take is:

int underflow() {
  int bytes_read = fillbuffer(m_pBuffer, MAX_BUFFER_SIZE);
  setg(m_pBuffer, m_pBuffer, m_pBuffer + bytes_read);

The setg() call sets the object's stream buffer. The arguments are, in order:
  • A pointer to the first byte of the buffer.

  • A pointer to the current byte in the buffer (which will usually be the first byte of the buffer, if you read a whole buffer at a time).

  • A pointer to the byte after the end of the buffer.
For tcpstream, I added a helper method, fillbuffer(), which checks the socket for new data and is called at the beginning of all data-related methods. Since I didn't want to limit my stream to a predetermined fixed line length, I needed to buffer an arbitrary amount of data, and for this I settled on storing a linked list of data chunks. Each chunk represents a single read operation on the socket, and can store up to BUFFER_SIZE bytes.

struct datachunk {
  int bytes;
  uint8_t *data;
  datachunk *next; // pointer to next datachunk
  datachunk(int size) : bytes(size),
     data(new uint8_t[size]), next(NULL) { }
  ~datachunk() { delete[] data; }

With this linked list, the logic became pretty simple: If there's a new data chunk queued up, delete the current chunk and start using the new chunk's data. If there isn't then we've temporarily run out of data and we return EOF.

// implementation of underflow() for std::streambuf
int tcpbuf::underflow() {

  // bail if we don't have another buffer to swap to yet
  if (m_data->inbox == NULL || m_data->inbox->next == NULL)
    return EOF;

  // we've used up the buffer at the front of the inbox, free it
  datachunk *old = m_data->inbox;
  m_data->inbox = old->next;
  delete old;

  // and set the new buffer up for the stream to read from
  char_type *newdata = (char_type *)m_data->inbox->data;
  setg(newdata, newdata, newdata + m_data->inbox->bytes);
  return *newdata;

Note: In order to keep the interface completely platform-independent without resorting to factory methods and the like, the class stores its data in a wrapper struct m_data. For this method it's enough to know that m_data->inbox is a pointer to the head of the list of datachunks.

The second of the mandatory methods, overflow(), is called in the inverse condition, when data is written to the stream. It just needs to make sure the byte it's given gets to where it needs to be. Unless you're buffering the data to write later (not a good plan for a socket class that's intended to be used somewhat interactively!) then it's usually a simple matter:

// implementation of overflow() for std::streambuf
int tcpbuf::overflow(int c) {
  if (m_data->socket == -1) return -1;
  return write(m_data->socket, &c, sizeof(c));

In this case, I didn't want to write my bytes to the socket individually every time, so I added an implementation for the optional xsputn() method, which simply writes n bytes instead of a single byte:

// implementation of xsputn() for std::streambuf
std::streamsize tcpbuf::xsputn(const char *s, std::streamsize n) {
  if (m_data->socket == -1) return -1;
  return write(m_data->socket, s, n);

So there you go. That's all you have to do to implement a std::streambuf. Of course, I haven't covered any of the socket code here. That's for next time.

One last thing; it's a drag to create a stream buffer, connect that to your network, and then create a new std::iostream to use that streambuffer, every time you want to connect a socket. There's a simple way around this though:

// tcpstream - a tcpbuf-based iostream, for convenience
class tcpstream : public std::iostream, public tcpbuf {
  tcpstream() : std::iostream(this) { }

So now all of the socket-related methods in tcpbuf are available on a tcpstream, and you can also use the stream for your overloaded IO operators.

No comments:

Post a Comment