IC221: Systems Programming (SP16)


Home Policy Calendar Resources

Lec 27: Server Sockets

Table of Contents

1 Client Socket Programming (Review)

In this lesson, we are going to look at the other side of the socket protocol, the server side. To bootstrap that discussion, lets first review the relevant system calls associated with a client socket. We can visual these functions like below:

lec23-client-socket.png

Figure 1: Client Socket Life Cycle

To open a new socket, use the socket() system call, but the socket being open doesn't mean it is connected to anything. To do that, you use the connect() system call that takes as input a given address, IP-port pair. Once connected with the remote server, the client can read and write to that socket, participating in the application layer protocol.

At the other end, at the server, sockets are also used. The interface for using sockets is similar but there are a few additional steps involved.

2 Server Sockets

Server sockets are much like client sockets except instead of connecting they accept incoming connection. The result of accepting an incoming connection generates a new socekt for that client which is used for all further communication. The server socket remains and can accept more incoming connections on the listening port.

Before a connection can be accepted, there is some setup, which we can trace through the server socket life cycle below:

lec23-server-socket.png

Figure 2: Server Socket Life Cycle

2.1 Binding a Socket: bind()

The first step in establishing a server socket is to bind the socket to a given IP address and port. This way the O.S. knows what port the socket is listening on. The bind() system call has the following description:

int bind(int socket, const struct sockaddr *address, socklen_t address_len);

The arguments can be interpreted as follows:

  • socket : an open socket from socket()
  • address : a reference to a socket address to be bound, in our case, this will reference a sockaddr_in with an IP address and port in the AF_INET family.
  • address_len : the size of the socket address, which will be sizeof(struct sockaddr_in)

It returns 0 on success and a negative value on error. We can see how this functions in code with the following example:

char hostname[]="127.0.0.1";   //localhost ip address to bind to
short port=1845;               //the port we are to bind to
int server_sock;               //file descriptor for the server socket

struct sockaddr_in saddr_in;  //socket interent address of server
//set up the address information
saddr_in.sin_family = AF_INET;
inet_aton(hostname, &saddr_in.sin_addr);
saddr_in.sin_port = htons(port);

//open a socket
if( (server_sock = socket(AF_INET, SOCK_STREAM, 0))  < 0){
  perror("socket");
  exit(1);
}

//bind the socket
if(bind(server_sock, (struct sockaddr *) &saddr_in, saddr_len) < 0){
  perror("bind");
  exit(1);
}

Note that we are binding to an ip address 127.0.0.1 which is special IP address referring the local machine. You could also use the domain name localhost and perform a look up with getaddrinfo().

2.2 Queuing incoming connections: listen()

Once you've bound the socket to an IP address and port, you must still indicated to the Operating System that this socket is a server socket. The system call that does this is called listen() and has the following function description:

int listen(int socket, int backlog);

The argument socket is the server socket file descriptor, but the argument backlog requires a bit more explanation. As you will see below, to establish a connection with a client, accept() must be called, but this doesn't happen immediately. There is a period of limbo between when the incoming connection is recognized and accept() is called to establish the connection. Further, many incoming connections can occur at the same time, and the operating system has limited resources to queue up client connections prior to accept(). The backlog argument indicates to the OS how many incoming connections should be allowed to queue prior to accept() before starting to reject connections. A typical value for backlog is 5, but higher and lower values is acceptable.

Here is an example of the listen() in the running example:

//ready to listen, queue up to 5 pending connections
if(listen(server_sock, 5)  < 0){
  perror("listen");
  exit(1);
}

2.3 Accepting Incoming Connections: accept()

Finally, everything is in place to accept a connection, and to do that we use the accept() system call. It has the following function description:

int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);

The arguments to accept are as follows:

  • socket is the server socket that you have bound and established as a listener.
  • adress is a reference to socket address structure of the client. Since we are using AF_INET sockets, you can pass a reference to a struct sockaddr_in and cast appropriately.
  • address_len is a pointer to a size reference for the address. This is necessary because, as we learned already, not all socket addresses are the same size, but since we are using AF_INET we know that this will sizeof(struct sockaddr_in)

When a client connection has been accepted properly, the return value of accept is a file descriptor for a new socket that we can use to communicate with the client. The server socket remains because we might want to use that to accept other incoming connections.

Here is an example in code:

int client_sock;

