|
I often see budding network programmers confused about how to send messages (packets
of a specific size) across a stream protocol, such as TCP. (The same problem also
applies when writing structured data to files on disk).
Because TCP is a stream protocol, you can't assume that a call to send()
will correspond to a single return from recv() because the implementation
is free to split a single send across multiple packets -- or coalesce multiple
packets into one.
To make sure you can still send a message and have it come out as such on the
other end, you need to prefix the message by a size. You may also want to add a
known message identifier (such as an enum or int) while you're at it. Here is some
code that will wrap send() and recv() to do that:
|
struct Header {
unsigned int msg;
unsigned int size;
};
struct Packet {
Packet() { data = 0; h.msg = 0; h.size = 0; }
~Packet() { free( data ); }
Packet( Packet const & o ) { ... }
Packet& operator=( Packet const & o ) { ... }
Header h;
void * data;
};
/// send_packet is used on the sending side to send a packet
void send_packet( Packet const & p, SOCKET s ) {
if( ::send( s, sizeof(p.h), &p.h, 0 ) != sizeof(p.h) ) {
throw BadSend;
}
if( p.h.size > 0 && ::send( s, p.h.size, p.data, 0 ) != p.h.size ) {
throw BadSend;
}
}
/// recv_all is a convenient wrapper to ensure receiving
/// enough data on a reliable stream.
bool recv_all( SOCKET s, size_t z, void * d ) {
while( z > 0 ) {
int r = ::recv( s, z, d );
if( r < 1 ) return false;
z -= r;
d = (void *)(((char *)d)+r);
}
return true;
}
/// recv_packet is used on the receiving side to receive
/// a packet.
void recv_packet( Packet * oP, SOCKET s ) {
if( !recv_all( s, sizeof(oP->h), &oP->h ) ) {
throw BadRecv;
}
oP->data = realloc( oP->data, oP->h.size );
if( oP->h.size > 0 && !recv_all( s, oP->h.size, oP->data ) ) {
throw BadRecv;
}
}
|
|
|
Now, if you're using non-blocking sockets (which is a good idea if you're
running an interactive system using a single thread), you can't block for
data like the above code illustrates. Instead, you'll have to buffer input
data, and build a state machine that knows whether it's looking for a
header/length, or looking for that much data coming over the wire.
|
class ReceiveBuffer {
public:
ReceiveBuffer( size_t maxSize = 1024 ) {
base_ = new char[maxSize];
end_ = base_+maxSize;
ptr_ = base_;
}
~ReceiveBuffer() {
delete[] base_;
}
void poll( SOCKET s ) {
int r = ::recv( s, ptr_, end_-ptr_, 0 );
if( r > 0 ) {
ptr_ += r;
}
}
size_t size() const {
return ptr_-base_;
}
char const * data() const {
return base_;
}
void remove( size_t s ) {
assert( s <= size() );
::memmove( base_, base_+s, size()-s );
ptr_ -= s;
}
size_t maxSize() const {
return ptr_-base_;
}
char * base_;
char * end_;
char * ptr_;
};
class ReceiveStateMachine {
public:
ReceiveStateMachine( SOCKET s ) : s_( s ), gotHdr_(false) {
packet_.data = 0;
}
Packet * poll() {
if( packet_.data ) {
assert( gotHdr_ );
gotHdr_ = false;
packet_.data = 0;
buf_.remove( packet_.h.size );
}
buf_.poll( s_ );
if( !gotHdr_ && buf_.size() >= sizeof(packet_.h) ) {
::memcpy( &packet_.h, buf_.data(), sizeof(packet_.h) );
buf_.remove( sizeof(packet_.h) );
gotHdr_ = true;
if( packet_.h.size >= buf_.maxSize() ) {
throw NetError( "junk input data" );
}
}
if( gotHdr_ && buf_.size() >= packet_.h.size ) {
packet_.data = buf_.data();
return &packet_;
}
return 0;
}
SOCKET s_;
ReceiveBuffer buf_;
bool gotHdr_;
Packet packet_;
};
|
|
| |
|