README.md

January 29, 2023 ยท View on GitHub

REDCON

Fast Redis compatible server framework for C

Redcon is a custom Redis server framework that is fast and simple to use. This is a C version of the original Redcon, and is built on top of evio.c.

Features

  • Create a Fast custom Redis compatible server in C
  • Simple interface
  • Multithread support
  • Super lightweight
  • Support for pipelining and telnet commands
  • Works with Redis clients such as redigo, redis-py, node_redis, and jedis

This library is also avaliable for Go and Rust.

Install

Clone this respository and then use pkg.sh to import dependencies.

$ git clone https://github.com/tidwall/redcon.c
$ cd redcon.c/
$ pkg.sh import

Example

Here's a simple Redis clone. Save the following file to clone.c

Check out example/clone.c for a more robust example.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "redcon.h"
#include "hashmap.h"

void *zmalloc(size_t size);

// keyspace is a collection of all keys. (github.com/tidwall/hashmap.c)
struct hashmap *keyspace; 

void cmdSET(struct redcon_conn *conn, struct redcon_args *args, void *udata) {
    if (redcon_args_count(args) != 3) {
        redcon_conn_write_error(conn, "ERR wrong number of arguments");
    } else {
        // Each key/value item is a single allocation of two contiguous 
        // series of bytes, the first is a c-string and the second is binary.
        int vallen;
        const char *key = redcon_args_at(args, 1, NULL);
        const char *val = redcon_args_at(args, 2, &vallen);
        char *item = zmalloc(strlen(key)+5+vallen);
        strcpy(item, key); 
        *(int32_t*)(item+strlen(item)+1) = vallen;
        memcpy(item+strlen(item)+5, val, vallen);
        char **prev = hashmap_set(keyspace, &item);
        if (prev) free(*prev);
        redcon_conn_write_string(conn, "OK");
    }
}

void cmdGET(struct redcon_conn *conn, struct redcon_args *args, void *udata) {
    if (redcon_args_count(args) != 2) {
        redcon_conn_write_error(conn, "ERR wrong number of arguments");
    } else {
        const char *key = redcon_args_at(args, 1, NULL);
        char **item = hashmap_get(keyspace, &key);
        if (!item) {
            redcon_conn_write_null(conn);
        } else {
            char *val = *item+strlen(*item)+5;
            int vallen = *(int32_t*)(*item+strlen(*item)+1);
            redcon_conn_write_bulk(conn, val, vallen);
        }
    }
}

void cmdDEL(struct redcon_conn *conn, struct redcon_args *args, void *udata) {
    if (redcon_args_count(args) != 2) {
        redcon_conn_write_error(conn, "ERR wrong number of arguments");
    } else {
        const char *key = redcon_args_at(args, 1, NULL);
        char **prev = hashmap_delete(keyspace, &key);
        if (!prev) {
            redcon_conn_write_int(conn, 0);
        } else {
            free(*prev);
            redcon_conn_write_int(conn, 1);
        }
    }
}

void cmdPING(struct redcon_conn *conn, struct redcon_args *args, void *udata) {
    redcon_conn_write_string(conn, "PONG");
}

void command(struct redcon_conn *conn, struct redcon_args *args, void *udata) {
         if (redcon_args_eq(args, 0, "set"))  cmdSET(conn, args, udata);
    else if (redcon_args_eq(args, 0, "get"))  cmdGET(conn, args, udata);
    else if (redcon_args_eq(args, 0, "del"))  cmdDEL(conn, args, udata);
    else if (redcon_args_eq(args, 0, "ping")) cmdPING(conn, args, udata);
    else redcon_conn_write_error(conn, "ERR unknown command");
}

void serving(const char **addrs, int naddrs, void *udata) {
    printf("* Listening at %s\n", addrs[0]);
}

void error(const char *msg, bool fatal, void *udata) {
    fprintf(stderr, "- %s\n", msg);
}

uint64_t hash(const void *item, uint64_t seed0, uint64_t seed1) {
    return hashmap_murmur(*(char**)item, strlen(*(char**)item), seed0, seed1);
}

int compare(const void *a, const void *b, void *udata) {
    return strcmp(*(char**)a, *(char**)b);
}

void *zmalloc(size_t size) {
    void *ptr = malloc(size);
    if (!ptr) abort();
    return ptr;
}

int main() {
    // use do-or-die allocator
    redcon_set_allocator(zmalloc, free);
    hashmap_set_allocator(zmalloc, free);

    keyspace = hashmap_new(sizeof(char *), 0, 0, 0, hash, compare, NULL);
    struct redcon_events evs = {
        .serving = serving,
        .command = command,
        .error = error,
    };
    const char *addrs[] = { 
        "tcp://0.0.0.0:6380",
    };
    redcon_main(addrs, 1, evs, NULL);
}

Then build and run the server.

$ cc *.c && ./a.out

And connect using the Redis cli.

$ redis-cli -p 6380

Multithreading

Use the redcon_main_mt function to start the server using multiple threads.

// Use five threads
redcon_main_mt(addrs, 1, evs, NULL, 5);

// Use ten threads
redcon_main_mt(addrs, 1, evs, NULL, 10);

// Use the number of threads equal to the number of cores on the machine.
redcon_main_mt(addrs, 1, evs, NULL, 0);

Benchmarks

For following results were generated using the redis-benchmark tool that is provided by the official Redis distribution.

CmdPipeline 1Pipeline 8Pipeline 16Pipeline 32Pipeline 64Pipeline 128
Redis 6.06SET114,467618,432765,284872,984947,724990,307
Redis 6.06GET114,888692,764898,7241,048,9871,151,2771,208,751
Redis cloneSET114,554895,5021,310,7871,670,9021,954,2952,122,295
Redis cloneGET114,602894,8621,346,2761,735,8092,046,2452,230,151

In my above benchmark test the Redis clone used about 7% less memory than Redis 6.06 -- 79.7 MB to 84.8 MB.

The machine was an AWS c5.2xlarge instance (Intel Xeon 3.6 GHz).

The benchmark command looked like:

redis-benchmark -q -t set,get -r 1000000 -n 10000000 -p 6379  -P 16   # redis 6.06
redis-benchmark -q -t set,get -r 1000000 -n 10000000 -p 6380  -P 16   # redis clone