views:

948

answers:

5

I have a socket server, written in C++ using boost::asio, and I'm sending data to a client.

The server sends the data out in chunks, and the client parses each chunk as it receives it. Both are pretty much single threaded right now.

What design should I use on the server to ensure that the server is just writing out the data as fast as it can and never waiting on the client to parse it? I imagine I need to do something asynchronous on the server.

I imagine changes could be made on the client to accomplish this too, but ideally the server should not wait on the client regardless of how the client is written.

I'm writing data to the socket like this:

size_t bytesWritten = m_Socket.Write( boost::asio::buffer(buffer, bufferSize));

Update:

I am going to try using Boost's mechanism to write asynchronously to the socket. See http://www.boost.org/doc/libs/1_36_0/doc/html/boost_asio/tutorial/tutdaytime3/src.html

e.g.

 boost::asio::async_write(socket_, boost::asio::buffer(message_),
        boost::bind(&tcp_connection::handle_write, shared_from_this(),
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
  • Alex
+1  A: 

You can ensure asynchronous communication by transporting the data not over TCP but over UDP. However, if you need to use TCP, let the client store the data away quickly and process it in a different thread or asynchronously with a cron job.

stefanw
Is there not a way for the server to just pump out the data and have it queued somewhere? E.g. queued in the server TCP stack, or maybe queued in the client TCP stack?
Alex Black
Sure there is... but there isn't an OS (at least not that I'm aware of in Windows) utility to do it for you so you will have to implement it. This is basically what stefanw is saying, the client quickly storing the data is equivalent to your client queuing it.
DeusAduro
@DeusAduro: got it, but I'd like to do that on the *server* if possible. Can I write asychronously to the socket on the server? Or do I need to spin up a new thread on the server to write the data out?
Alex Black
@Alex have a look at my answer below.
DeusAduro
Getting the client to shift data as quickly as possible is a good idea, but is not sufficient. The delay is not necessarily with the client - it could be some router in between is the bottleneck, or (equivalently) that the client's bandwidth can't handle data as fast as the server can produce it. Then there's nothing the client can do to help, but you still don't want the server to block (unless it has a dedicated thread per socket, but OP apparently wants to avoid this)
Steve Jessop
+1  A: 

When you pass data to a socket, it does not wait for the receiver to process it. It does not even wait for the data to be transmitted. The data is put into an outbound queue that is processed by the OS in the background. The writing function returns how many bytes were queued for transmission, not how many bytes were actually transmitted.

Remy Lebeau - TeamB
Does the OS first check for connectivity before queuing it? Because our read/write functions are what tell us whether the client/server has disconnected. If it was to go straight to the queue and return these functions could never give us this information?
DeusAduro
Hmm, that doesn't seem to be what I'm seeing. If I comment out my client parsing code (so it just reads all of the data blindly), then my server requests finish faster.
Alex Black
@Remy: I said above that what you said is not what I'm seeing, BUT, that is what I'd like to see.
Alex Black
What Remy says is true until all the buffers are full between client and server. Eventually the server's outbound buffer is full too, and then in a BSD-style sockets API the write will either block or fail EAGAIN, according to whether the socket is blocking or non-blocking.
Steve Jessop
@onebyone: Interesting, thx. I'll have to look more closely at what is going on then between my server and client.
Alex Black
So how much will the socket API/OS buffer for us? Do you have a link I could look at, I'm really interested in this idea of the OS doing the buffering because I've never read about it before, it would be good stuff to know.
DeusAduro
Yes, the OS checks, but only if the socket is in a well-defined state. If the connection were lost abnormally, the OS would have no way of knowing that until the socket times out internally. In the meantime, the OS happily continues to queue outbound data until the queue fills up, at which time the calling code is blocked if the socket is in blocking mode, or the sending function fails with WSAEWOULDBLOCK if the socket is in non-blocking mode.
Remy Lebeau - TeamB
+1  A: 

If you set your socket to non-blocking, then writes should fail if they would otherwise block. You can then queue up the data however you like, and arrange for another attempt to be made later to write it. I don't know how to set socket options in the boost socket API, but that's what you're looking for.

But this is probably more trouble than it's worth. You'd need to select a socket that's ready for writing, presumably from several open simultaneously, shove more data into it until it's full, and repeat. I don't know if the boost sockets API has an equivalent of select, so that you can wait on multiple sockets at once until any of them is ready to write.

The reason that servers typically start a thread (or spawn a process) per client connection is precisely so that they can get on with serving other clients while they're waiting on I/O, while avoiding implementing their own queues. The simplest way to "arrange for another attempt later" is just to do blocking I/O in a dedicated thread.

What you can't do, unless boost has done something unusual in its sockets API, is require the OS or the sockets library to queue up arbitrary amounts of data for you without blocking. There may be an async API which will call you back when the data is written.

Steve Jessop
Thanks for the info, that sounds like what I am looking for.
Alex Black
Ah, I see you did find an async API. From the point of view of a BSD-style API, what this typically does is queue the write operation in the process (not in the TCP stack) until it can be written. If you're lucky, you can have arbitrary many writes on the same socket at the same time, which is pretty much as good as it gets. Often you'll only be allowed one.
Steve Jessop
cool, I will let you know how it goes...
Alex Black
+1  A: 

Continuing from the comments on Stefan's post:

It is definitely possible to buffer on either the client or server side. But make sure to consider what Neil wrote. If we just begin to buffer data blindly and if the processing can never keep up with the sending then our buffer will grow in a fashion we probably don't want.

Now I recently implemented a straightforward 'NetworkPipe' which was meant to function as a connection between a single client/server, server/client where the outside user doesn't know/care if the Pipe is the client or the server. I implemented a buffering situation similar to what you are asking about, how? Well the class was threaded, this was about the only way I could figure out to cleanly buffer the data. Here is the basic process that I followed, and note that I set a maximum size on the Pipes:

  1. Process 1 starts pipe, defaults to server. Now internal thread waits for client.
  2. Process 2 starts pipe, already a server, defaults to Client.
  3. We are now connected, first thing to do is exchange maximum buffer sizes.
  4. Process 1 writes data (it notes that the other end has an empty buffer [see #3])
  5. Process 2's internal thread (now waiting on a select() for the socket) sees that data is sent and reads it, buffers it. Process 2 now sends back the new buffered size to P1.

So thats a really simplified version but basically by threading it I can always be waiting on a blocking select call, as soon as data arrives I can read and buffer it, I send back the new buffered size. You could do something similar, and buffer the data blindly, its actually quite a bit simpler because you don't have to exchange buffer sizes, but probably a bad idea. So the above example allowed external users to read/write data without blocking their thread (unless the buffer on the other end is full).

DeusAduro
In 30 minutes this question has become so convoluted I can't follow it. Still it seems to me you have just reinvented part of TCP on top of TCP. Unless I am missing something, what you are doing is exactly what TCP is doing with its buffers and window sizes.
Newton Falls
Maybe I have, could you maybe direct me to an article about how the buffering is done. What I'm really interested in is how much data will the OS buffer for me? My method above allowed me to say "I want to buffer up to 1000 bytes, but no more.", is there someway of getting this well defined behavior out of the built in buffering?
DeusAduro
Every tcp socket has a send and a receive buffer in the kernel which can be managed by getsockopt()/setsockopt() with SO_RCVBUF and SO_SNDBUF options. How empty they are is what tcp tells its peer it can accept (i.e. window size). Don't know of good article but a search on the above and/or tcp buffers, flow control, and window size should turn up something. There's no substitute for a good tcp or sockets book (e.g. Stevens).
Newton Falls
As for how well-defined, probably not but you don't need it. Just let tcp manage its buffers and your app just reads however many bytes it thinks it can manage at one gulp. TCP will tell the peer to slow down if its buffers can't manage.
Newton Falls
A: 

I implemented a solution using the boost::asio::async_write method.

Basically:

  • I have one thread per client (my threads are doing CPU bound work)
  • As each thread accumulates some amount of data, it writes it to the socket using async_write, not caring if previous writes have completed
  • The code is careful to manage the lifetime of the socket and the data buffers being written out because the CPU processing finishes before all the data has written out

This works well for me. This enables the server thread to finish as soon as its done its CPU work.

Overall the the time for the client to receive and parse all of its data went down. Similarly the time (clock on the wall time) that the server spends on each client goes down.

Code snippet:

void SocketStream::Write(const char* data, unsigned int dataLength)
{
    // Make a copy of the data
    // we'll delete it when we get called back via HandleWrite
    char* dataCopy = new char[dataLength];
    memcpy( dataCopy,  data, dataLength );

    boost::asio::async_write
     (
     *m_pSocket,
     boost::asio::buffer(dataCopy, dataLength),
     boost::bind
      (
      &SocketStream::HandleWrite,      // the address of the method to callback when the write is done
      shared_from_this(),        // a pointer to this, using shared_from_this to keep us alive
      dataCopy,           // first parameter to the HandleWrite method
      boost::asio::placeholders::error,     // placeholder so that async_write can pass us values
      boost::asio::placeholders::bytes_transferred
      )
     );
}

void SocketStream::HandleWrite(const char* data, const boost::system::error_code& error, size_t bytes_transferred)
{
    // Deallocate the buffer now that its been written out
    delete data;

    if ( !error )
    {
     m_BytesWritten += bytes_transferred;
    }
    else
    {
     cout << "SocketStream::HandleWrite received error: " << error.message().c_str() << endl;
    }
}
Alex Black