Making a MUD - 1 - Basic Server Code

Go back to part 0

Go forward to part 2

I decided that the starting point of this project would be server code that I can test by connecting using Telnet. For the most part, I'm going to leave out all the #include directives and such when posting code.

This may be the largest amount of code I will put into one post, but it's necessary to build up from for the rest of the project.

The Main Function

int main (int argc, char * argv [])
{
    log_open("/dev/stdout");
    log_printf("MUD Server started");

    struct Listening_Socket * server = malloc(sizeof(*server));
    start_listening_for_clients(server, "50000", 5);

    log_printf("Waiting for game shutdown signal");
    wait_for_game_shutdown_signal();
    log_printf("Game shutdown signal fired");

    stop_listening_for_clients(server);
    free(server);

    log_printf("MUD Server stopped");
    log_close();

    exit(EXIT_SUCCESS);
}

First, we open a log to tell us what is going on. While debugging, the log file will be /dev/stdout, which means all log messages will be displayed on the command line that we execute this on. We will eventually point to a real file when the game becomes a real server that we aren't watching the entire time it runs.

Then, we create a listening socket on port 50000, allowing a connection backlog of 5 connections. These numbers were chosen arbitrarily. start_listening_for_clients spawns a new thread and starts listening.

Then, the main thread waits to be told to clean up.

Once the shutdown signal is triggered, we close the listening socket, clean up its resources, close the log, and exit.

Logging

static FILE * log_stream = NULL;
static pthread_mutex_t write_lock = PTHREAD_MUTEX_INITIALIZER;

void log_open (const char * filename)
{
    /* TODO: Error */
    log_stream = fopen(filename, "a");
}

void log_close ()
{
    if (log_stream == NULL)
    {
        return;
    }

    fclose(log_stream);
    log_stream = NULL;
}

void log_printf (const char * format, ...)
{
    if (log_stream == NULL)
    {
        return;
    }

    pthread_mutex_lock(&write_lock);

    time_t current_time = time(NULL);
    fprintf(log_stream, "%lld : ", (unsigned long long)current_time);

    va_list ap;
    va_start(ap, format);
    vfprintf(log_stream, format, ap);
    va_end(ap);

    fprintf(log_stream, "\n");

    pthread_mutex_unlock(&write_lock);
}

These are the logging functions. Although they amount to fprintf, I prefer to abstract the logging functions. This way, if I decide to log to syslog, log errors to stderr, or do a combination, I only change this file and no other code has to be touched.

There is also mutex locking, meaning that my log will be thread safe. There will be many threads running in this game, and they will all try to write to the log at the same time. If you've never experienced that, imagine 20 people all typing an essay, ignoring each other, while using the same keyboard. The output would be nothing but gibberish. The mutex only allows one person to type at a time.

Each line of the log will display the UNIX time and one line of output.

The header file is worth mentioning:

#ifndef _LOG_H_
#define _LOG_H_

void log_open (const char * filename);
void log_close ();

void log_printf(const char * format, ...);

#ifdef DEBUG
#define THERESERIOUSLYISNTAMACROFORTHISYET(x) #x
#define STRINGIFY(x) THERESERIOUSLYISNTAMACROFORTHISYET(x)

#define log_debug(...) log_printf("[" __FILE__ ":" STRINGIFY(__LINE__) "] " __VA_ARGS__)

#else
#define log_debug(...)
#endif

#endif /* _LOG_H_ */

There are two logging functions: log_printf and log_debug.

log_printf always logs to the file.

log_debug is log_printf with two caveats. First, the source file name and line number are prepended to the log message. This makes following what the code is doing much easier when debugging. For example, this is taken from a future version of this code:

1397064553 : MUD Server started
1397064553 : [src/game_config.c:40] Determining config filename

If the game crashed right after this line was output, then I know to start looking for the bug right after line 40 of game_config.c.

The other part is during compilation. All the debug messages are removed from the executable during compile time if -DDEBUG isn't passed to the compiler. When debugging is not enabled, this makes the executable smaller and the code faster (because no message exists to process and no function call is performed).

Alternatively, I can turn on debugging for only one source file at a time by adding #define DEBUG to the top of the file. When a thousand threads are outputting debug messages, the log becomes almost useless. If I know there is a bug in one section of the code, I can turn on debug messages for just the relevant section.

Finally, because C string concatenation is used, the code to call log_debug and log_printf is the same, meaning I can switch which log messages are "normal" or "debug" with a quick Find & Replace in my editor.

The Server Socket

This is the listening socket structure:

struct Listening_Socket
{
    int socket_fd;
    pthread_t thread_id;
    char port_string [6];
};

It stores the socket file descriptor, the thread that the thread is listening in, and the listening port number saved as a string.

int start_listening_for_clients (struct Listening_Socket * listening_socket, const char * port_string, int maximum_connection_queue)
{
    /* Set up listening TCP socket */
    struct addrinfo hints;
    struct addrinfo * servinfo;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    /* TODO: Check error */
    getaddrinfo(NULL, port_string, &hints, &servinfo);

    /* Create socket */
    log_debug("Creating socket");
    int server_socket_fd = socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol);
    if (server_socket_fd < 0)
    {
        return -1;
    }

    /* Only wait for a connection from localhost */
    const char interface_name [] = "lo";
    setsockopt(server_socket_fd, SOL_SOCKET, SO_BINDTODEVICE, interface_name, sizeof(interface_name));

    /* Allow quick restarts */
    int allow = 1;
    setsockopt(server_socket_fd, SOL_SOCKET, SO_REUSEADDR, &allow, sizeof(allow));

    bind(server_socket_fd, servinfo->ai_addr, servinfo->ai_addrlen);

    /* Start listening for the connection */
    listen(server_socket_fd, maximum_connection_queue);

    freeaddrinfo(servinfo);


    /* Spawn new thread to wait for connections */
    listening_socket->socket_fd = server_socket_fd;
    listening_socket->thread_id = 0;
    strcpy(listening_socket->port_string, port_string);

    struct Listen_Thread_Arg * listen_thread_arg = malloc(sizeof(*listen_thread_arg));
    listen_thread_arg->listening_socket = *listening_socket;

    /* TODO: Error */
    pthread_create(&(listening_socket->thread_id), NULL, listen_thread, listen_thread_arg);

    return 0;
}

int stop_listening_for_clients (struct Listening_Socket * listening_socket)
{
    log_printf("Stopping server fd %d listening on port %s", listening_socket->socket_fd, listening_socket->port_string);

    /* TODO: Error */
    pthread_cancel(listening_socket->thread_id);

    /* TODO: Error */
    pthread_join(listening_socket->thread_id, NULL);

    /* TODO: Error */
    close(listening_socket->socket_fd);

    log_printf("Closed server fd %d listening on port %s", listening_socket->socket_fd, listening_socket->port_string);

    return 0;
}

Most of this code is taken directly from the man pages. getaddrinfo is used rather than the deprecated method to be IPv6 compatible.

SO_REUSEADDR is set so I can close the server, make a code fix, and start it again quickly rather than having to wait for the built-in timeout in Linux.

I also bound the listening socket to the loopback interface, meaning that no one can connect to this socket except the machine that is actually running the server. This isn't really necessary; it was already in the code from the project I copied this from. I've written a lot of code that requires loopback sockets for various reasons.

Once the socket is set up, lines 39-48 set up a structure and spawn a thread running this function:

struct Listen_Thread_Arg
{
    struct Listening_Socket listening_socket;
};

static void * listen_thread (void * arg)
{
    struct Listen_Thread_Arg * listen_thread_arg = arg;

    int server_socket_fd = listen_thread_arg->listening_socket.socket_fd;

    log_printf("Server fd %d listening on port %s", server_socket_fd, listen_thread_arg->listening_socket.port_string);

    free(arg);

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    for (;;)
    {
        /* TODO: Save client information */
        int socketfd = accept(server_socket_fd, NULL, NULL);

        /* TODO: Spawn new connection thread */
        pthread_t player_thread;
        struct Player_Connection_Arg * player_connection_arg = malloc(sizeof(*player_connection_arg));
        player_connection_arg->socket_fd = socketfd;
        pthread_create(&player_thread, &attr, start_player_connection, player_connection_arg);
    }

    pthread_attr_destroy(&attr);

    pthread_exit(NULL);
}

