mario::konrad
programming / C++ / sailing / nerd stuff
Multithreading: Tutorial 04: Client/Server using Sockets and Threads
© 2002 / Mario Konrad

Introduction

Prequisites for this tutorial is knowledge in programming (lanugage C), basics about threading (see previous tutorials) and basic knowledge of sockets. Sockets not in detail, but the mechanism in general.

This tutorial is intended to show you how to use threads in more than a small example of synchronisation. Client/Server-Systems are very useful if you like to provide a service, reachable in the entire network. This is what many of the deamons do.

Usually, those services are programmed using the fork command, this means multi-processing (in the terms of having multiple processes and not multi processors). The process handling is much more resource consuming than the handling of threads, the complexity is about the same. The advantage of being less resource consuming has an important disadvantage. If one thread (handling one connection) dumps the core, it will affect the other threads as well.

Code Walkthrough

First, we’ll have a look at the client and afterwards at the server. I choose this order because all the threading is in the server part and the server part is also more complex. So let’s do the boring part first.

Client

The client program is designed to take three commandline paramters:

  1. The name or IP address of the host on which the server is running
  2. The port number on which the server is listening
  3. The message

First several headers to include.

#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>

The function main is the only one we’ll need within the client program. But we’ll need some variables.

int main(int argc, char ** argv)
{
    int port;
    int sock = -1;
    struct sockaddr_in address;
    struct hostent * host;
    int len;

Since the client program should take several commandline parameter, we have to check them. If they are not in a proper form, let’s print the usage of the client program.

    /* checking commandline parameter */
    if (argc != 4)
    {
        printf("usage: %s hostname port text\n", argv[0]);
        return -1;
    }

The number of parameters was ok, let’s try to parse the port number. If it does not work the client program must stop.

    /* obtain port number */
    if (sscanf(argv[2], "%d", &port) <= 0)
    {
        fprintf(stderr, "%s: error: wrong parameter: port\n", argv[0]);
        return -2;
    }

So far, so good. Now, we’re ready to handle the socket. First we have to create one, a TCP socket for streaming data and for the Internet domain.

    /* create socket */
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock <= 0)
    {
        fprintf(stderr, "%s: error: cannot create socket\n", argv[0]);
        return -3;
    }

Next, we have to deal with the address of the server. The function gethostbyname does the trick for us:

    /* connect to server */
    address.sin_family = AF_INET;
    address.sin_port = htons(port);
    host = gethostbyname(argv[1]);
    if (!host)
    {
        fprintf(stderr, "%s: error: unknown host %s\n", argv[0], argv[1]);
        return -4;
    }
    memcpy(&address.sin_addr, host->h_addr_list[0], host->h_length);

Ok, we have now all we need: socket, address to host. We’re able to connect to the server:

    if (connect(sock, (struct sockaddr *)&address, sizeof(address)))
    {
        fprintf(stderr, "%s: error: cannot connect to host %s\n", argv[0], argv[1]);
        return -5;
    }

If the program reaches this far, a connection from the client machine to the server machine is now open (even they are on the same machine).

Next action is to send the message to the server. First we send the length of the message, letting the server know what’s going on, and then the message itself. This is very easy because we can use the well known write statement.

    /* send text to server */
    len = strlen(argv[3]);
    write(sock, &len, sizeof(int));
    write(sock, argv[3], len);

All what’s left to do is to close the socket and quit the program.

    /* close socket */
    close(sock);

    return 0;
}

This client program was quite simple, wasn’t it?

Server

The server program is more complex than the client program, because here sits the entire handling of multiple connections, using threads of course.

Don’t be afraid, this sounds more complex than it really is.

The server program takes one parameter: the port number.

The first thing we need is the inclusion of a few header files:

#include <stdio.h>
#include <pthread.h>
#include <sys/socket.h>
#include <linux/in.h>

For a better handling of the connection context, let’s define a data structure:

typedef struct
{
    int sock;
    struct sockaddr address;
    int addr_len;
} connection_t;

This data structure holds three varialbes:

  1. sock : the socket of the connection to one client
  2. address : the address of a connected client
  3. addr_len : the length of the address field

Those are all available information about one connected client.

It is not a good design to handle all the clients in one function (main). So, let’s do the work in a separate function. This comes also in handy, because of the thread handling. Remember: we’ll need a function with the following signature:

void * function(void *);

to do the threads work.

