Socket programming

Sockets are endpoints in communication. We create a socket on the server application and sit and listen for incomming connections. Clients initiate a connection and send data once the connection is established.

There are two types of sockets; connected and datagram sockets. Connected sockets keep the connection between the client-server. With datagram sockets, there is no gurantee of delivery of packet and we have to use packet counting mechanism if we want to gurantee delivery of data. However, datagram sockets are less resource intensive and can be used with applications like media streaming applications.

Let us look at connection oriented sockets.

For the server side, the basic steps are:

  • Create a socket (int aSocket = ::socket(...) )
  • Bind the socket to an address and port
  • Call accept in a loop to listen to connection requests. This call blocks until there is a connection request. On return, the OS returns a socket which we use to communicate with the client application (see example)

On the client side, we create a socket simply call connect and we are off.

Data structures used:

1. sockaddr and sockaddr_in are used to specify the host and port we want to connect to. Both these structures are of the same size. All networking calls where we need to specify an address take a pointer to a sockaddr structure as a parameter. With TCP/IP sockets, we fill in a sockaddr_in structure and typecast it to a sockaddr structure in calls to networking functions.


typedef uint32_t in_addr_t;

struct in_addr
{
  in_addr_t s_addr;
};

struct sockaddr_in
{
  sa_family_t sin_family;
  in_port_t sin_port;
  struct in_addr sin_addr;

  unsigned char __pad[16 - sizeof(short int)
   - sizeof(unsigned short int) - sizeof(struct in_addr)];
};

The following functions can be used to convert dotted (127.0.0.1) to binary and vice-versa:

    #include 
    in_addr_t inet_addr(const char *cp);
    char *inet_ntoa(struct in_addr in);

--more stuff to follow ---

Using the select command

Typically, on the server side, we would have several sockets which we would read and write data to. We use the select function to determine which sockets we can read and/or write to without the call blocking.

The select call takes the following parameters:

   int select(int maxFD, fd_set *read, 
              fd_set *write, 
              fd_set *exceptions, 
              struct timeval *tv);

where:

  • The first parameter, i.e. maxFD in the above call is the highest number +1 of descriptor in read, write and except set we are interested in. For example, if we currently have sockets 4,7 and 9 open and we wish to read from any one of these, we would pass 10 as the first argument to select call.
  • read, write and ex in the call above are pointers to three fd_set structures which specify the sockets we are interested in. This structure is described below. Any or all of these parameters can be null. For example, if we are only interested in determining which sockets have data to read, we would pass NULL for the write and exceptions sets. We use the FD_SET, FD_CLR calls to set, clear individual sockets and FD_ZERO to zero out the entire set. FD_ISSET is used to determine which sockets are ready (see example below).
  • The timeval struct gives the number of seconds to wait before returning. This argument can be used to specify an indefinite wait, or a definite wait time, as described below.
  • The return value is the total number of descriptors that are ready, with -1 representing an error condition, 0 means that the call timed out before any of the descriptors became ready, etc.

The fd_set structure is simply a bit map with each bit representing an integer value. For example, if we use the FD_SET macro like FD_SET( 4,&readFD), bit 4 in the readFD will be set.

The struct timeval is as follows:

    struct timeval
    {
        long tv_sec;
        long tv_usec;
     };

where tv_sec and tv_usec specify the seconds and microseconds to wait.

  • If the tv parameter is null, then the kernal will wait forever until a socket does become ready.
  • If both tv_sec and tv_usec are zero, then the call will return immediately.
  • If either of these parameters is non-zero, then the call will wait for the specified interval. We can then use the return value from this call to check if any of the sockets are ready. The POSIX specs allow the call to modify this struct, so remember to reset the values before each call.

If a bit is set on the exception set, that indicates the arrival of out-of-band data.

Note that setting a socket in the non-blocking mode will not affect the way select works. So, even if all the sockets in the read set are non-blocking, the select call will wait as specified by the tv parameter.

Producer-Consumer using Windows threading

Here is a simple solution to the producer-consumer problem using Windows threading. Basic classes are
  • Producer This is the thread that produces jobs at random intervals
  • Consumer This is the thread that pulls jobs off the messageQueue and processes the job. There are multiple consumers.
  • MessageQueue Fixed size queue to hold the jobs
  • Job Abstract job class that the Consumer consumes
  • Thread Utility class to manage trreads