Berkeley Sockets - Multiplayer Game Programming: Architecting Networked Games (2016)

Multiplayer Game Programming: Architecting Networked Games (2016)

Chapter 3. Berkeley Sockets

This chapter introduces the most commonly used networking construct for multiplayer game development, the Berkeley Socket. It presents the most common functions for creating, manipulating, and disposing sockets, discusses differences between platforms, and explores a type-safe, C++ friendly wrapper for socket functionality.

Creating Sockets

Originally released as part of BSD 4.2, the Berkeley Sockets API provides a standardized way for processes to interface with various levels of the TCP/IP stack. Since its release, the API has been ported to every major operating system and most popular programming languages, so it is the veritable standard in network programming.

Processes use the API by creating and initializing one or more sockets, and then reading data from or writing data to those sockets. To create a socket, use the aptly named socket function:

SOCKET socket(int af, int type, int protocol);

The af parameter, standing for address family, indicates the network layer protocol which the socket should employ. Potential values are listed in Table 3.1.

Image

Table 3.1 Address Family Values for Socket Creation

Most games written these days support IPv4, so your code will most likely use AF_INET. As more users switch to IPv6 Internet connections, it becomes more worthwhile to support AF_INET6 sockets as well.

The type parameter indicates the meaning of packets sent and received through the socket. Each transport layer protocol that the socket can use has a corresponding way in which it groups and uses packets. Table 3.2 lists the most commonly supported values for this parameter.

Image

Table 3.2 Type Values for Socket Creation

Creating a socket of type SOCK_STREAM informs the operating system that the socket will require a stateful connection. It then allocates the necessary resources to support a reliable, ordered stream of data. This is the appropriate socket type to use when creating a TCP socket.SOCK_DGRAM, on the other hand, provides for no stateful connection and allocates only the minimal resources necessary to send and receive individual datagrams. The socket should make no effort to maintain reliability or ordering of packets. This is the appropriate socket type for a UDP socket.

The protocol parameter indicates the specific protocol that the socket should use to send data. This can include transport layer protocols, or various utility network layer protocols that are part of the Internet protocol suite. Typically, the value passed in as the protocol is copied directly into the protocol field of the IP header for each outgoing packet. This signifies to the receiving operating system how to interpret data wrapped by the packet. Table 3.3 gives typical values for the protocol parameter.

Image

Table 3.3 Protocol Values for Socket Creation

Note that passing 0 as the protocol tells the OS to pick the default implemented protocol for the given socket type. This means you can create an IPv4 UDP socket by calling

SOCKET udpSocket = socket(AF_INET, SOCK_DGRAM, 0);

You can create a TCP socket by calling

SOCKET tcpSocket = socket(AF_INET, SOCK_STREAM, 0);

To close a socket, regardless of type, use the closesocket function:

int closesocket( SOCKET sock );

When disposing of a TCP socket, it is important to do so in a manner that ensures all outgoing and incoming data are transmitted and acknowledged. It is best to first cease transmitting on the socket, then wait for all data to be acknowledged and all incoming data to be read, and then to close the socket.

To cease transmitting or receiving before closing, use the shutdown function:

int shutdown(SOCKET sock, int how)

For how, pass SD_SEND to cease sending, SD_RECEIVE to cease receiving, or SD_BOTH to cease sending and receiving. Passing SD_SEND will cause a FIN packet to transmit once all data has been sent, which will notify the other end of the connection it can safely close its socket. That will result in a FIN packet being sent back in response. Once your game receives the FIN packet, it is safe to actually close the socket.

This closes the socket and returns any associated resources to the operating system. Make sure to close all sockets when they are no longer needed.


Note

In most cases, the operating system creates the IP layer header and transport layer header for each packet sent out over a socket. However, by creating a socket of type SOCK_RAW and protocol 0, you can directly write each of the header values for those two layers. This allows you to set header fields directly which are not normally editable. For instance, you could easily specify a custom TTL for each outgoing packet: That is exactly what the Traceroute utility does. Manually writing the values for various header fields is often the only way to insert illegal values in those fields, which can be particularly useful when fuzz testing your servers, as mentioned in Chapter 10, “Security.”

Because raw sockets allow illegal values in header fields, they are a potential security risk, and most operating systems allow the creation of raw sockets only in programs with elevated security credentials.


API Operating System Differences

Although Berkeley Sockets are the standard low-level way to interface with the Internet on various platforms, the API is not perfectly uniform across all operating systems. There are several idiosyncrasies and differences worth understanding before jumping into cross-platform socket development.

The first of these is the data type used to represent the socket itself. The socket function as listed earlier returns a result of type SOCKET, but this type actually exists only on Windows-based platforms like Windows 10 and Xbox. A little digging into the Windows headers files shows thatSOCKET is a typedef for a UINT_PTR. That is, it points to an area of memory that holds state and data about the socket.

Contrariwise, on POSIX-based platforms like Linux, Mac OS X, and PlayStation, a socket is represented by a single int. There is no socket data type per se: The socket function returns an integer. This integer represents an index into the operating system’s list of open files and sockets. In this way, a socket is very similar to a POSIX file descriptor, and in fact can be passed to many OS functions that take file descriptors. Using sockets in this way limits some of the flexibility provided by the dedicated socket functions, but in some cases provides an easy path to porting a non-network based process to a network compatible one. One significant drawback of the socket function returning an int is the lack of type safety, as the compiler will not balk at code which passes any integral expression (e.g., 5 × 4) to a function that takes a socket parameter. Several code examples in this chapter address this problem, as it is a general weakness of the Berkeley Socket API on all platforms.

Regardless of whether your platform represents a socket as an int or a SOCKET, it’s worth noting that sockets should always be passed by value to functions in the socket library.

The second major difference between platforms is the header file which contains the declarations for the library. The Windows version of the socket library is known as Winsock2, and thus files which use socket functionality must #include the file WinSock2.h. There is an older version of the Winsock library called Winsock, and this version is actually included by default in the overarching Windows.h file used in most Windows programs. The Winsock library is an earlier, limited, less optimized version of the WinSock2 library, but it does contain several basic library functions, such as the socket creation one discussed earlier. This creates a name conflict when both Windows.h and WinSock2.h are included in the same translation unit: Multiple declarations for the same functions cause the compiler to choke and spew errors confusing to those who are unaware of this conflict. To avoid this, you must make sure to either #include WinSocket2.h before Windows.h, or to #define the macro WIN32_LEAN_AND_MEAN before including Windows.h. The macro causes the preprocesser to omit, among other things, the inclusion of Winsock from the list of files contained in Windows.h, thus preventing the conflict.

WinSock2.h only contains declarations for the functions and data types directly related to sockets. For tangential functionality, you will have to include other files. For instance, to use address conversion functionality discussed in this chapter, you will also need to include Ws2tcpip.h.

On POSIX platforms, there is only one version of the socket library and it is usually accessed by including the file sys/socket.h. To use IPv4-specific functionality you may also have to include netinet/in.h. To use address conversion functionality, include arpa/inet.h. To perform name resolution you may have to include netdb.h.

