Menu

Class 21: Networks IV, a TCP simple server


Reading
16.3.4 and 16.4 of APUE.

Homework
Printout the Homework and answer the questions on that paper.

Why servers are different
Last class we created a simple TCP client, which jsut connected to an existing "capitalization server", running on port 10000. Recall that the basic steps for the client were:
  1. call socket to create a IPv4, TCP socket, and
  2. call connect to bind the socket to a random port, and make a TCP connection from that port to a host+port where we knew there'd be a server running.

Now we're going to consider the server, and see how it's implemented. What it needs to do is a bit different because, as we keep hammering, the roles of client and server are asymmetric: the server must be bound to a previously-decided-upon port, the client's port number is irrelevent; the server must sit and wait, listening for a connection request, whereas the client takes the active role and makes the connection request. Thus, the basic steps for the server are quite different:

  1. call socket to create a IPv4, TCP socket,
  2. call bind to bind the socket to a specific port number,
  3. call listen to listen for connection requests for that port, and
  4. call accept to accept a connection request and begin communicating.
That's the sequence we'll be following.

Creating the socket
Creating a socket for the server is no different than for the client:
  /* Set up socket */
  int sfd = socket(AF_INET,SOCK_STREAM,0);
  if (sfd == -1) { fprintf(stderr,"Socket not created!\n"); exit(1); }

Binding the socket to a specific host and port number
The next task is to bind the socket sfd to a specific host+port. For our first iteration, we'll hard-code the IP address of our server. Later, it'll make much more sense (though require a bit more work) to have the program automatically use get the IP address of the current host. Hard-coding the port number is OK, but it'd be better if that could at least be changed from the command-line.

The bind system call is what we use to accomplish this step, and its syntax should look hauntingly familiar:

int bind(int s, const struct sockaddr *name, int namelen);
You see that specifying a host+port has to be done the same mickey mouse way as we did for calls to connect.

  /* Hardcoded IP address and port number. */
  unsigned int  ipnum = 2205833693U; // The "U" makes this an usigned int literal
  unsigned short pnum = 10000;

  /* Set up address structure */
  struct sockaddr_in mysa;
  mysa.sin_family = AF_INET;
  mysa.sin_addr.s_addr = htonl(ipnum);
  mysa.sin_port = htons(pnum);

  /* Bind socket to address */
  if (bind(sfd,(struct sockaddr*)&mysa,sizeof(mysa)) < 0)
  {
    close(sfd);
    fprintf(stderr,"bind failed!\n");
    exit(3);
  }
Of course the call to bind may fail, and if it does, -1 is returned. Otherwise 0 is returned. Now, why might bind fail? Well already at this point we have enough to see how/why. Let's run the program in its current state (see ex0.c) and use netstat -a to see how it's doing:
banshee$ gcc -o ex0 ex0.c -lnsl -lsocket
banshee$ ./ex0 &
[1] 5430
banshee$ netstat -a
TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
-------------------- -------------------- ----- ------ ----- ------ -----------
mich302csd21d.32769  nwtime.usna.edu.ldap 130572      0 49640      0 ESTABLISHED
localhost.32776            *.*                0      0 49152      0 LISTEN
      *.fs                 *.*                0      0 49152      0 LISTEN
      *.dtspc              *.*                0      0 49152      0 LISTEN
      *.time               *.*                0      0 49152      0 LISTEN
      *.ftp                *.*                0      0 49152      0 LISTEN
      *.ssh                *.*                0      0 49152      0 LISTEN