//accept incoming connections
if((client_sock = accept(server_sock, (struct sockaddr *) &client_saddr_in, &saddr_len)) < 0){
  perror("accept");
  exit(1);
}

printf("Connection From: %s:%d (%d)\n", 
       inet_ntoa(client_saddr_in.sin_addr), //address as dotted quad
       ntohs(client_saddr_in.sin_port),     //the port in host order
       client_sock);                        //the file descriptor number

You'll note that the address of the new socket is different than the address of the server socket. This make sense. You can't have two sockets communicating on the same port, so when a client connects, the OS must establish a new connection on a different port so not to collide with the server port. Further, if you think about it more, once the connection is established, the port doesn't really matter as long as both ends agree on it. The server port address, the address that accepts connections, is the only port that must truly be known and declared. We will see more examples of this later.

Another important thing to note about the server socket is that accepting an incoming connections is a blocking operation. That means, accept() will not return until a new connection is provided. This fact becomes quite a challenge when developing many server programs that wish to provide service to multiple clients, and we will explore different techniques for achieving multi-client services.

2.4 Communicating with the Client: read()/write()/close()

Once we have the connection established with the client, the new client socket, from accept(), is how we communicate. Recall that a socket is just a file descriptor, so we can use the standard read() and write() operations on the socket to send and receive data from the child. At this point, these procedures should be familiar to you:

 //read from client
if((n = read(client_sock,response, BUF_SIZE-1)) < 0){
  perror("read");
  exit(1);
}
response[n] = '\0'; //NULL terminate string

printf("Read from client: %s", response);

//construct response
snprintf(response, BUF_SIZE, "Hello %s:%d \nGo Navy! Beat Army\n", 
         inet_ntoa(client_saddr_in.sin_addr),    //address as dotted quad
         ntohs(client_saddr_in.sin_port));       //the port in host order

printf("Sending: %s",response);

//send response
if(write(client_sock, response, strlen(response)) < 0){
  perror("write");
  exit(1);
}

printf("Closing socket\n\n");

//close client socket
close(client_sock);

//close the server socket
close(server_sock);

Once all the operations are over, the act of closing the socket will bring down the connection with client. Closing the server socket stops the listening process.

2.5 Putting it all together

With all the pieces in place, we can connect the client and server socket procedures and see how the two interfaces interact:

lec23-client-server-socket.png

Figure 3: Client and Server Socket Life Cycle

And the entirity of the hello server program:

/*hello_server.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define BUF_SIZE 4096

int main(){

  char hostname[]="127.0.0.1";   //localhost ip address to bind to
  short port=1845;               //the port we are to bind to


  struct sockaddr_in saddr_in;  //socket interent address of server
  struct sockaddr_in client_saddr_in;  //socket interent address of client

  socklen_t saddr_len = sizeof(struct sockaddr_in); //length of address

  int server_sock, client_sock;         //socket file descriptor


  char response[BUF_SIZE];           //what to send to the client
  int n;                             //length measure

  //set up the address information
  saddr_in.sin_family = AF_INET;
  inet_aton(hostname, &saddr_in.sin_addr);
  saddr_in.sin_port = htons(port);

  //open a socket
  if( (server_sock = socket(AF_INET, SOCK_STREAM, 0))  < 0){
    perror("socket");
    exit(1);
  }

  //bind the socket
  if(bind(server_sock, (struct sockaddr *) &saddr_in, saddr_len) < 0){
    perror("bind");
    exit(1);
  }

  //ready to listen, queue up to 5 pending connectinos
  if(listen(server_sock, 5)  < 0){
    perror("listen");
    exit(1);
  }


  saddr_len = sizeof(struct sockaddr_in); //length of address

  printf("Listening On: %s:%d\n", inet_ntoa(saddr_in.sin_addr), ntohs(saddr_in.sin_port));

  //accept incoming connections
  if((client_sock = accept(server_sock, (struct sockaddr *) &client_saddr_in, &saddr_len)) < 0){
    perror("accept");
    exit(1);
  }


  printf("Connection From: %s:%d (%d)\n", 
         inet_ntoa(client_saddr_in.sin_addr), //address as dotted quad
         ntohs(client_saddr_in.sin_port),     //the port in host order
         client_sock);                        //the file descriptor number

  //read from client
  if((n = read(client_sock,response, BUF_SIZE-1)) < 0){
    perror("read");
    exit(1);
  }
  response[n] = '\0'; //NULL terminate string

  printf("Read from client: %s", response);

  //construct response
  snprintf(response, BUF_SIZE, "Hello %s:%d \nGo Navy! Beat Army\n", 
           inet_ntoa(client_saddr_in.sin_addr),    //address as dotted quad
           ntohs(client_saddr_in.sin_port));       //the port in host order

  printf("Sending: %s",response);

  //send response
  if(write(client_sock, response, strlen(response)) < 0){
    perror("write");
    exit(1);
  }

  printf("Closing socket\n\n");

  //close client_sock
  close(client_sock);

  //close the socket
  close(server_sock);

  return 0; //success
}

And we can see a few runs of the program. In one terminal we have our small server program running and in the other we have a netcat client running.

#> ./hello_server
Listening On: 127.0.0.1:1845
Connection From: 127.0.0.1:58740 (4)
Read from client: hello
Sending: Hello 127.0.0.1:58740 
Go Navy! Beat Army
Closing socket
#> netcat localhost 1845
hello
Hello 127.0.0.1:58740 
Go Navy! Beat Army

Note that the client socket, after the connection was accepted, is operating on the port 58740, which is not the same as the port the server socket was listening on 1845.

3 Handling Multiple Incoming Connections of Server Sockets

Now that we understand how to setup a server, let's consider how we might be able to handle multiple incoming connections. This is a common task for servers since every client has its own client socket, it is only natural to serve multiple clients at the same time. However, this is more complicated then it might seem at first because of blocking operations. Let's first explore the perils of blocking operations and how to overcome this challenge. In a later lessons, we'll see another method for handling multiple client services that uses threads that is simpler but comes with its own challenge.

3.1 Challenge of blocking

Let's consider an improvement to our server: instead of just responding with a token phrase, it echos back whatever is sent to it until the client closes the connection. Further, we'd like to be able to serve multiple clients. To start, we could simply just change the program logic to look like this:

//accept incoming connections in a loop
 while((client_sock = accept(server_sock, (struct sockaddr *) &client_saddr_in, &saddr_len)) > 0){

   printf("Connection From: %s:%d (%d)\n", 
          inet_ntoa(client_saddr_in.sin_addr), 
          ntohs(client_saddr_in.sin_port), 
          client_sock);


   //echo loop, break when read 0 or error
   while((n = read(client_sock, response, BUF_SIZE-1)) > 0){
     response[n] = '\0' ; //NULL terminate

     printf("Received From: %s:%d (%d): %s\n",  //LOGGING
            inet_ntoa(client_saddr_in.sin_addr), 
            ntohs(client_saddr_in.sin_port), 
            client_sock, 
            response);


     if(write(client_sock, response, n) < 0){
       perror("write");
       break;
     }
   }

   if( n < 0){
     perror("read");
   }

   printf("Client Closed: %s:%d (%d)\n",    //LOGGING
          inet_ntoa(client_saddr_in.sin_addr), 
          ntohs(client_saddr_in.sin_port), 
          client_sock);

   //close client socket
   close(client_sock);


   //reset socket len just in case
   saddr_len = sizeof(struct sockaddr_in); //length of address

 }

Essentially, we've place the server accepting incoming connections in a loop. When there is a connection, anything that is written from the client is echoed back. If the client closes the connection, so does the server. We can see that happening with a simple example:

#> ./echo_server
>./echo_server
serer sock listening: (3)
Connection From: 127.0.0.1:59088 (4)
Received From: 127.0.0.1:59088 (4): testing client #1

Received From: 127.0.0.1:59088 (4): who are you?
#>netcat localhost 1845
testing client #1
testing client #1
who are you?
who are you?

However, if the first client keeps the connection open when the next client connects, what happens?

#>nc localhost 1845
testing
why am I not getting an echo?

There is no echo response. That's because the server is blocking while attempting to read() from the first client and the connection has not been accepted(). The connection is queued, so when the first client closes the socket, the expected response is provided, but we'd like all of this to occur simultaneously. In this way, a server can provide services to multiple clients.

3.2 Identifying Readable File Descriptors with select()

There are a few ways to solve this problem. One is to set all the socket file descriptors to non-blocking, which is a possibility and we've seen how to do this with fnctl() and pipes. But it can overly complicate the code.

Instead, we need a way to check to see if a file descriptors is ready to be used (e.g., read, write, or accepted) so that the operation will not block and return immediately. The simplest method for testing the readiness of a file descriptor is to use select().

select() is a system call that given a set of file descriptors, will allow you to iterate over the file descriptors that are ready for reading (or writing). The protocol for select has a few functions, but it's easy to see their use with an example.

/*echo_server.c*/
fd_set select_set; //stores interested file descriptors

