Player
contains 3 int
s and a std::string
This int
s can easily be written to a stream with write
(watch out for the differing byte orders, endian, used by processors) , but the string
is too complex an object. Option 1 is to replace the string
with a fixed size char
array to simplify the structure, but this adds more pain than it's worth. The char
array can easily be overflowed, and always writes the full size of the array whether you used it or not.
The better approach is to establish a communication protocol and serialize the data.
First establish the endian to be used by the protocol so that both sides know exactly which byte order is used. Traditionally Big Endian is used for network communications, but there are fewer and fewer big endian devices, so this is increasingly becoming a case of, "Your call."
Let's stick with big endian because the tools for this are ancient, well known, and well established. If you are using a socket library odds are very good you have access to tools to perform the operations required.
Next, explicitly size the integer data types. Different implementations of C++ may have different sizes for fundamental types. Solution for this one is simple: Don't use the fundamental types. Instead, use the fixed width integers defined in in cstdint
Right now our structure looks something like
struct player
{
int x,y;
int score;
std::string name;
}
It needs to look more like
struct player
{
int32_t x,y;
int32_t score;
std::string name;
}
Now there are no surprises about the size of an integer. The code either compiles or does not support int32_t
. Stop laughing. There are still 8 bit micro controllers out there.
Now that we know what the integers look like, we can make a pair of functions to handle reading and writing:
bool sendInt32(int32_t val)
{
int32_t temp = htonl(val);
int result = SDLNet_TCP_Send(client, (char*)&temp, sizeof(temp));
return result == sizeof(temp); // this is simplified. Could do a lot more here
}
bool readInt32(int32_t & val)
{
int32_t temp;
if (readuntil(client, (char*)&temp, sizeof(temp)))
{
val = ntohl(temp);
return true;
}
return false;
}
Where readuntil
is a function that keeps reading data from the socket until the socket either fails or all of the requested data has been read. Do NOT assume that all of the data you wanted will arrive all to the same time. Sometimes you have to wait. Even if you wrote it all in one chunk it won't necessarily arrive all in one chunk. Dealing with this is another question, but it gets asked just about weekly so you should be able to find a good answer with a bit of searching.
Now for the string
. You go the C route and send a null terminated string, but I find this makes the reader much more complicated than it needs to be. Instead I prefix the string data with the length so all the reader has to do is read the length and then read length bytes.
Basically this:
bool sendstring(const std::string & str)
{
if (sendInt32(str.size()))
{
int result = SDLNet_TCP_Send(client, str.c_str(), str.size());
return result == str.size();
}
return false;
}
bool readstring(std::string & str)
{
int32 len;
if (readInt32(len))
{
str.resize(len);
return readuntil(client, str.data(), len) == len;
}
return false;
}
All together, a writer looks something like
bool writePlayer(const Player & p)
{
return sendInt32(p.x) &&
sendInt32(p.y) &&
sendInt32(p.score) &&
sendString(p.name);
}
and a reader
bool readPlayer(Player & p)
{
return readInt32(p.x) &&
readInt32(p.y) &&
readInt32(p.score) &&
readString(p.name);
}