macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

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

Our Program for the Day

Today we'll create two small C applications. We won't be learning any new Cocoa today. To code a C application, you have two options. In the first, you can create a new project in Project Builder for the client application, and another project for the server application. The type of project you need to make is a Standard Tool, which is the last item in the New Project Assistant's list of project types. When you create a new standard tool project, you will be presented with a single file in the Sources group: main.c. This file has some code in it that does the whole "Hello, World!" bit -- we can replace all of this with our own code (even the main function and #include statements). If you do the code here today, remember that you have to create a project for both the client and the server code, since we need two executables.



The other alternative is to do this all from Terminal, which is what I did when I wrote the code for this column. Here we only have to work with two files, server.c and client.c, and the cc command. When we get to the relevant parts, I'll show how to compile the code from both the Unix shell and from Project Builder. In each section where I present client and server code, I will go through the steps we have to code in a somewhat isolated manner, and at the end present the entire source code for the component (client or server).

So, with that, onward!

Clients

Clients use sockets in the following manner: first, a client application must create a socket, which is done using the socket() function, as shown here:

int sockfd

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

This will create a TCP/IP streaming socket. A TCP streaming socket is a very reliable means of communication with all sorts of error checking built into the TCP protocol, and a convenient, two-way, byte based interface to communicating. The socket() function takes three arguments: the domain, the type, and the protocol. With the domain parameter we indicate the communications domain of the socket, which is based on a particular address family. The constant AF_INET that we use here tells the socket() function that we will be working with IPv4 addresses. The second argument, the type, specifies the type of socket we want. SOCK_STREAM indicates that we want a TCP streaming socket. Other types include datagram and raw sockets. Finally, we have the protocol argument. This argument isn't used much, since each combination of domain and type usually supports only one protocol. So, we pass 0, and leave it at that.

Related Reading

Objective-C Pocket Reference
By Andrew M. Duncan

Note how we handle an error in the socket() function. If a call to socket() is successful, it will return the file descriptor for the new socket, which is a small, positive integer. However, if the call to socket() resulted in an error, -1 is returned. We can check to see if the value of sockfd is positive or negative in an if-statement, as shown above. If there is an error, we handle it by printing the error message with a call to perror(), and we exit with a status of 1 (1 indicates to the parent process, usually a shell such as tcsh, that there was an error in the program. For more information on perror(), see the perror man page by typing man perror in the Terminal).

With a socket in hand, we call the connect() function, which will attempt to make a connection between the specified socket and a socket listening on a remote host. In the list of functions above, we saw that the connect() function's second argument is type struct sockaddr, which is also used in the bind() and accept() functions. Interestingly, we never work with a sockaddr struct. Rather, sockaddr is kind of like an abstract superclass for protocol and address family-specific socket address structures. In other words, we never work with an actual sockaddr structure, but rather with an address structure for IPv4 or IPV6 addresses.

For IPv4, the address structure we use is of type struct sockaddr_in. The primary reason for the existence of the generic socket address structure type is that when the socket APIs were first written, ANSI C hadn't settled on void * as the generic pointer type. However, sockets had to support multiple address families, and the idea of having a separate set of socket functions for each domain was unappealing to the API developers. To get around this, the writers of the sockets API had to come up with their own generic pointer type specifically for socket address structures: struct sockaddr. That bit of history aside, the definition for struct sockaddr_in is found in the header netinet/in.h (when we are using the sockets API with IPv4, we have to include this header in addition to sys/sockets.h). The definition for struct sockaddr_in is as follows:

struct sockaddr_in {
   u_char  sin_len;
   u_char  sin_family;
   u_short sin_port;
   struct  in_addr sin_addr;
   char    sin_zero[8];     
}; 

The first member of the structure, u_char sin_len, is the length in bytes of the structure. The data type u_char is a synonym for unsigned char; the type definitions for that and other commonly-used types can be found in the header sys/types.h. Except for special uses of sockets, we don't need to set or examine the value of the sin_len member; it is used internally by the kernel in various socket routines. The second member of sockaddr_in is sin_family, which is the address family of the socket. This member is set to the same constant that we passed as the protocol argument of the socket() function (AF_INET, for example). The next member, sin_port, is the port number to which we wish to connect. Our next member, sin_addr, is itself a structure of type struct in_addr. Next we have another struct, in_addr. This structure looks like this (and you'll probably think this is dumb):

struct in_addr {
   in_addr_t s_addr;
};