Initialization and shutdown of the socket library also differ between platforms. On POSIX platforms, the library is active by default and nothing is required to enable socket functionality. Winsock2, however, requires explicit startup and cleanup and allows the user to specify what version of the library to use. To activate the socket library on Windows, use WSAStartup:

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

wVersionRequested is a 2-byte word in which the low-order byte specifies the major version and the high-order byte specifies the minor version of the Winsock implementation desired. The highest version supported as of this printing is 2.2, so typically you will pass MAKEWORD(2, 2) for this parameter.

lpWSAData points to a Windows-specific data structure which the WSAStartup function fills in with data about the activated library, including the version of the implementation provided. Typically this will match the version requested and you will not usually need to check this data.

WSAStartup returns either a 0, indicating success, or an error code, indicating why the library could not be started up. Note that no Winsock2 functions will work properly unless your process first invokes WSAStartup successfully.

To shut down the library, call WSACleanup:

int WSACleanup();

WSACleanup takes no parameters and returns an error code. When a process invokes WSACleanup, all pending socket operations are terminated and all socket resources are deallocated, so it is a good idea to make sure all sockets are closed and truly unused before shutting down Winsock.WSAStartup is reference counted, so you must call WSACleanup exactly as many times you called WSAStartup to make sure that anything is actually cleaned up.

Error reporting is handled slightly differently between platforms. Most functions on all platforms return −1 in the case of an error. On Windows, you can use the macro SOCKET_ERROR instead of the magic number −1. A single −1 does little to reveal the source of the error, though, so Winsock2 provides the function WSAGetLastError to fetch an additional code that expands on the cause of the error:

int WSAGetLastError();

This function returns only the latest error code generated on the currently running thread, so it is important to check it immediately after any socket library function returns a −1. Calling a successive socket function after an error could cause a secondary error due to the initial one. This would change the result returned by WSAGetLastError and mask the true cause of the problem.

POSIX-compatible libraries similarly provide a method to retrieve specific error information. However, these use the C standard library global variable errno to report specific error codes. To check the value of errno from code, you must include the file errno.h. After that, you can read from errno like any other variable. Just like the result returned by WSAGetLastError, errno can change after every function call, so it is important to check it at the first sign of error.


Tip

Most platform-independent functions in the socket library use purely lowercase letters, like socket. Most Windows-specific Winsock2 functions, however, begin with capital letters, and sometimes the WSA prefix, to mark them as nonstandard. When developing for Windows, try to keep capital letter Winsock2 functions isolated from the cross-platform ones so that porting to POSIX platforms will be simpler.


There are additional Winsock2-specific functions that are not supported by the POSIX version of the Berkeley Socket library, just like most POSIX-compatible operating systems have their own platform-specific networking functions in addition to the POSIX standard ones. The standard socket functions provide adequate functionality for a typical multiplayer networked game, so for the rest of this chapter, we will explore only the standard, cross-platform functions. The sample code for this book targets the Windows Operating System but uses Winsock2-specific functions only when necessary, to start up, shut down, and check for errors. The text will call out multiple versions whenever a function differs across platforms.

Socket Address

Every network layer packet requires a source address and a destination address. If the packet wraps transport layer data, it also requires a source port and a destination port. To pass this address information in and out of the socket library, the API provides the sockaddr data type:

struct sockaddr {
uint16_t sa_family;
char sa_data[14];
};

sa_family holds a constant identifying the type of the address. When using this socket address with a socket, the sa_family field should match the af parameter used to create the socket. sa_data is 14 bytes which hold the actual address. The sa_data field is a necessarily generic array of bytes because it must be able to hold the address format appropriate for whatever address family is specified. Technically, you could fill in the bytes manually, but this would require knowing the memory layout for various address families. To remedy this, the API provides dedicated data types to help initialize addresses for common address families. Because there were no classes or polymorphic inheritance at the time of the socket API’s creation, these data types must be manually cast to the sockaddr type when passed into any socket API function that requires an address. To create an address for an IPv4 packet, use the sockaddr_in type:

