macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Networking and the BSD Sockets API
Pages: 1, 2, 3

Servers

Now we discuss the startup procedure for a server, which is slightly more complicated than what a client must do. The first step is the same -- create a socket:



struct sockaddr_in serverAddress;
int listenfd, connectfd;

if ( (listenfd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) {
   perror( "socket" );
   exit(1);
}

The only thing that changed here is that we declared a second socket file descriptor variable to hold the return value of accept(), and we changed the name of the original socket file descriptor variable from sockfd to listenfd, to reflect the changed nature of the socket in a server application.

Next, we have to bind the socket to a port and address using the bind() function. This function takes a sockaddr_in struct, so we have to prepare one as we did for the client:

bzero( &serverAddress, sizeof(serverAddress) );
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons( 12345 );
serverAddress.sin_addr.s_addr = htonl( INADDR_ANY );

Initializing the server's socket address structure is done in the same way as the client's, with a few small changes. We first zero the memory space occupied by serverAddress, and then set the family, port number, and address. Notice, however, that we set the address differently than before. We could have used inet_pton() with the same localhost IP address that we used for the client, but to do so would restrict the server to accepting connections only on the localhost interface. In other words, our server would not be able to accept connections from the network, since it would only be listening for connections on the IP address 127.0.0.1.

There are functions that let us obtain the IP address of the Ethernet interface, but there is a better solution that will allow the socket to accept connections on any of the available interfaces (Ethernet, localhost, Airport, Firewire, etc.). By setting the address to the constant INADDR_ANY, the kernel will bind the socket to ress.sin_addr.s_addr to the network representation (obtained using htonl()) of the constant INADDR_ANY all available network interfaces. Thus, if we simultaneously have an active Ethernet connection, an active Airport connection, and the loopback interface (127.0.0.1), we will be able to connect to the socket over any of these interfaces' respective IP addresses.

Next we have to bind the socket to the address specified in the struct serverAddress. This is done using the bind() function:

if ( bind( listenfd, (struct sockaddr *)&serverAddress, 
            sizeof(serverAddress)) < 0 ) {
   perror( "bind" );
   exit(1);
}

Like the connect() function, we pass the socket file descriptor, the socket address structure that specifies the address and port to bind to, and finally, the length of the address structure. As always, we check to see if the function executed successfully by comparing the return value to zero.

Next we call the listen() function to tell the socket to listen for incoming connections. By calling listen(), we are converting our socket into a passive socket that can accept connections. Calling listen() is pretty straightforward:

if ( listen( listenfd, 5 ) < 0 ) {
   perror( "listen" );
   exit(1);
}

listen() takes two arguments: the socket file descriptor and the backlog. The backlog argument is used to limit the size of the queue for incoming connections. Thus, by passing 5 we tell the kernel to queue up to 5 pending connections. If a client attempts to connect when the queue is full, the kernel will refuse the connection, and the client's call to connect() will return with a connection-refused error. Note that the backlog does not specify the total number of connects the server can handle, because once a connection has been accepted by the server, the request is removed from the queue, thus making room for additional connection requests.

The next thing we do is call the accept() function within a rudimentary run loop. What accept() does is connect to the host whose connection request is at the front of the queue, and return a socket file descriptor for this connection. We can then read and write data to the client using this new socket. Let's take a look at how our simple server will send a message to clients:

for (;;) {
   char *buffer = "Howdy!\n";

   if ( (connectfd = accept( listenfd, 
          (struct sockaddr *)NULL, NULL )) < 0 ) {
      perror( "accept" );
      exit(1);
   }

   if ( write( connectfd, buffer, strlen(buffer)) < 0 ) {
      perror( "write" );
      exit(1);
   }    
   close( connectfd );
}

The rudimentary run loop I mentioned above is done with the infinite for loop; the server will continue waiting for connections until the user kills the process (using Ctrl-C, for example). Our server is pretty inflexible, since the message it sends to clients that connect is hard-coded, and short: "Howdy!" The message is coded as a null-terminated string. In the call to accept(), we pass the file descriptor for the listening socket (and nothing for the address structure, since we don't need any of that information that accept() returns in this structure), and in return, accept() gives us the file descriptor for the connected socket. This socket, connectfd, is our end of the connection between the server host and the client host. This is the socket that we read data from and write data to when we communicate with the client.

Finally, after we send the message, we close the connected socket using the close() function and return to the top of the loop, ready to accept a new connection.

Now, putting all of these pieces together (with error checking in place), we have the following code for a small server application:

#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main( int argc, char **argv )
{
   struct sockaddr_in serverAddress;
   int listenfd, connectfd;

   if ( (listenfd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) {
      perror( "socket" );
      exit(1);
   }

   bzero( &serverAddress, sizeof(serverAddress) );
   serverAddress.sin_family = AF_INET;
   serverAddress.sin_port = htons(12345);
   serverAddress.sin_addr.s_addr = htonl( INADDR_ANY );

   if ( bind( listenfd, (struct sockaddr *)&serverAddress, 
               sizeof(serverAddress) ) < 0 ) {
      perror( "bind" );
      exit(1);
   }

   if ( listen( listenfd, 5 ) < 0 ) {
      perror( "listen" );
      exit(1);
   }

   for (;;) {
      char *buffer = "Howdy\n";

      if ( (connectfd = accept( listenfd, 
             (struct sockaddr *)NULL, NULL )) < 0 ) {
         perror( "accept" );
         exit(1);
      }

      if ( write( connectfd, buffer, strlen(buffer) ) < 0 ) {
         perror( "write" );
         exit(1);
      }

      close( connectfd );
   }
}

And that, my friends, is our server. If you created a standard tool project for both the client and the server, you can compile and run each. Before you run the client, however, make sure you have the server running. If you want to compile and run these in a shell, type the following commands to invoke the compiler:

% cc -o server server.c

% cc -o client client.c

When running, you might want to open a second shell so you have one for the client and one for the server. Again, make sure you have the server running (by typing ./server from the directory where you compiled the code), and then run the client (./client from the same directory). If you have trouble, here are the source files I worked with for you to play around with. If you want to see something kind of cool, try typing the following in the shell while the server is running (this, incidentally, is a good way of testing server applications):

% telnet localhost 12345

So there you have it -- a very simple example that shows how Unix does networking. If you're interested at all in networking, I can't recommend strongly enough that you pick up the book Unix Network Programming, Volume 1 by W. Richard Stevens. With the prominence of networking capabilities in most applications today, every programmer should have this book. There is a lot more to network programming than what we saw here. There are many considerations to be made for scalability, protocol independence, security, and more. This book covers it all.

In the next column, we'll take what we learned here and see how we use some of the Foundation classes to make RCE an application that can actually communicate over a network.

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the MacDevCenter.com.