FD_ZERO(select_set); //clear the set
FD_SET(fd, select_set); //add fd to the set
//add other file descriptors

//select at most FD_SETSIZE file descriptor from set that are ready for an action
select(FD_SETSIZE, &select_set, NULL, NULL, NULL) < 0)

//check for activity on all file descriptors                                                                                                                                                                    
 for(i=0; i < FD_SETSIZE; i++){

   //was the file descriptor i set?
   if(FD_ISSET(i, &select_set)){

     //i is the file descriptor number
     read( i, buf, BUF_SIZE); 
     //etc.

     FD_CLR(i,select_set); //remove file descriptor i from the set
   }
}

First, a set of file descriptors must be declared, this is of type fd_set. After initialization, FD_ZERO(), interested file descriptors can be added to the set using FD_SET(). Once all the file descriptors are provided, the select() system call will check all the file descriptors in the set to see if they are reading for an action, like a read(). Finally, you can iterate through all the file descriptor numbers checking if the file descriptor was selected with FD_ISSET() and if so, do some action. A file descriptor can be removed from the set with FD_CLR().

The specifics of how this works is not important here. The key takeaway is that we can now check if a file descriptor needs an action before taking said action: we are avoiding blocking! Let's see how we would use select() with our echo server:

/* echo_server_select.c*/
  fd_set activefds, readfds;


  //server setup and etc.


  while(1){ //loop


    //update the set of selectable file descriptors
    readfds = activefds;

    //Perform a select
    if( select(FD_SETSIZE, &readfds, NULL, NULL, NULL) < 0){
      perror("select");
      exit(1);
    }

    //check for activity on all file descriptors
    for(i=0; i < FD_SETSIZE; i++){

      //was the file descriptor i set?
      if(FD_ISSET(i, &readfds)){

        if(i == server_sock){ //activity on server socket, incoming connection

          //accept incoming connections = NON BLOCKING
          client_sock = accept(server_sock, (struct sockaddr *) &client_saddr_in, &saddr_len);

          printf("Connection From: %s:%d (%d)\n", inet_ntoa(client_saddr_in.sin_addr), 
                 ntohs(client_saddr_in.sin_port), client_sock);

          //add socket file descriptor to set
          FD_SET(client_sock, &activefds);

        }else{

          //otherwise client socket sent something to us
          client_sock = i;

          //get the address of the socket
          getpeername(client_sock, (struct sockaddr *) &client_saddr_in, &saddr_len);

          //read from client and echo back
          n = read(client_sock, response, BUF_SIZE-1);   

          if(n <= 0){ //closed or error on socket

            //close client sockt
            close(client_sock);

            //remove file descriptor from set
            FD_CLR(client_sock, &activefds);

            printf("Client Closed: %s:%d (%d)\n",           //LOG
                   inet_ntoa(client_saddr_in.sin_addr), 
                   ntohs(client_saddr_in.sin_port), 
                   client_sock);

          }else{ //client sent a message

            response[n] = '\0'; //NULL terminate

            //echo messget to client
            write(client_sock, response, n);

            printf("Received From: %s:%d (%d): %s",         //LOG
                   inet_ntoa(client_saddr_in.sin_addr), 
                   ntohs(client_saddr_in.sin_port), 
                   client_sock, response);
          }

        }

      }

    }

  }

That's a lot to take in, but here are few key points. First off, the accept() call is also a blocking operation, so we can use select() to determine if we have an incoming connection. We still need to check for clients closing on every read, which increases the complexity of the code. In the end, though, it works.

But, it's not clean. It lacks a certain something. What's not quite right about this code block is that we like to think of the process of handling a client a lot more like the while loop from echo_server and not the while loop from echo_server_select. What we want is a way to parallelize the process so we can write a simple bit of code that can handle all client connections the same and let that code run in parallel from the accepting connection. That's exactly what we'll look at next when we investigated threading and how threading can be used for socket server programming.