Basic C TCP/IP Programming

Standard

In this article, I share how to have some TCP/IP programming with the C language.  I am using FreeBSD because it is my most familiar platform but it does not prevent you trying the source code elsewhere.  There are of course thousands of examples online.  To show that I am different, I will present my code in an unorthodox but effective way.  (I am a protestant, if you force me to answer.  🙂

I am using the word TCP/IP in the title, in case I want to have things like libibverbs in another time.  In this article, when I use the word “network”, I refer to TCP/IP.

Header Files

Some header files are required, such as standard system call headers, data type definitions, string operations, etc.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netdb.h>
#include <string.h>

System Calls and File Descriptors

In Unix system, a lot of things are presented as file descriptors and these include the network sockets we discuss here.  There are various system calls to get file descriptors of different types, such as open(2) for a named file, pipe(2) for inter-process communications, and socket(2) for a network socket.  Even so, once a file descriptor is prepared, the operations are similar: read(2) for withdrawing message, write(2) for depositing message, and close(2) for finishing after use.

Things are easy to be said than to be done.  To make a network connection, there are quite some steps that must go through.  Here I split into two roles, a server that receives connection, and a client that initiates connection.

Server:

  1. socket(2) to create a network socket
  2. bind(2) to attach to a particular defined network port
  3. listen(2) to create a connection queue
  4. accept(2) to accept a connection request
  5. read(2) or write(2) to communicate
  6. close(2) to finish

Client:

  1. socket(2) to create a network socket
  2. connect(2) to connect to a particular address
  3. read(2) or write(2) to communicate
  4. close(2) to finish

Obtaining Socket Address

In order to bind or connect, one needs to provide a socket address.  It is a structure with complex data structure.  Some writers would tell readers to fill in the structures one by one, tell them to be caution about the endiness, etc.  I am lazy and just use the getaddrinfo(3) function.  It is also the most reliable method if you want to handle different network types.  It takes the hostname, port number, and optionally hints as input.  It generates a data structure and return the pointer through a pointer of pointer.  The most difficult part is filling in the hints, but these can be blindly copied.  For example, to get a TCP over IPv4, we request SOCK_STREAM and AF_INET like…

memset(&hint, 0, sizeof(hint));
hint.ai_family = AF_INET;
hint.ai_socktype = SOCK_STREAM;
hint.ai_protocol = 0;
getaddrinfo(host, port, &hint, &info);

Given there are not errors, the info structure now contains the answer.  “info->ai_addr” points to the required sockaddr structure, and “info->ai_addrlen” points to the length of the answer.  Just in case you did not define the numbers, also helps by filling in the “info->ai_family,” “info->ai_socktype”, and “info->ai_protocol” according to the hints.  These are useful in creating the first socket.

Please note the address info can be a linked list when there are multiple options for the connection.  To be robust, one may want to try out all the connections.  For demonstration, trying one is enough.  After use, it can be cleanup with the corresponding freeaddrinfo(3) function.

Error Handling

Most system calls come with exceptional situations, like when a file is not found, or a network socket is not connectable.  It is a good practice to check the return value of each system call.  The code becomes very messy and this is when people yell for the “exception” feature of a language — put the error handling code out of the normal execution!  Hold on, checking return value can be trivial with C macro functions.  I learned this trick from a famous book but I do not recall the name.

#define pt {fprintf(stderr, "%s:%d: ", __FILE__, __LINE__); perror("");}
#define ez(x) {if ((x) != 0) {pt; goto error;}}
#define ep(x) {if ((x) <= 0) {pt; goto error;}}
#define ezp(x) {if ((x) < 0) {pt; goto error;}}

Whenever I expect it to return zero, I use ez (expect zero); also ep for expecting positive, and zp for expecting natural numbers.  With these, the error checking can be much easier, for example,

if (listen(fd, 1) != 0) {
        fprintf(stderr, "%s:%d: ", __FILE__, __LINE__);
        perror("");
        goto error;
}

can be replaced as

ez(listen(fd, 1));

At the end of the function, I just define a label to catch these errors and perform cleanup.  This is now even simpler than handling exceptions.  The actual cleanup code will be shown later.

The Server Code

The server first prepares the hints and obtains the socket address accordingly.  Then it calls the socket, bind, listen, accept, write system call accordingly.  There are two file descriptors, one for binding to the particular port, and one (or more) for communicating with the clients.  After use, it cleans up cautiously.  If there is ever error, the flow jumps to “error” immediately and the variable is updated accordingly.

int server(const char* host, const char* port)
{
        int fd = -1;
        int fd2 = -1;
        int error = 0;
        struct addrinfo* info = 0;
        struct addrinfo  hint;
        char message[16] = "hello world";
        memset(&hint, 0, sizeof(hint));
        hint.ai_family = AF_INET;
        hint.ai_socktype = SOCK_STREAM;
        hint.ai_protocol = 0;
        ez(getaddrinfo(host, port, &hint, &info));

        ezp(fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol));
        ez(bind(fd, info->ai_addr, info->ai_addrlen));
        ez(listen(fd, 1));
        ezp(fd2 = accept(fd, 0, 0));
        ep(write(fd2, message, sizeof(message)));

cleanup:
        if (info != 0) freeaddrinfo(info);
        if (fd2 != -1) close(fd2);
        if (fd != -1) close(fd);
        return error;

error:
        error = 1;
        goto cleanup;
}

The Client Code

The client code is similar to the server, except it only has one file descriptor and it connects rather than bind or connect.  After reading the message from the server, it prints it out and finish.

int server(const char* host, const char* port)
{
        int fd = -1;
        int fd2 = -1;
        int error = 0;
        struct addrinfo* info = 0;
        struct addrinfo  hint;
        char message[16] = "";

        memset(&hint, 0, sizeof(hint));
        hint.ai_family = AF_INET;
        hint.ai_socktype = SOCK_STREAM;
        hint.ai_protocol = 0;
        ez(getaddrinfo(host, port, &hint, &info));
        ezp(fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol));
        ez(connect(fd, info->ai_addr, info->ai_addrlen));
        ep(read(fd, message, sizeof(message)));
        printf("client received: %s\n", message);

cleanup:
        if (info != 0) freeaddrinfo(info);
        if (fd2 != -1) close(fd2);
        if (fd != -1) close(fd);
        return error;

error:
        error = 1;
        goto cleanup;
}

Putting Them Together

Finally, we need a main function to run a server and a client about the same time.  Here we use fork(2) and wait(3) calls.  The client is delayed 1 second to ensure the server has got ready before a connection is established.  The code pasting will be left as an exercise.  In short…

  1. The header files
  2. The error handling macros
  3. The server code
  4. The client code
  5. The main function

The main function is as follows.  After fork, there are two processes for the two roles.  The server is started to accept connections from port 8080 of any given IP addresses.  The client is targeting localhost port 8080.

int main(int argc, char** argv)
{
        int status = 0;
        pid_t pid = fork();

        // Forked as a parent
        if (pid > 0) {
                server(0, "8080");
                waitpid(pid, &status, 0);
        }

        // Forked as a child
        if (pid == 0) {
                sleep(1);
                client("localhost", "8080");
        }

        // Something went wrong with forking
        if (pid < 0) {
                perror("fork");
        }
}

And the program execution is as simple as…

# clang net.c -o net
# ./net
client received: hello world
Advertisements