Socket Programming in C

Building a simple client/server application is the standard first internet-based application developers attempt. These applications are built on top of the socket communication library, but socket programming in C++ is not obvious as there are no standard libraries, and thus, you have to fall back to the C API. The closest "standardish" sort of thing we have is Boost.asio, which is at the other end of the spectrum in terms of API and involves a cognitive leap to understand what is happening underneath (or you can trust the library maintainers). The other alternative is libcurl; the "easy curl" layer is an abstraction of the socket() API, while the "multi curl" layer is an abstraction of the pselect() API that allows multiple sockets to be handled in a single thread.

I am writing a series of articles, starting with a basic C++ client/server application and walking through the process of building a C++ communication library. During this process, I will be using examples from codereview.stackexchange.com to illustrate common mistakes and try to show how to write the code correctly (This will also be a learning exercise for me, so please let me know if you spot a mistake).

Currently, the plan is to write the following articles:

  • Client/Server C
  • Client/Server C Read/Write
  • Client/Server C++ Wrapper
  • Mult-Threaded Server
  • Non-Blocking Socket
  • Co-Routines

Client/Server C++ Basic Version

The minimum example of a working Client/Server application in C++: The whole working version is here

C Server

#include <netinet/in.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SERVER_BUFFER_SIZE      1024

int main()
{
    int socketId = socket(PF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serverAddr;
    bzero((char*)&serverAddr, sizeof(serverAddr));
    serverAddr.sin_family       = AF_INET;
    serverAddr.sin_port         = htons(8080);
    serverAddr.sin_addr.s_addr  = INADDR_ANY;
    bind(socketId, (struct sockaddr *) &serverAddr, sizeof(serverAddr));

    listen(socketId, 5);

    int                         finished    = 0;
    while(!finished)
    {
        struct  sockaddr_storage    serverStorage;
        socklen_t                   addr_size   = sizeof serverStorage;
        int newSocket = accept(socketId, (struct sockaddr*)&serverStorage, &addr_size);

        char        buffer[SERVER_BUFFER_SIZE];
        int         get = read(newSocket, buffer, SERVER_BUFFER_SIZE - 1);

        buffer[get] = '\0';
        fprintf(stdout, "%s\n", buffer);

        write(newSocket, "OK", 2);

        fprintf(stdout, "Message Complete\n");

        close(newSocket);
    }
    close(socketId);
}

C Client

#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define CLIENT_BUFFER_SIZE     1024

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        fprintf(stderr, "Usage: client <host> <Message>\n");
        exit(1);
    }

    int socketId = socket(PF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serverAddr;
    socklen_t addrSize = sizeof(serverAddr);
    bzero((char*)&serverAddr, sizeof(serverAddr));
    serverAddr.sin_family       = AF_INET;
    serverAddr.sin_port         = htons(8080);
    serverAddr.sin_addr.s_addr  = inet_addr(argv[1]);
    connect(socketId, (struct sockaddr*)&serverAddr, addrSize);

    write(socketId, argv[2], strlen(argv[2]));

    shutdown(socketId, SHUT_WR);

    char    buffer[CLIENT_BUFFER_SIZE];
    size_t  get = read(socketId, buffer, CLIENT_BUFFER_SIZE - 1);

    buffer[get] = '\0';
    fprintf(stdout, "%s %s\n", "Response from server", buffer);

    close(socketId);
}

This version of the Client/Server works (a lot of the time) but has a couple of significant issues.

Checking Error Codes

If the calls to socket(), bind(), listen(), or connect() fail, we have a catastrophic error; any further actions will also fail. A few of the error codes generated by these functions can potentially be recovered from, but most are programming errors or permission failures. As a result, a human-readable message with application termination is an acceptable solution (at this point).

Note: When these functions fail, they set the global variable errno, which can be translated into a human-readable string with strerror(). Thus, the simplest solution is to generate an appropriate error message for the user and terminate the application.

Socket Validation

int socketId = socket(PF_INET, SOCK_STREAM, 0);
if (socketId == -1)
{
    fprintf(stderr, "Failed: socket()\n%s\n", strerror());
    exit(1);
}

Bind Validation

if (bind(socketId, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) == -1)
{
    fprintf(stderr, "Failed: bind()\n%s\n", strerror());
    close(socketId);    // Don't forget to close the socket.
    exit(1);
}

Listen Validation

if (listen(socketId, 5) == -1)
{
    fprintf(stderr, "Failed: connect()\n%s\n", strerror());
    close(socketId);    // Don't forget to close the socket.
    exit(1);
}

Connect Validation

if (connect(socketId, (struct sockaddr*)&serverAddr, addrSize) == -1)
{
    fprintf(stderr, "Failed: connect()\n%s\n", strerror());
    close(socketId);    // Don't forget to close the socket.
    exit(1);
}

Summary

The basic socket programs are relatively trivial. However, version 1 has some obvious flaws, the foremost of which is checking error states (which many beginners forget in their first version). The following article will provide more details about read and write operations on the socket.

Inspiration for Article

Related Posts

C++ Wrapper for Socket

The last two articles examined the "C Socket" interface provided by the OS. In this article, I wrap this functionality in a simple C++ class to provide guaranteed closing and apply a consistent except

Read More

Common Mistakes

### 1: using namespace Every new developer who comes to C++ always starts writing code like this: #### myfirstprog.cpp ```c #include <iostream> using namespace std; ``` It seems reasonable, and e

Read More

Control Flow

So far, we have created basic programs that perform a single task without making any decisions. Most (all but the most trivial) programming languages provide decision-making constructs (Conditional B

Read More