struct sockaddr_in {
short sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

sin_family overlaps sockaddr’s sa_family and thus has the same meaning.

sin_port holds the 16-bit port section of the address.

sin_addr holds the 4-byte IPv4 address. The in_addr type varies between socket libraries. On some platforms, it is a simple 4-byte integer. IPv4 addresses are not usually written as 4-byte integers, but instead as 4 individual bytes separated with dots. For this reason, other platforms provide a structure that wraps a union of structs that can be used to set the address in different formats:

struct in_addr {
union {
struct {
uint8_t s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct {
uint16_t s_w1,s_w2;
} S_un_w;
uint32_t S_addr;
} S_un;
};

By setting the s_b1, s_b2, s_b3, and s_b4 fields of the S_un_b struct inside the S_un union, you can enter the address in a human readable form.

sin_zero is unused and merely exists to pad the size of sockaddr_in to match the size of sockaddr. For consistency, it should be set to all zeroes.


Tip

In general, when instancing any BSD socket struct, it is a wise idea to use memset to zero out all its members. This can help prevent cross-platform errors from uninitialized fields that arise when one platform uses fields that another platform ignores.


When setting the IP address as a 4-byte integer, or when setting the port number, it is important to account for that fact the TCP/IP suite and the host computer may use different standards for the ordering of bytes within multibyte numbers. Chapter 4, “Object Serialization,” provides an in-depth look at platform-dependent byte ordering, but for now, it is sufficient to know that any multibyte numbers set in a socket address structure must be converted from host byte order to network byte order. To facilitate this, the socket API provides the functions htons and htonl:

uint16_t htons( uint16_t hostshort );
uint32_t htonl( uint32_t hostlong );

The htons function takes any unsigned, 16-bit integer in the host’s native byte order and converts it to the same integer represented in the network’s native byte order. The htonl function performs the same operation on 32-bit integers.

On platforms where the host byte order and network byte order are the same, these functions will do nothing. When optimizations are turned on, the compiler will recognize this fact and omit the function calls without generating any extra code. On platforms where the host byte order does not match the network byte order, the returned values will have the same bytes as the input parameters, but their order will be swapped. This means that if you are on such a platform, and you use the debugger to examine the sa_port field of a properly initialized sockaddr_in, the decimal value represented there will not match that of your intended port. Instead it will be the decimal value of a byte-swapped version of your port.

Sometimes, as in the case of receiving a packet, the socket library fills in the sockaddr_in structure for you. When this happens, the sockadd_in fields will still be in network byte order, so if you wish to extract them and make sense of them, you should use the functions ntohs andntohl to convert the values from network byte order to host byte order:

uint16_t ntohs(uint16_t networkshort);
uint32_t ntohl(uint32_t networklong);

These two functions work the same way as their host-to-network counterparts.

Putting all these techniques together, Listing 3.1 shows how to create a socket address that represents port 80 at IP address 65.254.248.180.

Listing 3.1 Initializing a sockaddr_in


sockaddr_in myAddr;
memset(myAddr.sin_zero, 0, sizeof(myAddr.sin_zero));
myAddr.sin_family = AF_INET;
myAddr.sin_port = htons(80);
myAddr.sin_addr.S_un.S_un_b.s_b1 = 65;
myAddr.sin_addr.S_un.S_un_b.s_b2 = 254;
myAddr.sin_addr.S_un.S_un_b.s_b3 = 248;
myAddr.sin_addr.S_un.S_un_b.s_b4 = 180;



Note

Some platforms add an extra field in the sockaddr to store the length of the structure used. This is to enable longer-length sockaddr structures in the future. On these platforms, just set the length to the sizeof the structure used. For instance, on Mac OS X, initialize asockaddr_in named myAddr by setting myAddr.sa_len = sizeof(sockaddr_in).


Type Safety

Because there was very little consideration for type safety when the initial socket library was created, it can be useful to wrap the basic socket data types and functions with custom object-oriented ones, implemented at the application level. This also helps isolate the socket API from your game, in case you decide to change out the socket library for some alternative networking library at a later date. In this book, we will be wrapping many structs and functions both as a way to demonstrate proper use of the underlying API and to provide a more type-safe framework on which you can build your own code. Listing 3.2 presents a wrapper for the sockaddr structure.

Listing 3.2 Type-Safe SocketAddress Class


class SocketAddress
{
public:
SocketAddress(uint32_t inAddress, uint16_t inPort)
{
GetAsSockAddrIn()->sin_family = AF_INET;
GetAsSockAddrIn()->sin_addr.S_un.S_addr = htonl(inAddress);
GetAsSockAddrIn()->sin_port = htons(inPort);
}
SocketAddress(const sockaddr& inSockAddr)
{
memcpy(&mSockAddr, &inSockAddr, sizeof( sockaddr) );
}

size_t GetSize() const {return sizeof( sockaddr );}

private:
sockaddr mSockAddr;

sockaddr_in* GetAsSockAddrIn()
{return reinterpret_cast<sockaddr_in*>( &mSockAddr );}
};
typedef shared_ptr<SocketAddress> SocketAddressPtr;


SocketAddress has two constructors. The first takes a 4-byte IPv4 address and port and assigns the value to an internal sockaddr. The constructor automatically sets the address family to AF_INET because the parameters are only sensible for an IPv4 address. To support IPv6, you could extend this class with another constructor.

The second constructor takes a native sockaddr and copies it into the internal mSockAddr field. This is useful when the network API returns a sockaddr and you wish to wrap it with a SocketAddress.

The GetSize helper method of SocketAddress keeps the code clean when dealing with functions that need the size of the sockaddr.

Finally, the shared pointer type to a socket address ensures there is an easy way to share socket addresses without having to worry about cleaning up the memory. At the moment, SocketAddress wraps very little, but it provides a good base on which to add more functionality as future examples require it.

Initializing sockaddr from a String

It takes a bit of work just to feed an IP address and port into a socket address, especially considering that the address information will probably be passed to your program as a string in a config file or on a commandline. If you do have a string to turn into a sockaddr, you can skip this work by using the inet_pton function on POSIX-compatible systems or the InetPton function on Windows.

int inet_pton(int af, const char* src, void* dst);
int InetPton(int af, const PCTSTR src void* dst);

Both functions take an address family, either AF_INET or AF_INET6, and convert a string representation of an IP address into an in_addr representation. src should point to a null terminated character string containing the address in dotted notation and dst should point to thesin_addr field of the sockaddr to be set. The functions return 1 on success, 0 if the source string is malformed, or −1 if some other system error occurred. Listing 3.3 shows how to initialize a sockaddr using one of these presentation-to-network functions.

Listing 3.3 Initializing sockaddr with InetPton


sockaddr_in myAddr;
myAddr.sin_family = AF_INET;
myAddr.sin_port = htons( 80 );
InetPton(AF_INET, "65.254.248.180", &myAddr.sin_addr);


Although inet_pton converts a human readable string to a binary IP address, the string must be an IP address. It cannot be a domain name, as no DNS lookup is performed. If you wish to perform a simple DNS query to translate a domain name into an IP address, use getaddrinfo:

int getaddrinfo(const char *hostname, const char *servname, const addrinfo
*hints, addrinfo **res);

hostname should be a null terminated string holding the name of the domain to look up. For instance, “live-shore-986.herokuapp.com.”

servname should be a null terminated string containing either a port number, or the name of a service which maps to a port number. For instance, you can send either “80” or “http” to request a sockaddr_in containing port 80.

hints should be a pointer to an addrinfo structure containing information about the results you wish to receive. You can specify a desired address family or other requirement using this parameter, or you can just pass nullptr to get all matching results.

Finally, res should be a pointer to a variable that the function will set to point to the head of a linked list of newly allocated addrinfo structures. Each addrinfo represents a section of the response from the DNS server:

struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
sockaddr *ai_addr;
addrinfo *ai_next;
}

ai_flags, ai_socktype, and ai_protocol are used to request certain types of responses when you pass an addrinfo into getaddrinfo as a hint. They can be ignored in the response.

ai_family identifies the address family to which this addrinfo pertains. A value of AF_INET indicates an IPv4 address and a value of AF_INET6 indicates an IPv6 address.

ai_addrlen gives the size of the sockaddr pointed to by ai_addr.

ai_canonname holds the canonical name of the resolved hostname, if the AI_CANONNAME flag is set in the ai_flags field of the addrinfo passed as hints in the original call.

ai_addr contains a sockaddr of the given address family, which addresses the host specified by the hostname and the port specified by the servname parameters of the original call.

ai_next points to the next addrinfo in the linked list. Because a domain name can map to multiple IPv4 and IPv6 addresses, you should iterate through the linked list until you find a sockaddr that suits your needs. Alternatively, you can specify the ai_family in the addrinfopassed as a hint and you will receive results for only the desired family. The final addrinfo in the list will have nullptr as its ai_next to indicate it is the tail.

Because getaddrinfo allocates one or more addrinfo structures, you should call freeaddrinfo to release the memory once you have copied the desired sockaddr out of the linked list:

void freeaddrinfo(addrinfo* ai);

In ai, pass only the very first addrinfo returned by getaddrinfo. The function will walk the linked list freeing up all addrinfo nodes and all associated buffers.

To resolve a host name into an IP address, getaddrinfo creates a DNS protocol packet and sends it using either UDP or TCP to one of the DNS servers configured in the operating system. It then waits for a response, parses the response, constructs the linked list of addrinfo structures, and returns this to the caller. Because this process is dependent on sending information to and receiving information from a remote host, it can take a significant amount of time. Sometimes this is on the order of milliseconds, but more often it is on the order of seconds. getaddrinfo has no provisions for asynchronous operation built in, so it will block the calling thread until it receives a response. This can cause an undesirable experience for the user, so if you need to resolve hostnames into IP addresses, you should consider calling getaddrinfo on a thread other than the main thread of your game. On Windows, you can alternatively call the Windows-specific GetAddrInfoEx function, which does allow for asynchronous operation without manually creating a different thread.

You can encapsulate the functionality of getaddrinfo nicely in the SocketAddressFactory given in Listing 3.4.

Listing 3.4 Name Resolution Using the SocketAddressFactory


class SocketAddressFactory
{
public:
static SocketAddressPtr CreateIPv4FromString(const string& inString)
{
auto pos = inString.find_last_of(':');
string host, service;
if(pos != string::npos)
{
host = inString.substr(0, pos);
service = inString.substr(pos + 1);
}
else
{
host = inString;
//use default port...
service = "0";
}
addrinfo hint;
memset(&hint, 0, sizeof(hint));
hint.ai_family = AF_INET;

addrinfo* result;
int error = getaddrinfo(host.c_str(), service.c_str(),
&hint, &result);
if(error != 0 && result != nullptr)
{
freeaddrinfo(result);
return nullptr;
}

while(!result->ai_addr && result->ai_next)
{
result = result->ai_next;
}
if(!result->ai_addr)
{
freeaddrinfo(result);
return nullptr;
}
auto toRet = std::make_shared< SocketAddress >(*result->ai_addr);

freeaddrinfo(result);

return toRet;
}
};


SocketAddressFactory has a single static method to create a SocketAddress from a string representing a host name and port. The function returns a SocketAddressPtr so that it has the option of returning nullptr if anything goes wrong with the name conversion. This is a nice alternative to making a SocketAddress constructor do the conversion because, without requiring exception handling, it makes sure there is never an incorrectly initialized SocketAddress in existence: If CreateIPv4FromString returns a non-null pointer, then it is guaranteed to be a valid SocketAddress.

The method first separates the port from the name by searching for a colon. It then creates a hint addrinfo to ensure that only IPv4 results are returned. It feeds all this into getaddrinfo and iterates through the resulting list until a non-null address is found. It copies this address into a new SocketAddress using the appropriate constructor and then frees the linked list. If anything goes wrong, it returns null.

Binding a Socket

The process of notifying the operating system that a socket will use a specific address and transport layer port is known as binding. To manually bind a socket to an address and port, use the bind function:

int bind(SOCKET sock, const sockaddr *address, int address_len);

sock is the socket to bind, previously created by the socket function.

address is the address to which the socket should bind. Note that this has nothing to do with the address to which the socket will send packets. You can think of this as defining the return address of any packets sent from the socket. It may seem curious that you must specify a return address at all, since any packets sent from this host are clearly coming from this host’s address. However, remember that a host can have multiple network interfaces, and each interface can have its own IP address.

Passing a specific address to bind allows you to determine which interface the socket should use. This is especially useful for hosts that serve as routers or bridges between networks, as their different interfaces may be connected to entirely different sets of computers. For multiplayer game purposes, it is usually not important to specify a network interface, and in fact often desirable to bind a given port for all available network interfaces and IP addresses that the host has. To do this, you can assign the macro INADDR_ANY to the sin_addr field of the sockaddr_in that you pass to bind.

address_len should contain the size of the sockaddr passed as the address.

bind returns 0 on success, or −1 in case of an error.

Binding a socket to a sockaddr serves two functions. First, it tells the OS that this socket should be the target recipient for any incoming packet with a destination matching the socket’s bound address and port. Second, it dictates the source address and port that the socket library should use when creating network and transport layer headers for packets sent out from the socket.

Typically you can only bind a single socket to a given address and port. bind will return an error if you try to bind to an address and port already in use. In that case, you can repeatedly try binding different ports until you find one that is not in use. To automate this process, you can specify 0 for the port to bind. This tells the library to find an unused port and bind that.

A socket must be bound before it can be used to transmit or receive data. Because of this, if a process attempts to send data using an unbound socket, the network library will automatically bind that socket to an available port. Therefore, the only reason to manually call bind is to specify the bound address and port. This is necessary when building a server that must listen for packets on a publically announced address and port, but usually not necessary for a client. A client can automatically bind to any available port: When it sends its first packet to the server, the packet will contain the automatically chosen source address and port, and the server can use those to address any return packets correctly.

UDP Sockets

You can send data on a UDP socket as soon as the socket is created. If it is not bound, the network module will find a free port in the dynamic port range and automatically bind it. To send data, use the sendto function:

int sendto(SOCKET sock, const char *buf, int len, int flags,
const sockaddr *to, int tolen);

sock is the socket from which the datagram should send. If the socket is unbound, the library will automatically bind it to an available port. The socket’s bound address and port will be used as the source address in the headers of the outgoing packet.

buf is a pointer to the starting address of the data to send. It does not have to be an actual char*. It can be any type of data as long as it is cast appropriately to a char*. Because of this, void* would have been a more appropriate data type for this parameter, so it is useful to think of it that way.

len is the length of data to send. Technically the maximum length of a UDP datagram including its 8-byte header is 65535 bytes, because the length field in the header holds only 16 bits. However, remember that the link layer’s MTU determines the largest packet that can be sent without fragmentation. The MTU for Ethernet is 1500 bytes, but this must include not only the game’s payload data, but also multiple headers and potentially any packet wrappers. As a game programmer trying to avoid fragmentation, a good rule of thumb is to avoid sending datagrams with data larger than 1300 bytes.

flags is a bitwise OR collection of flags controlling the sending of data. For most game play code, this should be 0.

to is the sockaddr of the intended recipient. This sockaddr’s address family must match the one used to create the socket. The address and port from the to parameter are copied into the IP header and UDP header as the destination IP address and destination port.

len is the length of the sockaddr passed as the to parameter. For IPv4, just pass sizeof(sockaddr_in).

If the operation is successful, sendto returns the length of the data queued to send. Otherwise it returns −1. Note that a nonzero return value doesn’t actually mean the datagram was sent, just that it was successfully queued to be sent.

Receiving data on a UDP socket is a simple matter of using the recvfrom function:

int recvfrom(SOCKET sock, char *buf, int len, int flags, sockaddr *from,
int *fromlen);

sock is the socket to query for data. By default, if no unread datagrams have been sent to the socket, the thread will block until a datagram arrives.

buf is the buffer into which the received datagram should be copied. By default, once a datagram has been copied into a buffer through a recvfrom call, the socket library no longer keeps a copy of it.

len should specify the maximum number of bytes the buf parameter can hold. To avoid a buffer overflow error, recvfrom will never copy more than this number of bytes into buf. Any remaining bytes in the incoming datagram will be lost for good, so make sure to always use a receiving buffer as large as the largest datagram you expect to receive.

flags is a bitwise OR collection of flags controlling the receiving of data. For most game play code, this should be 0. One occasionally useful flag is the MSG_PEEK flag. This will copy a received datagram into the buf parameter without removing any data from the input queue. That way, the next recvfrom call, potentially with a larger buffer, can refetch the same datagram.

from should be a pointer to a sockaddr structure that the recvfrom function can fill in with the sender’s address and port. Note that this structure does not need to be initialized ahead of time with any address information. It is a common misconception that one can specifically request a packet from a particular address by filling in this parameter, but no such thing is possible. Instead, datagrams are delivered to the recvfrom function in the order received, and the from variable is set to the corresponding source address for each datagram.

fromlen should point to an integer holding the length of the sockaddr passed in as from. recvfrom may reduce this value if it doesn’t need all the space to copy the source address.

After successful execution, recvfrom returns the number of bytes that were copied into buf. If there was an error, it returns −1.

Type-Safe UDP Sockets

Listing 3.5 shows the type-safe UDPSocket class, capable of binding an address and sending and receiving datagrams.

Listing 3.5 Type-Safe UDPSocket Class


class UDPSocket
{
public:
~UDPSocket();
int Bind(const SocketAddress& inToAddress);
int SendTo(const void* inData, int inLen, const SocketAddress& inTo);
int ReceiveFrom(void* inBuffer, int inLen, SocketAddress& outFrom);
private:
friend class SocketUtil;
UDPSocket(SOCKET inSocket) : mSocket(inSocket) {}
SOCKET mSocket;
};
typedef shared_ptr<UDPSocket> UDPSocketPtr;

int UDPSocket::Bind(const SocketAddress& inBindAddress)
{
int err = bind(mSocket, &inBindAddress.mSockAddr,
inBindAddress.GetSize());
if(err != 0)
{
SocketUtil::ReportError(L"UDPSocket::Bind");
return SocketUtil::GetLastError();
}
return NO_ERROR;
}

int UDPSocket::SendTo(const void* inData, int inLen,
const SocketAddress& inTo)
{
int byteSentCount = sendto( mSocket,
static_cast<const char*>( inData),
inLen,
0, &inTo.mSockAddr, inTo.GetSize());
if(byteSentCount >= 0)
{
return byteSentCount;
}
else
{
//return error as negative number
SocketUtil::ReportError(L"UDPSocket::SendTo");
return -SocketUtil::GetLastError();
}
}

int UDPSocket::ReceiveFrom(void* inBuffer, int inLen,
SocketAddress& outFrom)
{
int fromLength = outFromAddress.GetSize();
int readByteCount = recvfrom(mSocket,
static_cast<char*>(inBuffer),
inMaxLength,
0, &outFromAddress.mSockAddr,
&fromLength);
if(readByteCount >= 0)
{
return readByteCount;
}
else
{
SocketUtil::ReportError(L"UDPSocket::ReceiveFrom");
return -SocketUtil::GetLastError();
}
}

UDPSocket::~UDPSocket()
{
closesocket(mSocket);
}


The UDPSocket class has three main methods: Bind, SendTo, and ReceiveFrom. Each makes use of the SocketAddress class previously defined. To make this possible, SocketAddress must declare UDPSocket a friend class so that the methods can access the privatesockaddr member variable. Treating SocketAddress this way makes sure no code outside of this socket wrapper module can edit sockaddr directly, which reduces dependencies and prevents potential errors.

A nice benefit of the object-oriented wrapper is the ability to create destructors. In this case, ~UDPSocket automatically closes the internally wrapped socket to prevent sockets from leaking.

The UDPSocket code in Listing 3.5 introduces a dependency on the SocketUtil class for reporting errors. Isolating error reporting code this way makes it easy to change error handling behavior and cleanly wraps the fact that some platforms take their errors from WASGetLastErrorand some from errno.

The code does not provide a way to create a UDPSocket from scratch. The only constructor on UDPSocket is private. Similarly to the SocketAddressFactory pattern, this is so that there is no way to create a UDPSocket with an invalid mSocket inside it. Instead, theSocketUtil::CreateUDPSocket function in Listing 3.6 will create a UDPSocket only after the underlying socket call succeeds.

Listing 3.6 Creating a UDP Socket


enum SocketAddressFamily
{
INET = AF_INET,
INET6 = AF_INET6
};
UDPSocketPtr SocketUtil::CreateUDPSocket(SocketAddressFamily inFamily)
{
SOCKET s = socket(inFamily, SOCK_DGRAM, IPPROTO_UDP);
if(s != INVALID_SOCKET)
{
return UDPSocketPtr(new UDPSocket(s));
}
else
{
ReportError(L"SocketUtil::CreateUDPSocket");
return nullptr;
}
}


TCP Sockets

UDP is stateless, connectionless, and unreliable, so it needs only a single socket per host to send and receive datagrams. TCP, on the other hand is reliable, and requires a connection to be established between two hosts before data transmission can take place. In addition, it must maintain state to resend dropped packets and it has to store that state somewhere. In the Berkeley Socket API, the socket itself stores the connection state. This means a host needs an additional, unique socket for each TCP connection it maintains.

TCP requires a three-stage handshake to initiate a connection between a client and a server. For the server to receive the initial stage of the handshake, it must first create a socket, bind it to a designated port, and then listen for any incoming handshakes. Once it has created and bound the socket using socket and bind, it begins listening using the listen function:

int listen(SOCKET sock, int backlog);

sock is the socket to set into listen mode. Each time a socket in listen mode receives the first stage of a TCP handshake, it stores the request until the owning process makes a call to accept the connection and continue the handshake.

backlog is the maximum number of incoming connections that should be allowed to queue up. Once the maximum number of handshakes are pending, any further incoming connection is dropped. Pass SOMAXCONN to use the default backlog value.

The function returns 0 on success, or −1 in case of error.

To accept an incoming connection and continue the TCP handshake, call accept:

SOCKET accept(SOCKET sock, sockaddr* addr, int* addrlen);

sock is the listening socket on which an incoming connection should be accepted.

addr is a pointer to a sockaddr structure that will be filled in with the address of the remote host requesting the connection. Similarly to the address passed into recvfrom, this sockaddr does not need to be initialized and it does not control which connection is accepted. It merely receives the address of the accepted connection.

addrlen should be a pointer to the size in bytes of the addr buffer. It will be updated by accept with the size of the address actually written.

If accept succeeds, it creates and returns a new socket which can be used to communicate with the remote host. This new socket is bound to the same port as the listening socket. When the OS receives a packet destined for the bound port, it uses the source address and source port to determine which socket should receive the packet: Remember that TCP requires a host to have a unique socket for each remote host to which it is connected.

The new socket returned by accept is associated with the remote host which initiated the connection. It stores the remote host’s address and port, and tracks all outgoing packets so they can be resent if dropped. It is also the only socket which can communicate with the remote host: A process should never attempt to send data to a remote host using the initial socket in listen mode. That will fail, as the listen socket is never connected to anything. It only acts as a dispatcher to help create new sockets in response to incoming connection requests.

By default, if there are no connections ready to accept, accept will block the calling thread until an incoming connection is received or the attempt times out.

The process of listening for and accepting connections is an asymmetrical one. Only the passive server needs a listen socket. A client wishing to initiate a connection should instead create a socket and use the connect function to begin the handshake process with a remote server:

int connect(SOCKET sock, const sockaddr *addr, int addrlen);

sock is the socket on which to connect.

addr is a pointer to the address of the desired remote host.

addrlen is the length of the addr parameter.

On success, connect returns 0. If there is an error, it returns −1.

Calling connect initiates the TCP handshake by sending the initial SYN packet to a target host. If that host has a listen socket bound to the appropriate port, it can proceed with the handshake by calling accept. By default, a call to connect will block the calling thread until the connection is accepted or the attempt times out.

Sending and Receiving via Connected Sockets

A connected TCP socket stores the remote host’s address information. Because of this, a process does not need to pass an address with each call to transmit data. Instead of using sendto, send data through a connected TCP socket using the send function:

int send(SOCKET sock, const char *buf, int len, int flags)

sock is the socket on which the data should be sent.

buf is a buffer of data to write to the stream. Note that unlike for UDP, buf is not a datagram and not guaranteed to be transferred as a single data unit. Instead, the data is just appended to the socket’s outgoing buffer, and transferred sometime in the future at the socket library’s whim. If the Nagle algorithm is active, as described in Chapter 2, this may not happen until an MSS worth of data has accumulated.

len is the number of bytes to transmit. Unlike for UDP, there is no reason to keep this value below the expected MTU of the link layer. As long as there is room in the socket’s send buffer, the network library will append the data and then send it out in whatever chunk sizes it deems appropriate.

flags is a bitwise OR collection of flags controlling the sending of data. For most game play code, this should be 0.

If the send call succeeds, it returns the amount of data sent. This may be less than the len parameter, if the socket’s send buffer had some space free but not enough to hold the entire buf. If there is no room at all, then by default the calling thread will block until the call times out, or enough packets are sent for there to be room. If there is an error, send returns −1. Note that a nonzero return value does not imply any data was sent, just that data was queued to be sent.

To receive data on a connected TCP socket, call recv:

int recv(SOCKET sock, char *buf, int len, int flags);

sock is the socket to check for data.

buf is the buffer into which the data should be copied. The copied data is removed from the socket’s receive buffer.

len is the maximum amount of received data to copy into buf.

flags is a bitwise OR collection of flags controlling the receiving of data. Any flags usable with recvfrom are also usable with recv. For most game play code, this should be 0.

If the recv call is successful, it returns the number of bytes received. This will be less than or equal to len. It is not possible to predict the amount of data received based on remote calls to send: The network library on the remote host accumulates the data and sends out segments sized as it sees fit. If recv returns zero when len is nonzero, it means the other side of the connection has sent a FIN packet and promises to send no more data. If recv returns zero when len is zero, it means there is data on the socket ready to be read. With many sockets in use, this can be a handy way to check for the presence of data without having to dedicate a buffer to the task. Once recv has indicated there is data available, you can reserve a buffer and then call recv again, passing the buffer and a nonzero len.

If there is an error, recv returns −1.

By default, if there is no data in the socket’s receive buffer, recv blocks the calling thread until the next segment in the stream arrives or the call times out.


Note

You can actually use sendto and recvfrom on a connected socket if you want. However, the address parameters will be ignored and this can be confusing. Similarly, on some platforms it is possible to call connect on a UDP socket to store a remote host’s address and port in the socket’s connection data. This doesn’t establish a reliable connection, but it does allow the use of send to transmit data to the stored host without having to specify the address each time. It also causes the socket to discard incoming datagrams from any host other than the stored one.


Type-Safe TCP Sockets

The type-safe TCPSocket looks similar to UDPSocket, but with additional connection-oriented functionality wrapped. Listing 3.7 gives the implementation.

Listing 3.7 Type-Safe TCPSocket Class


class TCPSocket
{
public:
~TCPSocket();
int Connect(const SocketAddress& inAddress);
int Bind(const SocktetAddress& inToAddress);
int Listen(int inBackLog = 32);
shared_ptr< TCPSocket > Accept(SocketAddress& inFromAddress);
int Send(const void* inData, int inLen);
int Receive(void* inBuffer, int inLen);
private:
friend class SocketUtil;
TCPSocket(SOCKET inSocket) : mSocket(inSocket) {}
SOCKET mSocket;
};
typedef shared_ptr<TCPSocket> TCPSocketPtr;

int TCPSocket::Connect(const SocketAddress& inAddress)
{
int err = connect(mSocket, &inAddress.mSockAddr, inAddress.GetSize());
if(err < 0)
{
SocketUtil::ReportError(L"TCPSocket::Connect");
return -SocketUtil::GetLastError();
}
return NO_ERROR;
}
int TCPSocket::Listen(int inBackLog)
{
int err = listen(mSocket, inBackLog);
if(err < 0)
{
SocketUtil::ReportError(L"TCPSocket::Listen");
return -SocketUtil::GetLastError();
}
return NO_ERROR;
}

TCPSocketPtr TCPSocket::Accept(SocketAddress& inFromAddress)
{
int length = inFromAddress.GetSize();
SOCKET newSocket = accept(mSocket, &inFromAddress.mSockAddr, &length);

if(newSocket != INVALID_SOCKET)
{
return TCPSocketPtr(new TCPSocket( newSocket));
}
else
{
SocketUtil::ReportError(L"TCPSocket::Accept");
return nullptr;
}
}

int TCPSocket::Send(const void* inData, int inLen)
{
int bytesSentCount = send(mSocket,
static_cast<const char*>(inData ),
inLen, 0);
if(bytesSentCount < 0 )
{
SocketUtil::ReportError(L"TCPSocket::Send");
return -SocketUtil::GetLastError();
}
return bytesSentCount;
}

int TCPSocket::Receive(void* inData, int inLen)
{
int bytesReceivedCount = recv(mSocket,
static_cast<char*>(inData), inLen, 0);
if(bytesReceivedCount < 0)
{
SocketUtil::ReportError(L"TCPSocket::Receive");
return -SocketUtil::GetLastError();
}
return bytesReceivedCount;
}


TCPSocket contains the TCP-specific methods: Send, Receive, Connect, Listen, and Accept. Bind and the destructor are no different from the UDPSocket versions, so they are not shown. Accept returns a TCPSocketPtr to ensure the new socket closes automatically when no longer referenced. Send and Receive do not require addresses because they automatically use the address stored in the connected socket.

To enable creation of a TCPSocket, you must add a CreateTCPSocket function to SocketUtils.

Blocking and Non-Blocking I/O

Receiving from a socket is typically a blocking operation. If there is no data ready to be received, the thread will block until data comes in. This is undesirable if you are polling for packets on the main thread. Sending, accepting, and connecting can also block if the socket is not ready to perform the operation. This raises issues for a real-time application, like a game, that needs to check for incoming data without reducing the frame rate. Imagine a game server with TCP connections to five clients. If the server calls recv on one of its sockets to check for new data from the corresponding client, the server’s thread will pause until that client sends some data. This prevents the server from checking on its other sockets, accepting new connections on its listen socket, and running the game simulation. Clearly a game cannot ship that way. Luckily there are three common ways to work around this issue: multithreading, non-blocking I/O, and the select function.

Multithreading

One way to work around the problem of blocking I/O is to put each potentially blocking call on its own thread. In the example mentioned earlier, the server would need at least seven threads total: one for each client connection, one for the listen socket, and one or more for the simulation.Figure 3.1 shows the process.

Image

Figure 3.1 Multithreading process

At startup, the listen thread creates a socket, binds it, calls listen, and then calls accept. The accept call blocks until a client tries to connect. When a client does connect, the accept call returns a new socket. The server process spawns a new thread for this socket, which loops callingrecv. The recv call blocks until the client sends data. When the client sends data, the recv call unblocks and the unblocked thread uses some callback mechanism to send the new client data to the main thread before looping back and calling recv again. Meanwhile, the listen socket keeps blocking while accepting new connections, and the main thread runs the simulation.

This works, but has the drawback of requiring one thread per client, which does not scale well as the number of clients grows. It also can get hard to manage, as all client data comes in on parallel threads and needs to be passed to the simulation in a safe manner. Finally, if the simulation thread tries to send data on a socket at the same time the receive thread is receiving on that socket, it will still block the simulation. These are not insurmountable problems, but there are simpler alternatives.

Non-Blocking I/O

By default, sockets operate in blocking mode, as previously mentioned. However, sockets also support non-blocking mode. When a socket in non-blocking mode is asked to perform an operation that would otherwise require blocking, it instead returns immediately, with a result of −1. It also sets the system error code, errno or WSAGetLastError, to return a value of EAGAIN or WASEWOULDBLOCK, respectively. This code signifies that the previous socket action would have blocked and was aborted without taking place. The calling process can then react accordingly.

To set a socket into non-blocking mode on Windows, use the ioctlsocket function:

int ioctlsocket(SOCKET sock, long cmd, u_long *argp);

sock is the socket to place in non-blocking mode.

cmd is the socket parameter to control. In this case, pass FIONBIO.

argp is the value to set for the parameter. Any nonzero value will enable non-blocking mode, and zero will disable it.

On a POSIX-compatible operating system, use the fcntl function:

int fcntl(int sock, int cmd, . . .);

sock is the socket to place in non-blocking mode.

cmd is the command to issue to the socket. On newer POSIX systems, you must first use F_GETFL to fetch the flags currently associated with the socket, bitwise OR them with the constant O_NONBLOCK, and then use the F_SETFL command to update the flags on the socket. Listing 3.8shows how to add a method to enable non-blocking mode on the UDPSocket.

Listing 3.8 Enabling Non-Blocking Mode for a Type-Safe Socket


int UDPSocket::SetNonBlockingMode(bool inShouldBeNonBlocking)
{
#if _WIN32
u_long arg = inShouldBeNonBlocking ? 1 : 0;
int result = ioctlsocket(mSocket, FIONBIO, &arg);
#else
int flags = fcntl(mSocket, F_GETFL, 0);
flags = inShouldBeNonBlocking ?
(flags | O_NONBLOCK):(flags & ~O_NONBLOCK);
fcntl(mSocket, F_SETFL, flags);
#endif

if(result == SOCKET_ERROR)
{
SocketUtil::ReportError(L"UDPSocket::SetNonBlockingMode");
return SocketUtil::GetLastError();
}
else
{
return NO_ERROR;
}
}


When a socket is in non-blocking mode, it is safe to call any usually blocking function and know that it will return immediately if it cannot complete without blocking. A typical game loop using a non-blocking socket might look something like Listing 3.9.

Listing 3.9 Game Loop Using a Non-Blocking Socket


void DoGameLoop()
{
UDPSocketPtr mySock = SocketUtil::CreateUDPSocket(INET);
mySock->SetNonBlockingMode(true);

while(gIsGameRunning)
{
char data[1500];
SocketAddress socketAddress;

int bytesReceived = mySock->ReceiveFrom(data, sizeof(data),
socketAddress);
if(bytesReceived> 0)
{
ProcessReceivedData(data, bytesReceived, socketAddress);
}
DoGameFrame();
}
}


With the socket set to non-blocking mode, the game can check in each frame to see if any data is ready to be received. If there is data, the game processes the first pending datagram. If there is none, the game immediately moves on to the rest of the frame without waiting. If you want to process more than just the first datagram, you can add a loop which reads pending datagrams until it has read a maximum number, or there are no more present. It is important to limit the number of datagrams read per frame. If you do not, a malicious client could send a slew of single-byte datagrams faster than the server can process them, effectively halting the server from simulating the game.

Select

Polling non-blocking sockets each frame is a simple and straightforward way to check for incoming data without blocking a thread. However, when the number of sockets to poll is large, this can become inefficient. As an alternative, the socket library provides a way to check many sockets at once, and take action as soon as any one of them becomes ready. To do this, use the select function:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
const timeval *timeout);

