From the translator: This is a translation of the second article in the series “Networking for game programmers” . I really like the whole series of articles, plus I always wanted to try myself as a translator. Perhaps an experienced developers article seems too obvious, but, as it seems to me, the benefits of it in any case will be.
The first article is http://habrahabr.ru/post/209144/
Reception and transmission of data packets
Introduction
Hi, my name is Glenn Fiedler and I greet you in my second article in the series “Network Programming for Game Developers”.
In the
previous article, we discussed various ways of transferring data between computers over a network, and at the end decided to use the UDP protocol, rather than TCP. We decided to use UDP in order to be able to send data without delays associated with waiting for packet retransmission.
And now I'm going to tell you how to use UDP in practice to send and receive packets.
')
BSD sockets
Most modern operating systems have some kind of implementation of sockets based on BSD sockets (Berkeley sockets).
BSD sockets operate on simple functions such as socket, bind, sendto, and recvfrom. Of course, you can directly access these functions, but in this case your code will be platform dependent, since their implementations may differ slightly in different operating systems.
Therefore, even though I will continue to give the first simple example of interaction with BSD sockets, in the future we will not use them directly. Instead, after mastering the basic functionality, we will write several classes that abstract all work with sockets, so that our code will be platform-independent in the future.
Features of different OS
First, let's write the code that will determine the current OS, so that we can take into account the differences in the operation of sockets:
Now connect the header files needed for working with sockets. Since the set of necessary header files depends on the current OS, here we use the #define code, written above, to determine which files to include.
#if PLATFORM == PLATFORM_WINDOWS #include <winsock2.h> #elif PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #endif
In UNIX systems, socket functions are included in the standard system libraries, so we do not need any third-party libraries in this case. However, in Windows for this purpose we need to connect the winsock library.
Here's a little trick on how to do this without changing the project or the makefile:
#if PLATFORM == PLATFORM_WINDOWS #pragma comment( lib, "wsock32.lib" ) #endif
I like this trick because I'm lazy. Of course, you can connect the library to the project or to the makefile.
Socket initialization
In most unix-like operating systems (including macosx), no special actions are required to initialize the functionality of working with sockets, but in Windows, you must first do a couple of steps - you need to call the “WSAStartup” function before using any functions of working with sockets, and after finishing - call “WSACleanup”.
Let's add two new features:
inline bool InitializeSockets() { #if PLATFORM == PLATFORM_WINDOWS WSADATA WsaData; return WSAStartup( MAKEWORD(2,2), &WsaData ) == NO_ERROR; #else return true; #endif } inline void ShutdownSockets() { #if PLATFORM == PLATFORM_WINDOWS WSACleanup(); #endif }
Now we have a platform independent initialization and shutdown code for sockets. On platforms that do not require initialization, this code simply does nothing.
Create a socket
Now we can create a UDP socket. This is done like this:
int handle = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); if ( handle <= 0 ) { printf( "failed to create socket\n" ); return false; }
Next, we must bind the socket to a specific port number (for example, 30,000). Each socket must have its own unique port, because when a new packet arrives, the port number determines which socket to send it to. Do not use port numbers smaller than 1024 - they are reserved by the system.
If you do not care what port number to use for the socket, you can simply transfer to the “0” function, and then the system will allocate to you some unused port.
sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons( (unsigned short) port ); if ( bind( handle, (const sockaddr*) &address, sizeof(sockaddr_in) ) < 0 ) { printf( "failed to bind socket\n" ); return false; }
Now our socket is ready to send and receive data packets.
But what is the mysterious function “htons” called in the code? This is just a small auxiliary function that translates the byte order in a 16-bit integer - from the current (little or big-endian) to big-endian, which is used for network interaction. It needs to be called every time you use whole numbers when working with sockets directly.
You will find the “htons” function and its 32-bit twin - “htonl” in this article several more times, so be careful.
Putting the socket into non-blocking mode
By default, sockets are in the so-called “blocking mode”. This means that if you try to read data from it using “recvfrom”, the function will not return a value until the socket receives a packet with data that can be read. This behavior does not suit us at all. Games are applications that run in real time, at a speed of 30 to 60 frames per second, and the game cannot simply stop and wait until the data packet arrives!
This problem can be solved by putting the socket in “non-blocking mode” after its creation. In this mode, the “recvfrom” function, if there is no data to read from the socket, immediately returns a specific value, indicating that you need to call it again when data appears in the socket.
You can translate a socket into non-blocking mode as follows:
#if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX int nonBlocking = 1; if ( fcntl( handle, F_SETFL, O_NONBLOCK, nonBlocking ) == -1 ) { printf( "failed to set non-blocking socket\n" ); return false; } #elif PLATFORM == PLATFORM_WINDOWS DWORD nonBlocking = 1; if ( ioctlsocket( handle, FIONBIO, &nonBlocking ) != 0 ) { printf( "failed to set non-blocking socket\n" ); return false; } #endif
As you can see, in Windows there is no “fcntl” function, so together with it we use “ioctlsocket”.
Sending Packages
UDP is a connectionless protocol, so every time we send a packet, we need to specify the recipient address. You can use the same UDP socket to send packets to different IP addresses — on the other end of the socket you don’t have to have one computer.
You can forward a packet to a specific address as follows:
int sent_bytes = sendto( handle, (const char*)packet_data, packet_size, 0, (sockaddr*)&address, sizeof(sockaddr_in) ); if ( sent_bytes != packet_size ) { printf( "failed to send packet: return value = %d\n", sent_bytes ); return false; }
Note that the return value from the “sendto” function only shows whether the packet was successfully sent from the local computer. But it does not indicate whether the packet was accepted by the addressee! UDP does not have the means to determine whether a packet has reached its destination or not.
In the code above, we pass the sockaddr_in structure as the destination address. How do we get this structure?
Suppose we want to send a package to the address 207.45.186.98:370000.
We write the address in the following form:
unsigned int a = 207; unsigned int b = 45; unsigned int c = 186; unsigned int d = 98; unsigned short port = 30000;
And you need to make a couple more transformations in order to bring it to a form that “sendto” understands:
unsigned int destination_address = ( a << 24 ) | ( b << 16 ) | ( c << 8 ) | d; unsigned short destination_port = port; sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl( destination_address ); address.sin_port = htons( destination_port );
As you can see, we first combine the numbers a, b, c, d (which lie in the range [0, 255]) into a single integer, in which each byte is one of the original numbers. Then we initialize the “sockaddr_in” structure with our destination address and port, while not forgetting to convert the byte order using the “htonl” and “htons” functions.
We should also single out the case when you need to transfer the packet to yourself: you do not need to find out the IP address of the local machine, but you can simply use 127.0.0.1 as the address (the address of the local loop) and the packet will be sent to the local computer.
Reception of packages
After we have tied a UDP socket to a port, all UDP packets arriving at the IP address and port of our socket will be queued. Therefore, to receive packets, we simply call “recvfrom” in a loop until it gives an error, meaning that there are no more packets to read in the blackout.
Since the UDP protocol does not support connections, packets can come from many different computers on the network. Every time we accept a packet, the “recvfrom” function gives us the IP address and port of the sender, and therefore we know who sent the packet.
Code for receiving packets in a loop:
while ( true ) { unsigned char packet_data[256]; unsigned int maximum_packet_size = sizeof( packet_data ); #if PLATFORM == PLATFORM_WINDOWS typedef int socklen_t; #endif sockaddr_in from; socklen_t fromLength = sizeof( from ); int received_bytes = recvfrom( socket, (char*)packet_data, maximum_packet_size, 0, (sockaddr*)&from, &fromLength ); if ( received_bytes <= 0 ) break; unsigned int from_address = ntohl( from.sin_addr.s_addr ); unsigned int from_port = ntohs( from.sin_port );
Packages that are larger than the receive buffer size will simply be quietly removed from the queue. So, if you use a 256 byte buffer, as in the example above, and someone sends you a 300 byte packet, it will be discarded. You do not just get the first 256 bytes from the packet.
But since we are writing our own protocol, this will not be a problem for us. Just always be careful and check that the receive buffer size is large enough to accommodate the largest package you can send.
Close socket
On most unix-like systems, sockets are file descriptors, so in order to close sockets after use, you can use the standard “close” function. However, Windows, as always, stands out, and in it we need to use “closesocket”.
#if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX close( socket ); #elif PLATFORM == PLATFORM_WINDOWS closesocket( socket ); #endif
Keep it up, Windows!
Socket class
So, we have dealt with all the basic operations: creating a socket, binding it to a port, switching to non-blocking mode, sending and receiving packets, and, finally, closing the socket.
But, as you can see, all these operations are slightly different from platform to platform, and, of course, it is difficult every time when working with sockets to remember the features of different platforms and write all these #ifdef.
Therefore, we will make a “Socket” wrapper class for all these operations. We will also create the class “Address” to make it easier to work with IP addresses. It will allow not to carry out all manipulations with “sockaddr_in” every time we want to send or receive a package.
So, our Socket class:
class Socket { public: Socket(); ~Socket(); bool Open( unsigned short port ); void Close(); bool IsOpen() const; bool Send( const Address & destination, const void * data, int size ); int Receive( Address & sender, void * data, int size ); private: int handle; };
And Address class:
class Address { public: Address(); Address( unsigned char a, unsigned char b, unsigned char c, unsigned char d, unsigned short port ); Address( unsigned int address, unsigned short port ); unsigned int GetAddress() const; unsigned char GetA() const; unsigned char GetB() const; unsigned char GetC() const; unsigned char GetD() const; unsigned short GetPort() const; bool operator == ( const Address & other ) const; bool operator != ( const Address & other ) const; private: unsigned int address; unsigned short port; };
Use them for reception and transmission as follows:
As you can see, this is much easier than working directly with BSD sockets. And also this code will be the same for all OSs, because the whole platform-dependent functionality is inside the Socket and Address classes.
Conclusion
Now we have a platform independent tool for sending and prema UDP packets.
UDP does not support connections, and I wanted to make an example that would clearly show this. Therefore, I wrote a
small program that reads a list of IP addresses from a text file and sends them packets, one per second. Each time a program receives a packet, it displays the address and port of the sending computer and the size of the received packet to the console.
You can easily configure the program so that even on a local machine you can get several nodes exchanging packets with each other. To do this, just different instances of the program, set different ports, for example:
> Node 30000
> Node 30001
> Node 30002
Etc…
Each node will forward packets to all other nodes, forming something like a mini peer-to-peer system.
I developed this program on MacOSX, but it should compile on any unix-like OS and on Windows, however, if you need to make any improvements for this, let me know.