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: