ℹ️ New Book: Build Your Own Database
07. Basic Server: get, set, del
With the event loop code from the last chapter, we can finally start adding commands to our server.
7.1 The Protocol
The “command” in our design is a list of strings, like
set key val
. We’ll encode the “command” with the following
scheme.
+------+-----+------+-----+------+-----+-----+------+
| nstr | len | str1 | len | str2 | ... | len | strn |
+------+-----+------+-----+------+-----+-----+------+
The nstr
is the number of strings and the
len
is the length of the following string. Both are 32-bit
integers.
The response is a 32-bit status code followed by the response string.
+-----+---------+
| res | data... |
+-----+---------+
7.2 Handle Requests
Starts with the try_one_request
function.
static bool try_one_request(Conn *conn) {
// try to parse a request from the buffer
if (conn->rbuf_size < 4) {
// not enough data in the buffer. Will retry in the next iteration
return false;
}
uint32_t len = 0;
(&len, &conn->rbuf[0], 4);
memcpyif (len > k_max_msg) {
("too long");
msg->state = STATE_END;
connreturn false;
}
if (4 + len > conn->rbuf_size) {
// not enough data in the buffer. Will retry in the next iteration
return false;
}
// got one request, generate the response.
uint32_t rescode = 0;
uint32_t wlen = 0;
int32_t err = do_request(
&conn->rbuf[4], len,
&rescode, &conn->wbuf[4 + 4], &wlen
);
if (err) {
->state = STATE_END;
connreturn false;
}
+= 4;
wlen (&conn->wbuf[0], &wlen, 4);
memcpy(&conn->wbuf[4], &rescode, 4);
memcpy->wbuf_size = 4 + wlen;
conn
// remove the request from the buffer.
// note: frequent memmove is inefficient.
// note: need better handling for production code.
size_t remain = conn->rbuf_size - 4 - len;
if (remain) {
(conn->rbuf, &conn->rbuf[4 + len], remain);
memmove}
->rbuf_size = remain;
conn
// change state
->state = STATE_RES;
conn(conn);
state_res
// continue the outer loop if the request was fully processed
return (conn->state == STATE_REQ);
}
The do_request
function handles the request. Only 3
commands (get, set, del) are recognized now.
static int32_t do_request(
const uint8_t *req, uint32_t reqlen,
uint32_t *rescode, uint8_t *res, uint32_t *reslen)
{
std::vector<std::string> cmd;
if (0 != parse_req(req, reqlen, cmd)) {
("bad req");
msgreturn -1;
}
if (cmd.size() == 2 && cmd_is(cmd[0], "get")) {
*rescode = do_get(cmd, res, reslen);
} else if (cmd.size() == 3 && cmd_is(cmd[0], "set")) {
*rescode = do_set(cmd, res, reslen);
} else if (cmd.size() == 2 && cmd_is(cmd[0], "del")) {
*rescode = do_del(cmd, res, reslen);
} else {
// cmd is not recognized
*rescode = RES_ERR;
const char *msg = "Unknown cmd";
((char *)res, msg);
strcpy*reslen = strlen(msg);
return 0;
}
return 0;
}
7.3 Parse Commands
The parsing of the command is straightforward:
static int32_t parse_req(
const uint8_t *data, size_t len, std::vector<std::string> &out)
{
if (len < 4) {
return -1;
}
uint32_t n = 0;
(&n, &data[0], 4);
memcpyif (n > k_max_args) {
return -1;
}
size_t pos = 4;
while (n--) {
if (pos + 4 > len) {
return -1;
}
uint32_t sz = 0;
(&sz, &data[pos], 4);
memcpyif (pos + 4 + sz > len) {
return -1;
}
.push_back(std::string((char *)&data[pos + 4], sz));
out+= 4 + sz;
pos }
if (pos != len) {
return -1; // trailing garbage
}
return 0;
}
7.4 Respond Commands
The “implementation” of 3 commands:
enum {
= 0,
RES_OK = 1,
RES_ERR = 2,
RES_NX };
// The data structure for the key space. This is just a placeholder
// until we implement a hashtable in the next chapter.
static std::map<std::string, std::string> g_map;
static uint32_t do_get(
const std::vector<std::string> &cmd, uint8_t *res, uint32_t *reslen)
{
if (!g_map.count(cmd[1])) {
return RES_NX;
}
std::string &val = g_map[cmd[1]];
assert(val.size() <= k_max_msg);
(res, val.data(), val.size());
memcpy*reslen = (uint32_t)val.size();
return RES_OK;
}
static uint32_t do_set(
const std::vector<std::string> &cmd, uint8_t *res, uint32_t *reslen)
{
(void)res;
(void)reslen;
g_map[cmd[1]] = cmd[2];
return RES_OK;
}
static uint32_t do_del(
const std::vector<std::string> &cmd, uint8_t *res, uint32_t *reslen)
{
(void)res;
(void)reslen;
g_map.erase(cmd[1]);
return RES_OK;
}
7.5 The Client & Testing
Now it’s time to test with our client:
static int32_t send_req(int fd, const std::vector<std::string> &cmd) {
uint32_t len = 4;
for (const std::string &s : cmd) {
+= 4 + s.size();
len }
if (len > k_max_msg) {
return -1;
}
char wbuf[4 + k_max_msg];
(&wbuf[0], &len, 4); // assume little endian
memcpyuint32_t n = cmd.size();
(&wbuf[4], &n, 4);
memcpysize_t cur = 8;
for (const std::string &s : cmd) {
uint32_t p = (uint32_t)s.size();
(&wbuf[cur], &p, 4);
memcpy(&wbuf[cur + 4], s.data(), s.size());
memcpy+= 4 + s.size();
cur }
return write_all(fd, wbuf, 4 + len);
}
static int32_t read_res(int fd) {
// code omitted...
// print the result
uint32_t rescode = 0;
if (len < 4) {
("bad response");
msgreturn -1;
}
(&rescode, &rbuf[4], 4);
memcpy("server says: [%u] %.*s\n", rescode, len - 4, &rbuf[8]);
printfreturn 0;
}
int main(int argc, char **argv) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
("socket()");
die}
// code omitted...
std::vector<std::string> cmd;
for (int i = 1; i < argc; ++i) {
.push_back(argv[i]);
cmd}
int32_t err = send_req(fd, cmd);
if (err) {
goto L_DONE;
}
= read_res(fd);
err if (err) {
goto L_DONE;
}
:
L_DONE(fd);
closereturn 0;
}
Testing commands:
$ ./client get k
server says: [2]
$ ./client set k v
server says: [0]
$ ./client get k
server says: [0] v
$ ./client del k
server says: [0]
$ ./client get k
server says: [2]
$ ./client aaa bbb server says: [1] Unknown cmd
Source code:
See also:
codecrafters.io offers “Build Your Own X” courses in many programming languages.
Including Redis, Git, SQLite, Docker, and more.
Check it out
codecrafters.io offers “Build Your Own X” courses in many programming languages.
Including Redis, Git, SQLite, Docker, and more.