On POSIX platforms, nfds should be the socket identifier of the highest numbered socket to check. On POSIX, each socket is just an integer, so this is simply the maximum of all sockets passed in to this function. On Windows, where sockets are represented by pointers instead of integers, this parameter does nothing and can be ignored.

readfds is a pointer to a collection of sockets, known as an fd_set, which should contain sockets to check for readability. Information on how to construct an fd_set follows. When a packet arrives for a socket in the readfds set, select returns control to the calling thread as soon as it is able to. It first removes all sockets from the set that have not received a packet. Thus, when select returns, a read from any socket still in the readfds set is guaranteed not to block. Pass nullptr for readfds to skip checking any sockets for readability.

writefds is a pointer to an fd_set filled with sockets to check for writability. When select returns, any sockets that remain in the writefds are guaranteed to be writable without causing the calling thread to block. Pass nullptr for writefds to skip checking any sockets for writability. Typically a socket will block on writing only when its outgoing send buffer is too full of data.

exceptfds is a pointer to an fd_set filled with sockets to check for errors. When select returns, any sockets that remain in exceptfds have had errors occur. Pass nullptr for exceptfds to skip checking any sockets for errors.

timeout is a pointer to the maximum time to wait before timing out. If the timeout expires before any socket in the readfds becomes readable, any socket in the writefds becomes writable, or any socket in the exceptfds experiences an error, all the sets are emptied and selectreturns control to the calling thread. Pass nullptr for timeout to never time out.