This thread simply waits for an incoming connection. When a new connection is found, it takes that connection and spawns a new thread for it. This new thread will run start_player_connection. This new thread will be started DETACHED, meaning that once it exits, it will clean up after itself. Players will connect and disconnect all the time; we don't care when.

The Player's Connection

listen_thread filled in this structure with the new thread's parameters:

struct Player_Connection_Arg
{
    int socket_fd;
};

This is passed to this very rudimentary code:

void * start_player_connection (void * arg)
{
    struct Player_Connection_Arg * player_connection_arg = arg;
    int socket_fd = player_connection_arg->socket_fd;
    free(player_connection_arg);

    const char name_prompt [] = "Name: ";

    send(socket_fd, name_prompt, sizeof(name_prompt) - 1, 0);

    char buffer [1024];

    socket_receive(socket_fd, buffer, 1024);

    const char disc [] = "Bye!\n";
    send(socket_fd, disc, sizeof(disc) - 1, 0);

    close(socket_fd);

    if (strncmp(buffer, "shutdown", 8) == 0)
    {
        signal_game_shutdown();
    }

    pthread_exit(NULL);
}

All this code does is ask the player for a name. The player is disconnected immediately afterward.

If the player's name is "shutdown", then the shutdown signal is fired.

Socket Sending and Receiving

socket_send and socket_receive are the following functions. We are using TCP streams, so each call to the C functions send and recv aren't guaranteed to collect the entire message in one call.

socket_send won't return until the entire message is sent to the player.

socket_receive won't return until the user presses the Enter key i.e. sends a newline character. Carriage returns are automatically filtered. Only LATIN-1 characters are accepted; no multi-byte characters are allowed. This will be step 1 of sanitizing the user's input.

int socket_send (int socket_fd, const char * buffer, size_t buffer_size)
{
    size_t bytes_sent = 0;

    while (bytes_sent < buffer_size)
    {
        ssize_t retval = send(socket_fd, buffer + bytes_sent, buffer_size - bytes_sent, 0);
        if (retval > 0)
        {
            bytes_sent += retval;
        }
        else
        {
            /* TODO: Error */
            return -1;
        }
    }

    return -1;
}

int socket_receive (int socket_fd, char * dest_buffer, size_t buffer_size)
{
    /* TODO: Use buffer_size */

    char * index = dest_buffer;

    for (;;)
    {
        recv(socket_fd, index, 1, 0);
        log_debug("Received character %c", *index);

        if (*index == '\n')
        {
            *index = '\0';
            return 0;
        }
        else if (*index == '\r')
        {
            continue;
        }
        else if (*index & 0x80) /* Only accept LATIN-1 */
        {
            const char paranoia [] = "Illegal character. Paranoia disconnection.\n";
            socket_send(socket_fd, paranoia, sizeof(paranoia) - 1);
            return -1;
        }
        else
        {
            ++index;
        }
    }

    return -2;
}

The Game Shutdown Signal

Finally, this is the code used to trigger and catch the game shutdown signal.

static pthread_mutex_t game_shutdown_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t game_shutdown_signal = PTHREAD_COND_INITIALIZER;

void wait_for_game_shutdown_signal ()
{
    pthread_mutex_lock(&game_shutdown_lock);
    pthread_cond_wait(&game_shutdown_signal, &game_shutdown_lock);
    pthread_mutex_unlock(&game_shutdown_lock);
}

void signal_game_shutdown ()
{
    pthread_cond_broadcast(&game_shutdown_signal);
}

This is just a no-frills POSIX thread condition variable.

Finishing the Code Dump

This might be the largest code dump post in the project. All the revisions after this add on to this code, so they are smaller.

Most of this post is boilerplate C socket and mutex code, but I have to start with the foundation and work my way up.

You can get a copy of this code using subversion:

svn co -r2 http://waronpants.net/mudserver/trunk

Go back to part 0

Go forward to part 2