Continuing where we left of…
Last time we left off after having taken a look at
UDP
connections. But, as we saw, UDP
connections have several issues :
- No delivery guarantee
- Packages might not arrive in the same order they were sent
So how do we solve these issues? By creating an actual connection. A TCP
connection that is.
TCP connections
As mentioned in the previous post,
UDP
connections aren’t connections at all. It’s basically just two parts sending data back and forth between them. TCP
connections, however, are proper connections. This means we know the following :
- Both parts tries to keep the connection alive
- Both parts have received everything the other part has sent
- Every package has been received in order
These points requires two different mechanisms, so let’s look at the one by one.
How TCP works
In order to have a connection, both parts have to agree on it. This is different from
UDP
connection because in UDP
connections we just send the data without bothering about actual connections. So how do we set up the TCP
connection? By using a technique called three-way handshake
The three-way handshake
The process of establishing a
TCP
connections requires three different steps:
- Part 1 contacts part 2 and asks to start a connection
- Part 2 replies and either says OK, or declines
- If part 2 declines, the process stops here
- Part 1 replies back, to confirm that he’s received part 2’s request
- Now the actual connection is started and we can send data
The third step might seem kinda unnecessary, but remember that any packages can get lost. And so if part 2 doesn’t get the answer from part 1, part 2 doesn’t know whether part 1 is still there. Part 1 might have lost the connection or wished to stop it altogether. We can compare this to starting a phone call :
- Part 1 calls part 2
- Part 2 picks up and says hello
- Part 1 says hello back
After the last step, the conversation is started. All of these step are necessary for us to know there is an actual conversation. Skipping one of them might make you think something is wrong. For instance, if part 2 doesn’t say ‘hello’, you might think there is something wrong with the connection.
Dealing with the UDP issues
The major flaw of
UDP
is that it doesn’t deal with loss of packages. And another issue is that we don’t know if we’re getting the correct package at the correct time. Potentially we can get every package in the wrong order. Which could lead to everything from glitchy gameplay to crashes. So let’s look at the mechanics that makes TCP
connections able to give their guarantees :
ACK numbers
In
TCP
every package gets a unique number. This is basically just a counter so the first package gets the number 0
, next one 1
and so on…
This number is then sent back from the recipient to confirm that “okay, so now I have every package up to this package” ( I will refer to the confirmation as ACK
numbers.) So when we get an ACK for the last paackage we sent, we know that the other part has received everything we have sent so far. This means we don’t have to worry about any of the packages having been lost.
But say the receiver misses a package in the middle? For example, what if the receiver gets package 1,2,4, but not 3? The receiver will look at the ACK
numbers and think “huh… I’m missing a package here.” And will only send ACK
2 back. At this point, your application might get package 1 and 2, but not 4, since it’s out of order.
Flow of TCP
Let’s look at an example to see how the
TCP
might handle loss of packages.
- Sender sends package 1,2,3,4
- Receiver receives package
1
- Your application receives package 1
- Send
ACK 1
all good so far!- Receiver receives package
2
and4
- Uh oh! Where’s package
3
?- Sender receives
ACK 2
- Sender waits a while before resending package 3 and 4
- It sends package 4, which the receiver already has, because it only got
ACK
2- Receiver receives package 3
- Now your application will get the last two packages ( 3 and 4 )
- Receiver sends
ACK 4
- Sender receives
ACK 4
- Receiver receives package 3, again!
- The original package 3 was just delayed get it again.
- Since we already have these packages, it’ll get discarded
Now the receiver has got all 4 packages and all is fine and dandy. But if you study the example, you see that in our case the sender has to resend 2 packages ( 3
and 4
), while the receiver in reality only needed one ( 3
) And in addition, we got package 3
twice. But even though we got package 4 early, it wasn’t sent to our application before we got package 3
because TCP
we receive everything in order. This is one of the major drawback of TCP
; there can be lots of delays and re-sending of packages.
TCP send rate
A final issue about
TCP
performance is how it regulates package sending. TCP
can send several packages at any time. The basic idea is that every time every package is sent successfully and in time, it’ll send more packages the next time.
So the first time, it might only send 2
packages the first time. But if it gets ACK
for all those three in a given time, it might send 4
the next time. Then maybe 8
and keep increasing it until it doesn’t get an ACK
for all packages on time. When that happens, it’ll send less packages the next time. Let’s look at a simple example :
- Send
2
packages
- Receive
ACK
for both packages- Send
4
packages
- Receive
ACK
for all packages- Send
8
packages
- Receive
ACK
for only 5 packages- We’re missing 3 packages! Maybe this was too many packages? Try sending less…
- Send
6
packages
As you can see, TCP
will try its best to keep sending the maximum number of packages without having to resend anything.
Complexity of TCP
Although
TCP
is quite old ( more than 30 years old, ) it’s really complicated. There are a lot of different mechanisms involved to deal with anything that might happen. Not only do they handle the cases we’ve seen, but it also, as we saw, needs to control the speed for optimal performance.
I have purposely simplified it because its nice to have a basic understanding of how TCP
works as this might help you to choose whether you should use UDP
or TCP
The two different parts of TCP
connections
Since
TCP
connection are actual connections, there needs to be a central part ( server ) that the others ( clients ) connect to. I’ll briefly discuss servers and clients, and what their roles are when it comes to TCP
Server
The server is the part that all the clients will connect to. So the server will always be listening for new connection and accepting them as they come. The server will be accepting connections from any client, so we don’t specify
IP
s on their server side ( ,ore on this later. ) We will only specify the port
to listen to.
Client
The client tries to connect to the server. It needs to know both the
IP
and port
of the server before it can try to connect to the server. The server doesn’t know anything about the client until it tries to connect to the server.
When the server accepts the client, the connection is established. At this point in time, the server also has a connection to that client specifically. We’ll see this later.
Note that these are just different types of connection, not necessarily different computers. A computer can have any number of server and / or client connections.
Time for some code
So now that we have all the technical information about
TCP
is out of the way, we can start setting up a TCP
connection of our own. As you might expect, the source code for TCP
is a bit more involved than the UDP
one.
This part relies on code from my previous post. If something is unclear, you can go back and read that part if you want more information. I have also added links to the documentation in the headers.
SDLNet_ResolveHost
Just like with
UDP
, we need to ask SDL net
to get correct the correct representation ( because of different endianness). The function is the same ( but it is used in a different way for servers, so do read on)
123456 int SDLNet_ResolveHost(IPaddress* address,const char* host,Uint16 port)
Parameters :
IPaddress* address
– a pointer to an allocated / createdIPAdress
.const char* host
–IP address
to send to ( xxx.xxx.xxx.xxx )Uint16 port
– theport number
to send to
Return value
The int value
0
on success, otherwise-1
. In this case,address.host
will beINADDR_NONE
. This can happen if the address is invalid or leads to nowhere.
But there is a slight difference in how it’s used. Since TCP
connections are actual connections it has both a server and a client part :
Server
The server part needs to be listening for
IP
s that are trying to connect. So we’re not really resolving a host this time. We’re just preparing the IPaddress
for the next step.
So what we do is that we simply use null
as the IP
. This tells SDL_net
that we don’t want to use this IPaddress
to connect to a host, but rather just listen for other connections. SDL_net
does this by setting the IP
to INADDR_NONE
. This comes into play in the next step ( SDLNet_TCP_Open
)
Client
For clients, this function is used more or less exactly like in the previous part ; it’ll prepare the
IPaddress
with the information we supply.
Of course, the port
on both the server and client side has to be the same.
Note: no connections has been initiated yet, we’ve just asked SDL
to prepare the port
and IP address
for us.
TCPSocket
This is a new type. It represents a
TCP
connection. We’ll use it just like UDPSocket
but of course this time it represents a TCP
connection instead of a UDP
connection.
SDLNet_TCP_Open
Now that we have an
IPadress
correctly set up, we can try to connect to a host. This is done by the function SDLNet_TCP_Open
Here is the function signature.
1234 TCPsocket SDLNet_TCP_Open(IPaddress *ip)
Parameters :
IPaddress *ip
– anIPaddress*
contaning theIP
andport
we want to connect to. We’ll use the one we got fromSDLNet_ResolveHost
Return:
-
For clients : a
TCPsocket
to the server, which can be used for sending and receiving data -
For servers : a
TCPsocket
used for listening for new clients trying to connect
This function will try to open a connection. But just like with SDLNet_ResolveHost
, there are two different cases here
Server
Above we saw that if we call
SDLNet_ResolveHost
with null
as the IP
, SDL_net
will set the IP
of the INADDR_NONE
. This means we will be listening for connections, rather than trying to connect. This is because, as a server, we don’t actively try to connect to another host ( we just accept connections ), so we don’t know about any IP
address yet.
What this function does in this case, is that it tries to open the port for listening.
Client
For clients, this works much like for
UDP
: we try to connect to the server with the given IP
and port
At this point, the client is connected to the server, and now they can communicated. This is a little different from how it works in UDP
so let’s start by looking at how the communcation can be done in TCP
A quick example
Before we jump into the next part, let’s have a quick look at an example of how to use these two functions. These two functions are the initialization part of the
TCP
code. Since these steps are slightly different form client to server, I’ll cover them separately.
Server
Simply set up the
IP
address and use it to open a port
for listening :
123456789101112131415161718192021222324252627 int port = 12312;IPaddress ipAddress;// This sets the IPaddress up with the correct values.// Since we want to listen for connection, we set the IP parameter to null// This will set the ip in ipAddress to INADDR_NONEint success = SDLNet_ResolveHost( &ipAddress, nullptr, port );if ( success == -1 ){std::cout << "Failed to open port : " << port << std::endl;return false;}TCPsocket tcpSocket;// Remember : ipAddress has the IP set to INADDR_NONE,// so now we'll listen for new connections on 'port'tcpSocket = SDLNet_TCP_Open( &ipAddress );if ( tcpSocket == nullptr ){std::cout << "Failed to open port for listening : " << port<<" \n\tError : " << SDLNet_GetError()<< std::endl;return false;}
Client
Simply set up the
IP
address and try to connect to server with that IPaddress
:
1234567891011121314151617181920212223242526 // Since this is the client connection, we supply the ip of the server// In this case, 127.0.0.1 which basically means "this computer"int port = 12312;IPaddress ipAddress = "127.0.0.1";// This sets the IPaddress so we can try to connect to the server in the next stepint success = SDLNet_ResolveHost( &ipAddress, ipAddress, port );if ( success == -1 ){std::cout << "Failed to open port : " << port << std::endl;return false;}TCPsocket tcpSocket;// Try to open a connection to the servertcpSocket = SDLNet_TCP_Open( &ipAddress );if ( tcpSocket == nullptr ){std::cout << "Failed to connect to port : " << port<<" \n\tError : " << SDLNet_GetError()<< std::endl;return false;}
The job of the client
The clients are the parts you’ll be dealing with they most. A client communicates with other clients. This is more or less just like in
UDP
, but there are som differences.
SDLNet_TCP_Send
Sending data using TCP
is done using a slightly different function from that of UDP
:
123456 int SDLNet_TCP_Send(TCPsocket sock,const void *data,int len)
Parameters :
TCPsocket sock
– theTCPsocket*
on which to send the dataconst void *data
– the data to sendint len
– the length of the data ( in bytes )
This function is quite straight ahead. The only thing to note is the void*
. The type void*
is something that is widely used in C
but not so much in C++
. It’s basically a pointer to anything. So the data can be just about any form of data. This requires a bit of low-level C
“hacking” to get right.
Return:
The number of bytes that was sent. If this is less than the size of the data we tried to send ( or the
len
parameter, ) an error has occured. This error could be the client disconnecting or a network error.
Using this function correctly is tricky, in a similar way to UDP
. Let’s look at a possible way to implement it ;
12345678910 // Cast our std::string to void* so that SDL_net can understand it properly// (Don't worry too much about this )const char* charPtr = str.c_str();void* messageData = const_cast< char* > ( charPtr );int messageSize = str.length();int bytesSent = bytesSent = SDLNet_TCP_Send( tcpSocket, messageData, messageSize);if ( bytesSent < messageSize )// ERROR! SDLNet_TCP_Send sent less bytes than we wanted.
SDLNet_TCP_Recv
Receiving data using TCP
is also done using a slightly different function from that of UDP
:
123456 int SDLNet_TCP_Recv(TCPsocket sock,void *data,int maxlen)
Parameters :
TCPsocket sock
– theTCPsocket*
on which to recv the data fromconst void *data
– the data to receiveint maxlen
– the maximum data to receive
Return:
The number of data received. If this is less than
0
, an error has occured.
And since this is C
( and not C++
) we need to allocate a decent sized buffer in advance ( this is the void *date
part. It’ll have the same size as maxlen
. The setting of the buffer involves a little C
-style trickery.
Let’s look at an example :
12345678910111213141516171819202122 // Allocate a char buffer to hold the message for uschar buffer[bufferSize ];// Empty it ( set all bytes to '0; )memset( buffer, 0, bufferSize );// Try to receive data// NOTE: Receiving data involves a little C style pointer manipulation.// I'll try to find a cleaner, less C-like way to do it.int byteCount = SDLNet_TCP_Recv( tcpSocket, buffer, bufferSize );// Success! We received something ...if ( byteCount > 0 ){// Set the last character to '\0' which means "end of sting"buffer[byteCount] = '\0';return buffer;}// We received 0 or less bytes.// This may mean an error hs occurred or the connection may have been closed.// ( The TCPSocket is probably invalid at this point so we can't use it anymore )
The job of the server
So now we have a
TCPsocket
that listens to the port
we specified. And now we can try to accept new connections. For now, we’ll try to accept connections right out of the blue. But later we’ll look out how to check for clients trying to connect. Anyways ; here is the method we need:
SDLNet_TCP_Accept
This is the essentially the accept part of the three-way handshake. The client has tried to connect to us and we need to accept it before the connection is established. This function does exactly what you might expect : it accepts an incoming
TCP
connection, informs the client and thus establishing the connection.
1234 TCPsocket SDLNet_TCP_Accept(TCPsocket server)
Parameters :
TCPsocket *server
– theTCPsocket*
we use for listening for new connections. This is theTCPConnection
we created usingSDLNet_TCP_Open
.
Return :
A different
TCPsocket
thisTCPsocket
does represent a connection to a specific client. If it’s valid, it means a connection has been established. If it’snull
it means no connection was established. This can mean that there was an error. But it can also mean that there was no clients trying to connect.
This function might lead to some confusion as there are two TCPsocket
s, but remember :
The first one ( the parameter we supply ) is ther server TCPsocket
. This is not connected to any client, we just need it to be able to listen for new connection. We create this TCPSocket
by callling SDLNet_TCP_Open
The second TCPsocket
is for a specific client.We create this TCPSocket
by callling SDLNet_TCP_Accept
. When it’s created, it can be used exaclty like the TCPsocket
s created on the client side. ( As I talked about in the cleint part of SDLNet_TCP_Open
)
Dealing with SDLNet_TCP_Recv
There is a major issue with the receive function. It blocks. This means the function waits until it has received something. Actually, according to the documentation, it’ll wait til it has received exactly
maxlen
bytes and then set those in the void* data
. But from what I’ve found, this isn’t 100% true.
What I have found, is that the function will block. But only until it has received something ( at most maxlen
bytes. ) So, in other words, it waits til it has received something, no matter how little or much it is. But even though this is better than waiting for maxlen
bytes, the fact that it blocks is still an issue we’ll need to solve.
SDLNet_TCP_Recv
will also join together messages if it can. So say client 1 sends
“Hello”
and
“World”
in two separate messages, SDLnet
can join them together so that what client 2 gets is
“HelloWorld”
in one message.
This can ( and probably will ) happen if buffer size is large enough.
Or, if the buffer size is too small one call might only get part of the data. So if client 1 sends :
“HelloWorld”
But client 2 has the buffer size set to 6
, it’ll get
“HelloW”
The first time client 2 calls SDLNet_TCP_Recv
. And
“orld”
The second time it calls SDLNet_TCP_Recv
That means there are two issues to fix : the fact that it blocks and the fact that we might not receive everything with one call to SDLNet_TCP_Recv
.
SDLNet_SocketSet
To solve this, we can check if something has happened on a collection of
TCPsocket
s, this includes someone connecting, disconnecting or receiving data.
We can use a SDLNet_SocketSet
to solve this. Think of it as simply a set of sockets. We’ll be using it for storing and checking TCPsocket
s to see if there is any activity. A SDLNet_SocketSet
can contain any number of TCPSocket
s. Those can be both server and client connections.
SDLNet_TCP_AddSocket
This is a really simple function for adding a socket to a
SDLNet_SocketSet
. It also exists for UDP
, but we’ll be using the TCP
version, of course.
12345 int SDLNet_TCP_AddSocket(SDLNet_SocketSet set,TCPsocket sock);
Parameters :
SDLNet_SocketSet *set
– theSDLNet_SocketSet
we want to add theTCPsocket
toTCPsocket *sock
– theTCPsocket
we want to add to theSDLNet_SocketSet
Return :
The number of
TCPsocket
s in theSDLNet_SocketSet
on success. Or-1
on failure.
SDLNet_CheckSockets
Now that we’ve added sockets to the
SDLNet_SocketSet
, we can use the SDLNet_CheckSockets
function to check for activity. “Activity” in this case basically means that something has happened. This can either mean we have received data, that someone has disconnected or that there is an error.
12345 int SDLNet_CheckSockets(SDLNet_SocketSet set,Uint32 timeout);
Parameters :
SDLNet_SocketSet *set
– theSDLNet_SocketSet
we want to check for activityUint32 timeout
– a variable stating how long ( in milliseconds ) we want to wait for activity. We can wait anything between 0 milliseconds and… well anything up to 49 days.
Return :
The number of
TCPsocket
s in theSDLNet_SocketSet
with activity on success. Or-1
if either theSDLNet_SocketSet
is empty or there was an error.
SDLNet_SocketReady
After we’ve called
SDLNet_CheckSockets
, we can use this function to check whether a particular TCPSocket
has been marked as active. This function should be called on a socket on a SDLNet_SocketSet
after code>SDLNet_CheckSockets has been called on the SocketSet
that holds that TCPSocket
.
1234 int SDLNet_SocketReady(TCPSocket* socket);
Parameters :
TCPSocket *socket
– theTCPSocket
we want to check for activity
Return :
Count of
TCPSocket
s with activity
In other words ; we use SDLNet_CheckSockets
to see if any of the TCPSocket
s in a SDLNet_SocketSet
has any activity. If so, we can call SDLNet_SocketReady
on each of the SDLNet_SocketSet
s in that SDLNet_SocketSet
to see if that TCPSocket
in particular has any activity.
Examples
Now let’s look at how you could implement an update function that checks for activity. They’ll be different for server and client connections since client connections checks for incoming messages and disconnections. While on the server side we’ll simply check for clients trying to connect.
Client side example
As I mentioned above, on the client side we need to check for connections and incomming messages. Here is a way to do that :
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 TCPSocket tcpSock1;TCPSocket tcpSock2;TCPSocket tcpSock3;SDLNet_SocketSet clientSocketSet;std::vector<TCPSocket> clientSocketList;// Init TCP Sock 1, 2 and 3 ( like in the first client part )// ...// Add sockets :SDLNet_TCP_AddSocket(clientSocketSet, tcpSock1);clientSocketList.push_back(tcpSock1);SDLNet_TCP_AddSocket(clientSocketSet, tcpSock2);clientSocketList.push_back(tcpSock1);SDLNet_TCP_AddSocket(clientSocketSet, tcpSock3);clientSocketList.push_back(tcpSock1);// Check sockets.int32_t countActiveConnections = SDLNet_CheckSockets(clientSocketSet, 0);// Loop through all tcpSockets in socketList// Untill we've found countActiveConnections sockets with activityint i = 0;while (countActiveConnections != 0){// Check if this connection has activity, which means it's either disconnect or has received a messageif (!SDLNet_SocketReady(socketList[i])){// No activity, check next TCPsocket++i;continue;}// We now know that this socket has activity, reduce count and continue--countActiveConnections;// Check if client has disconnected// ( IsConnected() is not part of TCPsocket, I'll show you later )if ( (clientSocketList[i].IsConnected()){// Delete this tcpSocket and move on to next socketclientConnections.erase( std::begin(clientSocketList) + i );// Clean the connection and remove from socket// See final code at the bottom for example++i;std::cout << "Client has disconnected!\n";continue;}// Since the TCPsocket wasn't disconnect, we can assume it has new data for us// Think of ReadMessage() like I showed in the first partstd::cout << "New message : " << clientSocketList[i].ReadMessage() );}
A problem that arises here, is that calling code>SDLNet_CheckSockets kind of sets the TCPSocket
back to “inactive” when you call it. Even if there is several messages waiting to be read.
So when you have called ReadMessage()
, you have no way of knowing if it has any more data. Calling it again, would mean calling SDLNet_TCP_Recv
again which could block until the other client sent more data.
This is an issue lots of tutorials that I’ve seen has. But there is a solution that doesn’t block ; we just need to call SDLNet_CheckSockets
again. So just add this to the bottom of the previous function
12345678910 // Looping through all sockets...// ( Same code as Socket set example part 1// Even if we have checked all connections, something might have happened since last timeif (countActiveConnections ==0){countActiveConnections = SDLNet_CheckSockets(clientSocketSet, 0);i = 0;}} // End while loop
Server side example
On the server side, we need to check for clients trying to connect. This is fortunately a little bit simpler than what we had to do on the client side. Here is the code :
123456789101112131415161718192021222324252627282930313233343536373839404142434445 // Add sockets :TCPSocket tcpSock1;TCPSocket tcpSock2;SDLNet_SocketSet serverSocketSet;std::vector<TCPSocket> serverSocketList;// Init TCP Sock 1, 2 and 3 ( like in the first client part )// ...SDLNet_TCP_AddSocket(set, tcpSock1);serverSocketList.push_back(tcpSock1);SDLNet_TCP_AddSocket(set, tcpSock2);serverSocketList.push_back(tcpSock1);// Check sockets.int32_t countActiveConnections = SDLNet_CheckSockets(serverSocketSet, 0);// Loop through all tcpSockets in socketList// Untill we've found countActiveConnections sockets with activityint i = 0;while (countActiveConnections != 0){if (!SDLNet_SocketReady(serverSocketList[i])){--countActiveConnections;// Now we know that there is a connection waiting for us, so let's accept it!TCPsocket* newSocket = SDLNet_TCP_Accept(serverSocketList[i]);if (newConnection != nullptr){// Note : we add this to the list of client socket, not server socketsSDLNet_TCP_AddSocket(clientSocketSet, newSocket)clientSocketList.push_back(newConnection);}// Even if we have checked all connections, something might have happened since last timeif (countActiveConnections ==0){countActiveConnections = SDLNet_CheckSockets(serverSocketSet, 0);i = 0;}}
I think that’s all for now. You can find a working implementation
Conclusion
Setting up a
TCP
connection using SDL_Net
is quite tricky. Lots of tutorials out there just briefly discuss the topic without going into much detail about the different functions. Hopefully this post has helped you get a better view of the different parts of SDL_net
( I sure did writing it! ) I might also post a third networking post about an even better way of doing network communication using both UDP
and TCP
for maximum performance.
I’m also really glad to finally have finished and published a new post. I know it’s been a long time since last time, but I’ve been a bit busy at work and haven’t really had the time or energy. But I feel some of my energy is back. And getting positive feedback is always amazing, they help me keep going. So thanks to everyone who’s commented! : )
(Semi) Final code :
TCP
connections ( NOTE : work in progress! )Feel free to comment if you have anything to say or ask questions if anything is unclear. I always appreciate getting comments.
You can also email me : olevegard@headerphile.com