A Simple TCP Server
To begin with, servers must be able to exchange information with many peers at once. This can be achieved either by sequentially scanning the open sockets (in a nonblocking manner or we might get in trouble) or by writing a concurrent server that runs several processes in parallel, each with its own socket and peer.
To start our discussion of servers, we will focus on a single-peer server. This should suffice for two-player games and will serve as a foundation from which we will move on to more involved solutions.
In a two-player scenario, we don't need to worry about several sockets at once because we are in a one-to-one situation. But this is not a symmetrical relationship. The client and the server play different roles and thus need different system calls.
As a first step, the server must create its own socket and put it in a "listening" mode. Under this mode, the socket is left open on a specific port, so it awaits incoming connection requests from the client. As new connections arrive, a brief setup sequence takes place and data transfer can begin. So, let's focus on the initialization of our TCP server. As usual, it all begins with the creation of a socket. This process is identical to the client side and is performed by using the line:
int sock= socket(AF_INET, SOCK_STREAM,IPPROTO_TCP);
Now, we must create a relationship between this socket and an IP address/port pair. This way we state that the socket will be looking for connection requests arriving through that entry point. You might ask why we should specify an IP address to listen to if we know which server we are at. The answer lies in the nature of Internet servers. An Internet server might be known by different IP addresses (aliases to the base IP). So, we need to specify whether we want the socket to respond to only one of those addresses. However, most servers will want to respond to requests arriving at any of the IP addresses the server is identified by, so a special command will allow us to specify all of the addresses at once. Overall, this binding between socket and IP/port pair is performed by the bind call, whose prototype is as follows:
int bind(int socket, sockaddr *s,int saddrlen);
We will use the sockaddr (sockaddr_in in the case of Internet servers) to specify the IP/port pair. Here is the exact initialization code for such a call:
struct sockaddr_in adr; adr.sin_family=AF_INET; adr.sin_port = htons(port); adr.sin_addr.s_addr=INADDR_ANY; ZeroMemory(adr.sin_zero,8); bind(socket, (struct sockaddr *) &adr, sizeof(adr));
Notice the use of the INADDR_ANY wild card to specify that we want to respond using any of the aliases the server is known by. Alternatively, we could manually specify an IP address for the socket to respond to exclusively. That IP address could easily be taken from the gethostbyname() return value. Upon completion, bind() will return an integer value, which will be zero if the call was successful and –1 otherwise. Popular errors include the socket being undefined or already bound, or the IP/port pair being already in use by another service.
By now your server is up and running, and the OS knows which address and port it is bound to. Thus, it is now time to enter passive mode or, in other words, put the server to sleep while waiting for connections. This is performed by using the call listen(), which makes the server wait for an incoming connection:
int listen(int socket, int queuelen);
As usual, the first parameter is the socket to work with. The second parameter allows programmers to specify the length of the connection queue. The TCP/IP stack will queue up to queuelen connections while the server is handling another request. As you will soon see, a server can be busy taking care of one entrant connection. To prevent other connections from being lost while this process takes place, you can save them in a queue for later use. Traditionally, queuelen has a maximum size of five, allowing six connections to be "on hold" while the server handles one peer.
There is only one call ahead of us in the process of starting our server. This call effectively puts the server on hold until a new connection takes place. The call is
int accept(int socket, sockaddr *addr, int *addrlen);
Accept remains blocked until a new connection takes place. Its parameters are the socket we are listening from and a sockaddr/length pair that will hold the data from the client, mainly its IP and DNS addresses. As a result of the accept() call, we will receive either –1 if there was an error, or a nonnegative socket descriptor. This new socket is activated and ready to transfer data with the client, whereas the initial socket remains untouched so we can keep accepting connections.
After accept, we are ready to begin working with our client in a recv-send manner. As a summary of the client-server relationship, see Figure 10.1. Notice that the server must be in accept() state to be able to receive connections. Obviously, this means that the server-side boot process must already have taken place.