The sole member of this structure, s_addr, is type in_addr_t. The definition for this type is again found in sys/types.h, in which we find that in_addr_t is a 32-bit unsigned integer, which is a usually an unsigned int. When initializing our socket address structure, this is where we put the IP address to which we are either binding or attempting to connect. Finally, getting back up to sockaddr_in, the last member is just an array that pads the structure to a certain size. All we have to do is make sure that it is initialized to 0.

Now that we know what all is contained in a socket address structure, we can move on with our client code to connect to a server host. Before we can use the connect() function, we have to prepare an address structure so that the function knows where to connect. Socket address structures are prepared by first zeroing the memory that stores the structure, and then we go on to fill in the relevant fields, as shown here:

bzero( &serverAddress, sizeof(serverAddress) );
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons( 12345 );

inet_pton( AF_INET, "127.0.0.1", &serverAddress.sin_addr );

There are several things to take note of here. The first thing we do is initialize the entire structure to 0 using the bzero() function, which stands for byte zero. This function takes the starting address of a chunk of memory that we want to zero, and the number of bytes to zero. In our case we pass the address of the variable serverAddress, obtained using the address operator &, and we pass the result of the sizeof() macro with serverAddress as the parameter (sizeof is used quite frequently in C to determine at runtime the size in bytes of a variable or data type).

Next we set sin_family to AF_INET: the same address family we created our socket with. Next we set the port number in sin_port to 12345. When we get to the server side of all of this, we will see that 12345 is the port number that we will bind our listening socket to.

Note the use of the function htons(). This function stands for host to network short, and it is used to convert a short int from the host byte (little- or big-endian) order to the network byte order (big-endian). If you haven't been introduced to issues of byte order, then here is a quick rundown. Let's continue with the example of a short int: a short int is 16 bits in size, or 2 bytes. Different computer architectures store and read the value of a multi-byte data type in memory differently. If you're scanning through memory and come across the two bytes of a short int variable, will you come across either the low-order byte (little end) or the high-order byte (big end), first. In little-endian systems, you will run across the little end first, while in big-endian systems you run across the big end first.

The use of htons() and other similar functions is necessary because not all platforms order bytes within a multi-byte data type (such as short int, 2 bytes) the same way. We will see later another variant of this function, htonl() -- host to network long.

Lastly, we set the address to which we want to connect. For the purposes of this column, we will connect our socket to the server socket bound to port 12345 on localhost, which has an IP address of 127.0.0.1. The function inet_pton() is a convenient function for converting a string into an in_addr structure. The name of the function is short for Internet Presentation to Number. Presentation refers to the human-readable representation of an IP address, while Number refers to the integer representation of an IP address required by the in_addr struct. By specifying AF_INET in the first argument, we are telling the function that we are working with IPv4 addresses.

Now, let's connect. To connect to a remote socket we do the following:

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

If connect() returns successfully, then we can begin communicating with the server. In our simple client example, we will do nothing more than read a short message sent by the server when a connection is received, which is done using the read() function:

// Declared up above
char buffer[201];
int n;

while ( (n = read( sockfd, buffer, 200 )) > 0 ) {
   buffer[n] = 0;	// Terminate string with null character
   printf( buffer );
}

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

Due to the relatively slow nature of network connections, a client application is not guaranteed to receive the full message sent by the server in the time between calling connect() and the first read() call. Thus, we stick read() in a while loop and check the return value of read() at each pass to see how many bytes were read, and continue reading bytes until there are no more to be read (more data will be received in the time between subsequent calls to read()). Within the loop, we set the nth character of the buffer to the null character, and print out the string we have so far. The array buffer was made 201 bytes large to make room for the null character if the read function did indeed happen to get 200 bytes in one pass. Since we set the next character after the last one read to null, printf() will only print those characters received in the last call to read(). Functions that work with character arrays as strings always look for a null character to signal the end of the string, so that they don't attempt to access memory that is not theirs to access.

So let's take a look at our simple client program in its entirety, and then we'll get on to discuss how to make a super-simple server for our client. Here is client.c:

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

int main( int argc, char **argv )
{
   int n, sockfd;
   char buffer[201];
   struct sockaddr_in serverAddress;

   if ( (sockfd = 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 );

   inet_pton( AF_INET, "127.0.0.1", &serverAddress.sin_addr );

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

   while ( n = read( sockfd, buffer, 200) ) {
      buffer[n] = 0;
      printf( buffer );
   }

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

   return 0;
}

Pages: 1, 2, 3

Next Pagearrow