mich302csd21d.1023   chessie.cs.usna.edu.nfsd 49640      0 49640     68 ESTABLISHED
mich302csd21d.ssh    131.122.90.34.38390  64128      0 49232      0 ESTABLISHED
mich302csd21d.10000        *.*                0      0 49152      0 BOUND
We see from netstat that the socket has been bound to port 10000, just like we wanted. Now, what happens if, while the first ex0 process is still running (remember with the & it's still going in the background):
banshee$ ./ex0 &
[2] 5433
 bind failed!
[2]    Exit 3                        ./ex0
The call to bind failed! Why? Because only one socket may be bound to a given port, using a given protocol, on a given host at one time. That way a socket, once bound, has a truly unique address: it's protocol + address + port, where "protocol" is something like "IPv6 TCP". Anyway, you can see that it's worth checking to see if bind succeeds! (Better kill that ex0 process right now.)

Listening for a connection request
The next step is to tell the kernel to set your socket, now bound to a port, to listen for connection requests from clients. This is done with another conveniently named system call: listen (man -s 3socket listen).
int listen(int s, int backlog);
The first parameter is the descriptor referencing the socket, the second is an int that tells the kernel how many connection requests to queue up before it just starts turning requests away. You can imagine, if your server supplies a popular server, you may get lots of connection requests, and get them far faster than you can handle. The kernel will queue them up for you ... this parameter sets a limit on how long that queue will get. As with bind, a return value of 0 indicates success, while failure is indicated by -1.
  /* Listen for connection request */
  if (listen(sfd,20) < 0)
  {
    close(sfd);
    fprintf(stderr,"listen failed!\n");
    exit(4);    
  }

If we compile and run our implementation so far (ex1.c), then run netstat -a, we'll see that the state of your port has changed:
banshee$ gcc -o ex0 ex0.c -lnsl -lsocket
banshee$ ./ex0 &
[1] 5430
banshee$ netstat -a
TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
-------------------- -------------------- ----- ------ ----- ------ -----------
mich302csd21d.32769  nwtime.usna.edu.ldap 130572      0 49640      0 ESTABLISHED
localhost.32776            *.*                0      0 49152      0 LISTEN
      *.fs                 *.*                0      0 49152      0 LISTEN
      *.dtspc              *.*                0      0 49152      0 LISTEN
      *.time               *.*                0      0 49152      0 LISTEN
      *.ftp                *.*                0      0 49152      0 LISTEN
      *.ssh                *.*                0      0 49152      0 LISTEN
mich302csd21d.1023   chessie.cs.usna.edu.nfsd 49640      0 49640     68 ESTABLISHED
mich302csd21d.ssh    131.122.90.34.38390  64128      0 49232      0 ESTABLISHED
mich302csd21d.10000        *.*                0      0 49152      0 LISTEN
See? The state changed from BOUND to LISTEN ... we're making progress.

Accepting a connection
Once the socket is in the LISTEN state to listen for connection requests, you call yet another wonderfully named function to accept a connection request: accept (man -s 3socket accept):
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
The int s is the descriptor for the socket we're listening on. The other two arguments are there to let the kernel provide us with the address (i.e. host+port) from which the request is coming. If we don't care about that, and for the moment we don't, we can simply set them both to NULL. What accept returns for us is a file descriptor for this particular connection we've just accepted. You see, more requests may be coming in from other clients, and they'll come on the original descriptor sfd. This new descriptor provides a private channel for communicating just with the remote socket we've accepted a connection from ... as if all the other stuff going on with sfd wasn't even there.
  /* Accept the connection request */
  int sessionfd = accept(sfd,NULL,NULL);
  if (sessionfd == -1)
  { 
    close(sfd);
    fprintf(stderr,"accept failed!\n");
    exit(5);    
  }   
Now if we run our server (ex2.c), and launch our client to connect to it, netstat shows us yet another state:
banshee$ gcc -o ex2 ex2.c -lnsl -lsocket
banshee$ ./ex2 &
		
fireball$ gcc -o client client.c -lnsl -lsocket
fireball$ ./client banshee.cs.usna.edu

		
banshee$ netstat -a
TCP: IPv4
   Local Address        Remote Address    Swind Send-Q Rwind Recv-Q    State
-------------------- -------------------- ----- ------ ----- ------ -----------
mich302csd21d.32769  nwtime.usna.edu.ldap 130572      0 49640      0 ESTABLISHED
localhost.32776            *.*                0      0 49152      0 LISTEN
      *.fs                 *.*                0      0 49152      0 LISTEN
      *.dtspc              *.*                0      0 49152      0 LISTEN
      *.time               *.*                0      0 49152      0 LISTEN
      *.ftp                *.*                0      0 49152      0 LISTEN
      *.ssh                *.*                0      0 49152      0 LISTEN
mich302csd21d.1023   chessie.cs.usna.edu.nfsd 49640      0 49640     68 ESTABLISHED
mich302csd21d.ssh    131.122.90.34.38390  64128      0 49232      0 ESTABLISHED
mich302csd21d.10000  fireball.cs.usna.edu.41617 0      0 49152      0 ESTABLISHED

So, we see that a connection has been established, and netstat shows us the host+port that defines the address of the socket that has connected to us. At this point, we can do ... whatever it is we really wanted the server to do. My example is a "Capitalization server". It reads characters and echos them back, changing any lowercase to capitals. Check out ex3.c to see that in action.

Serving multiple clients
The server we've created thus far can only serve a single client, which is a serious limitation. Moving to a server that can simultaneously server many clients is actually trivial. The reason is this: once we have the file descriptor sessionfd, we can serve the client without thinking at all about networks or sockets. We just use sessionfd like any other descriptor, except that this one is bidirectional, i.e. we can both read and write, and the two operations don't conflict with one another. So, once we get that descriptor, we can fork a process to serve that connection, or spawn a thread to serve that connection. Which to do depends on the application. In this case, we have no data to communicate between the thread/process serving that client and any other thread/process, so that makes processes a better choice. So our server is essentially a big loop that accepts a connection, forks a process to serve that connection, and goes back to the top.
  /* Accept the connection request */
  while(1)
  {
    int sessionfd = accept(sfd,NULL,NULL);
    if (sessionfd == -1)
    { 
      close(sfd);
      fprintf(stderr,"accept failed!\n");
      exit(5);    
    }   

    /* fork a child to serve this connection. */
    if (fork() == 0)
    {
      char inc, outc;
      while(read(sessionfd,&inc,1) == 1)
      {
	if ('a' <= inc && inc <= 'z')
	  outc = inc - 'a' + 'A';
	else
	  outc = inc;
	write(sessionfd,&outc,1);
      }      
      close(sessionfd);
      exit(1);
    }
    else
    {
      close(sessionfd);
    }
  }
  fprintf(stderr,"Server shutting down!\n");
  close(sfd);
You can check out the complete server code to see the process in all of its glory, including grabbing the current host's name from the HOST environment variable, and using gethostbyname to find the IP address from the hostname.