void * process(void * ptr)
{
    char * buffer;
    int len;
    connection_t * conn;
    long addr = 0;

After the declaration of some local variables, we need to check the specified parameter and cast it to our desired data type:

    if (!ptr) pthread_exit(0);
    conn = (connection_t *)ptr;

The real work begins now. Maybe you have a look at the client code. The first thing that was submitted was the length of the message as an integer. As the server, we need to read this number from the socket.

    /* read length of message */
    read(conn->sock, &len, sizeof(int));
    if (len > 0)
    {

If the length of the message tells us the message is worth to be processed, we’ll have to allocate some memory (for the buffer). The first statement is just for fun: obtaining the client’s IP address for further use.

        addr = (long)((struct sockaddr_in *)&conn->address)->sin_addr.s_addr;
        buffer = (char *)malloc((len+1)*sizeof(char));
        buffer[len] = 0;

Reading the message is again a very simple task. Just use the function read to read the message from the socket.

        /* read message */
        read(conn->sock, buffer, len);

Print the message out to stdout and free the allocated memory for the buffer.

        /* print message */
        printf("%d.%d.%d.%d: %s\n",
            (addr      ) & 0xff,
            (addr >>  8) & 0xff,
            (addr >> 16) & 0xff,
            (addr >> 24) & 0xff,
            buffer);
        free(buffer);
    }

Since the great show is over, we have to do some clean up work. Closing the socket, freeing the memory of the conenction context and ending the thread.

    /* close socket and clean up */
    close(conn->sock);
    free(conn);
    pthread_exit(0);
}

In the method above we had a look at the real work: reading the message from the socket and processing it (in this case: write to stdout).

To make the server program work, we need to do some initialisation work of socket and threads. This is done within the function main. We already saw a few things in the client code.

int main(int argc, char ** argv)
{
    int sock = -1;
    struct sockaddr_in address;
    int port;
    connection_t * connection;
    pthread_t thread;

The commandline parameter processing hasn’t to be discussed again. If you’re not sure about this, have a second look at the client code.

    /* check for command line arguments */
    if (argc != 2)
    {
        fprintf(stderr, "usage: %s port\n", argv[0]);
        return -1;
    }

    /* obtain port number */
    if (sscanf(argv[1], "%d", &port) <= 0)
    {
        fprintf(stderr, "%s: error: wrong parameter: port\n", argv[0]);
        return -2;
    }

The creation of the socket is also simple. We saw this in the client code as well.

    /* create socket */
    sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock <= 0)
    {
        fprintf(stderr, "%s: error: cannot create socket\n", argv[0]);
        return -3;
    }

One new thing is that we have to bind the created socket to the specified port. This is done using the function bind. Please have a look at the man pages if you have questions about bind: $ man 2 bind.

    /* bind socket to port */
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(port);
    if (bind(sock, (struct sockaddr *)&address, sizeof(struct sockaddr_in)) < 0)
    {
        fprintf(stderr, "%s: error: cannot bind socket to port %d\n", argv[0], port);
        return -4;
    }

Now we have to specify that we are a server that likes to listen on the port. The function listen is doing exactly this. For more information:

$ man 2 listen
    /* listen on port */
    if (listen(sock, 5) < 0)
    {
        fprintf(stderr, "%s: error: cannot listen on port\n", argv[0]);
        return -5;
    }

A simple status message to tell that the server is now up and ready to receive connections from clients.

    printf("%s: ready and listening\n", argv[0]);

Now we have done all necessary initialisation work. The main loop is straight ahead. Actually it does only three things: creating a connection context (connection), accepting connections from clients (blocking IO) and starting threads with the proper connection context, that all.

    while (1)
    {
        /* accept incoming connections */
        connection = (connection_t *)malloc(sizeof(connection_t));
        connection->sock = accept(sock, &connection->address, &connection->addr_len);
        if (connection->sock <= 0)
        {
            free(connection);
        }
        else
        {
            /* start a new thread but do not wait for it */
            pthread_create(&thread, 0, process, (void *)connection);
            pthread_detach(thread);
        }
    }

Althrough this code is never reached it’s for a good design, and it let’s the compiler think: everything is ok, no warnings needed.

    return 0;
}

It wasn’t too complex, wasn’t it? Most of the code is handling of exceptions. If you reduce the real work (without bells and whistles), all that would be left are a few lines of code. I like you to notice one further thing: the handling of the threads is done in three (!) lines of code: creating the thread (pthread_create), detaching it (pthread_detatch) and correct termination of the thread (pthread_exit). The handling of the socket was much more complex.

Compilation and Execution

To compile and run the code provided by this exammple, you’ll need Linux, gcc and the pthread package. Small changes will make it run with Cygwin (an Unix environment for Win32, external link).

To compile use the following sequence of commands:

$ gcc -o server server.c -lpthread
$ gcc -o client client.c

To run the example you should start a new terminal. In one terminal you start the server, providing a port number on which the server should listen:

$ ./server 5678

On the other terminal run the client:

$ ./client localhost 5678 "Hello World"

It is also possible to run the client on another machine:

$ ./client host 5678 "Hello World"

With host as name or IP address of the machine on which the server program is listening.

If you like a usage of each program just type:

$ ./server
$ ./client

Download Source Code

All source code files provided by this page is free to copy, modify, redistribute and use for any purpose. The tutorial is copyrighted by Mario Konrad.