select returns the number of sockets which remain in readfds, writefds, and exceptfds after its execution completes. In the case of a timeout, this value is 0.

To initialize an empty fd_set, declare one on the stack and zero it out with the FD_ZERO macro:

fd_set myReadSet;
FD_ZERO(&myReadSet);

To add a socket to a set, use the FD_SET macro:

FD_SET(mySocket, &myReadSet);

To check if a socket is in a set after select returns, use the FD_ISSET macro:

FD_ISSET(mySocket, &myReadSet);

select is not a function of a single socket, so it does not fit as a method of the type-safe socket. It belongs more correctly as a utility method in the SocketUtils class. Listing 3.10 shows a Select function to work with the type-safe TCPSocket.

Listing 3.10 Using select with a Type-Safe TCPSocket


fd_set* SocketUtil::FillSetFromVector(fd_set& outSet,
const vector<TCPSocketPtr>*
inSockets)
{
if(inSockets)
{
FD_ZERO(&outSet);
for(const TCPSocketPtr& socket : *inSockets)
{
FD_SET(socket->mSocket, &outSet);
}
return &outSet;
}
else
{
return nullptr;
}
}

void SocketUtil::FillVectorFromSet(vector<TCPSocketPtr>* outSockets,
const vector<TCPSocketPtr>*
inSockets,
const fd_set& inSet)
{
if(inSockets && outSockets)
{
outSockets->clear();
for(const TCPSocketPtr& socket : *inSockets)
{
if(FD_ISSET(socket->mSocket, &inSet))
{
outSockets->push_back(socket);
}
}
}
}

int SocketUtil::Select(const vector<TCPSocketPtr>* inReadSet,
vector<TCPSocketPtr>* outReadSet,
const vector<TCPSocketPtr>* inWriteSet,
vector<TCPSocketPtr>* outWriteSet,
const vector<TCPSocketPtr>* inExceptSet,
vector<TCPSocketPtr>* outExceptSet)
{
//build up some sets from our vectors
fd_set read, write, except;

fd_set *readPtr = FillSetFromVector(read, inReadSet);
fd_set *writePtr = FillSetFromVector(read, inWriteSet);
fd_set *exceptPtr = FillSetFromVector(read, inExceptSet);

int toRet = select(0, readPtr, writePtr, exceptPtr, nullptr);

if(toRet > 0)
{
FillVectorFromSet(outReadSet, inReadSet, read);
FillVectorFromSet(outWriteSet, inWriteSet, write);
FillVectorFromSet(outExceptSet, inExceptSet, except);
}
return toRet;
}


The helper functions FillSetFromVector and FillVectorFromSet convert between a vector of sockets and an fd_set. They allow null to be passed for the vector to support cases where the user would pass null for the fd_set. This can be mildly inefficient but is probably not an issue compared to the time required to block on sockets. For slightly better performance, wrap fd_set with a C++ data type that provides a good way of iterating through any sockets that remain after the select call returns. Keep all relevant sockets in an instance of that data type and remember to pass a duplicate of it to select so that select does not alter the original set.

Using this Select function, Listing 3.11 shows how to set up a simple TCP server loop to listen for and accept new clients while receiving data from old clients. This could run either on the main thread or on a single dedicated thread.

Listing 3.11 Running a TCP Server Loop


void DoTCPLoop()
{
TCPSocketPtr listenSocket = SocketUtil::CreateTCPSocket(INET);
SocketAddress receivingAddres(INADDR_ANY, 48000);
if( listenSocket->Bind(receivingAddres ) != NO_ERROR)
{
return;
}
vector<TCPSocketPtr> readBlockSockets;
readBlockSockets.push_back(listenSocket);

vector<TCPSocketPtr> readableSockets;

while(gIsGameRunning)
{
if(SocketUtil::Select(&readBlockSockets, &readableSockets,
nullptr, nullptr,
nullptr, nullptr))
{
//we got a packet—loop through the set ones...
for(const TCPSocketPtr& socket : readableSockets)
{
if(socket == listenSocket)
{
//it's the listen socket, accept a new connection
SocketAddress newClientAddress;
auto newSocket = listenSocket->Accept(newClientAddress);
readBlockSockets.push_back(newSocket);
ProcessNewClient(newSocket, newClientAddress);
}
else
{
//it's a regular socket—process the data...
char segment[GOOD_SEGMENT_SIZE];
int dataReceived =
socket->Receive( segment, GOOD_SEGMENT_SIZE );
if(dataReceived > 0)
{
ProcessDataFromClient(socket, segment,
dataReceived);
}
}
}
}
}
}


