OnReadCompleted()
which is shown below. The difference being that we pass a pointer to a buffer to our call to Read()
. This pointer is returned to us by the ProcessDataStream()
function and will either be null
or a pointer to the buffer that was passed in to ProcessDataStream()
. The reason for this is that we need to accumulate a complete packet in the server before we echo the packet (or, in a server with slightly more complex business logic, act on the contents of the packet). ProcessDataStream()
implements our protocol and if it doesn't have enough bytes for a complete packet it returns a pointer to the buffer so that it will be used in the next call to Read()
, this call will read more data from the TCP stream into the same buffer, appending it to the existing buffer contents.
void CSocketServer::OnReadCompleted( IStreamSocket &socket, IBuffer &buffer) { try { IBuffer *pBuffer = ProcessDataStream(socket, buffer); socket.Read(pBuffer); } catch(const CException &e) { Output(_T("ReadCompleted - Exception - ") + e.GetDetails()); socket.AbortConnection(); } catch(...) { Output(_T("ReadCompleted - Unexpected exception")); socket.AbortConnection(); } }
ProcessDataStream()
is where the protocol itself is managed, since our protocol is so simple all we're actually doing is breaking the TCP byte stream into our packet structure. We do this by using a very simple state machine. Note that the state machine runs inside a processing loop as we may be dealing with 0, 1 or many protocol packets in a single buffer. IBuffer *CSocketServer::ProcessDataStream( IStreamSocket &socket, IBuffer &buffer) const { IBuffer *pBuffer = &buffer; bool done; do { done = true; const IBuffer::BufferSize used = buffer.GetUsed(); if (used >= GetMinimumMessageSize())
if (used >= GetMinimumMessageSize()) { const IBuffer::BufferSize messageSize = GetMessageSize(buffer); if (used == messageSize) { Output(_T("Got complete, distinct, message")); // we have a whole, distinct, message EchoMessage(socket, buffer); pBuffer = 0; done = true; }
(used < messageSize)
, yes we have a packet and that's all we have (used == messageSize)
and yes we have a packet and we have more data to process once we're done with that packet (used > messageSize)
. The incomplete packet state is dealt with by exiting the processing loop and returning a pointer to the buffer to the caller to add more data to. used == messageSize
then we have a single protocol packet in the buffer and we can pass the buffer off to our business logic, EchoMessage()
, break out of our processing loop and return a null
to our caller to tell it to read into a new buffer. else if (used > messageSize) { Output(_T("Got message plus extra data")); // we have a message, plus some more data // allocate a new buffer, copy the extra data into it and try again... CSmartBuffer message(buffer.SplitBuffer(messageSize)); EchoMessage(socket, message.GetRef()); // loop again, we may have another complete message in there... done = false; }
SplitBuffer()
for this; it takes x
bytes from the front of buffer 'A' and returns a new buffer (B) with those x
bytes in it. It then moves the remainder of the bytes in 'A' to the front of 'A'. We can then pass buffer 'B' to our business logic and continue to loop and process the remaining bytes in 'A'. else if (messageSize > buffer.GetSize()) { Output(_T("Error: Buffer too small\nExpecting: ") + ToString(messageSize) + _T("Got: ") + ToString(buffer.GetUsed()) + _T("\nBuffer size = ") + ToString(buffer.GetSize()) + _T("\nData = \n") + DumpData(buffer.GetMemory(), buffer.GetUsed(), 40)); socket.Shutdown(ShutdownSend); // throw the rubbish away buffer.Empty(); done = true; } }
} while (!done); // not enough data in the buffer, reissue a read into the same buffer to collect more data return pBuffer; }
null
if the buffer contained a complete packet or a pointer to the buffer if it contained a partial packet. EchoMessage()
function is the same as in the Basic Echo Server and could, of course, be replaced with something that actually processed the contents of our protocol's packets.