The routine begins by creating a listen socket and adding it into the list of sockets to check for readability. Then it loops until the application requests it do otherwise. The loop uses Select to block until a packet comes in on any socket in the readBlockSockets vector. When a packet does come in, Select ensures that readableSockets contains only sockets that have incoming data. The function then loops over each socket Select has identified as readable. If the socket is the listen socket, it means a remote host has called Connect. The function accepts the connection, adds the new socket to readBlockSockets, and notifies the application via ProcessNewClient. If the socket is not a listen socket, however, the function calls Receive to obtain a chunk of the newly arrived data and passes it to the application viaProcessDataFromClient.


Note

There are other ways to handle incoming data on multiple sockets, but they are platform specific and less commonly used. On Windows, I/O completion ports are a viable choice when supporting many thousands of concurrent connections. More on I/O completion ports can be found in the “Additional Reading” section.


Additional Socket Options

Various configuration options control the sending and receiving behavior of the sockets. To set these values for these options, call setsockopt:

int setsockopt(SOCKET sock, int level, int optname, const char *optval, int
optlen);

sock is the socket to configure.

level and optname describe the option to be set. level is an integer identifying the level at which the option is defined and optname defines the option.

optval is a pointer to the value to set for the option.

optlen is the length of the data. For instance, if the particular option takes an integer, optlen should be 4.

setsockopt returns 0 if successful or −1 if an error occurs.

Table 3.4 lists some useful options available at the SOL_SOCKET level.

Image

Image

Table 3.4 SOL_SOCKET Options

Table 3.5 describes the TCP_NODELAY option available at the IPPROTO_TCP level. This option is only settable on TCP sockets.

Image

Table 3.5 IPPROTO_TCP Options

Summary

The Berkeley Socket is the most commonly used construct for transmitting data over the Internet. While the library interface differs across platforms, the core fundamentals are the same.

The core address data type is the sockaddr, and it can represent addresses for a variety of network layer protocols. Use it any time it is necessary to specify a destination or source address.

UDP sockets are connectionless and stateless. Create them with a call to socket and send datagrams on them with sendto. To receive UDP packets on a UDP socket, you must first use bind to reserve a port from the operating system, and then recvfrom to retrieve incoming data.

TCP sockets are stateful and must connect before they can transmit data. To initiate a connection, call connect. To listen for incoming connections, call listen. When a connection comes in on a listening socket, call accept to create a new socket as the local endpoint of the connection. Send data on connected sockets using send and receive it using recv.

Socket operations can block the calling thread, creating problems for real-time applications. To prevent this, either make potentially blocking calls on non–real-time threads, set sockets to non-blocking mode, or use the select function.

Configure socket options using setsockopt to customize socket behavior. Once created and configured, sockets provide the communication pathway that makes networked gaming possible. Chapter 4, “Object Serialization” will begin to deal with the challenge of making the best use of that pathway.

Review Questions

1. What are some differences between POSIX-compatible socket libraries and the Windows implementation?

2. To what two TCP/IP layers does the socket enable access?

3. Explain how and why a TCP server creates a unique socket for each connecting client.

4. Explain how to bind a socket to a port and what it signifies.

5. Update SocketAddress and SocketAddressFactory to support IPv6 addresses.

6. Update SocketUtils to support creation of a TCP socket.

7. Implement a chat server that uses TCP to allow a single host to connect and relays messages back and forth.

8. Add support for multiple clients to the chat server. Use non-blocking sockets on the client and select on the server.

9. Explain how to adjust the maximum size of the TCP receive window.

Additional Readings

Information Sciences Institute. (1981, September). Transmission Control Protocol. Retrieved from http://www.ietf.org/rfc/rfc793. Accessed September 12, 2015.

I/O Completion Ports. Retrieved from https://msdn.microsoft.com/en-us/library/windows/desktop/aa365198(v=vs.85).aspx. Accessed September 12, 2015.

Porting Socket Applications to WinSock. Retrieved from http://msdn.microsoft.com/en-us/library/ms740096.aspx. Accessed September 12, 2015.

Stevens, W. Richard, Bill Fennerl, and Andrew Rudoff. (2003, November 24) Unix Network Programming Volume 1: The Sockets Networking API, 3rd ed. Addison-Wesley.

WinSock2 Reference. Retrieved from http://msdn.microsoft.com/en-us/library/windows/desktop/ms740673%28v=vs.85%29.aspx. Accessed September 12, 2015.