🪴 Daily gardening

Search

Search IconIcon to open search

(Build Your Own Redis with C/C++) 1. socket 통신, Protocol Parsing

Last updated Mar 5, 2023

Build Your Own Redis With C/C++ 을 보고 정리한 글입니다. 더 자세한 나용은 원문을 참고하실 수 있습니다. 직접 실습한 코드는 jiyeonseo/build-your-own-redis-with-c-cpp 에서 찾아볼 수 있습니다.

chap 1은 책에 대한 introduction으로 생략.

# Socket 동작 방식

# 서버 workflow 수도코드

1
2
3
4
5
6
7
fd = socket() 
bind(fd, address)
listen(fd)
while True:
    conn_fd = accept(fd)
    do_something_with(conn_fd)
    close(conn_fd)

# 클라이언트 workflow 수도코드

1
2
3
4
fd = socket()
connect(fd, address)
do_something_with(fd)
close(fd)

# Simple Server/Client

# Server

1
2
// 먼저 소켓 `fd` 생성
int fd = socket(AF_INET, SOCK_STREAM, 0);
1
2
3
// 소켓 옵션 세팅
int val = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// bind와 listen으로 0.0.0.0:1234 바인딩 하기 
    struct sockaddr_in addr = {};
    addr.sin_family = AF_INET;
    addr.sin_port = ntohs(1234);
    addr.sin_addr.s_addr = ntohl(0);    // wildcard address 0.0.0.0
    int rv = bind(fd, (const sockaddr *)&addr, sizeof(addr));
    if (rv) {
        die("bind()");
    }

    // listen
    rv = listen(fd, SOMAXCONN);
    if (rv) {
        die("listen()");
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 각각의 커넥션에 대해 do_something 하기
while (true) {
        // accept
        struct sockaddr_in client_addr = {};
        socklen_t socklen = sizeof(client_addr);
        int connfd = accept(fd, (struct sockaddr *)&client_addr, &socklen);
        if (connfd < 0) {
            continue;   // error
        }

        do_something(connfd);
        close(connfd);
    }

do_something 은 아래와 같이 read/write를 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static void do_something(int connfd) {
    char rbuf[64] = {};
    ssize_t n = read(connfd, rbuf, sizeof(rbuf) - 1);
    if (n < 0) {
        msg("read() error");
        return;
    }
    printf("client says: %s\n", rbuf); // 클라이언트로부터 온 메세지 출력

    char wbuf[] = "world";
    write(connfd, wbuf, strlen(wbuf)); // 응답으로 world를 보낸다 
}

# Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        die("socket()");
    }

    struct sockaddr_in addr = {};
    addr.sin_family = AF_INET;
    addr.sin_port = ntohs(1234);
    addr.sin_addr.s_addr = ntohl(INADDR_LOOPBACK);  // 127.0.0.1
    int rv = connect(fd, (const struct sockaddr *)&addr, sizeof(addr));
    if (rv) {
        die("connect");
    }

    char msg[] = "hello";
    write(fd, msg, strlen(msg)); // 연결되면 hello 라는 메세지를 보낸다. 

    char rbuf[64] = {};
    ssize_t n = read(fd, rbuf, sizeof(rbuf) - 1);
    if (n < 0) {
        die("read");
    }
    printf("server says: %s\n", rbuf);
    close(fd);

코드 : https://github.com/jiyeonseo/build-your-own-redis-with-c-cpp/tree/main/03

# Protocol Parsing

클라이언트와 주고 받을 프로토콜을 정의해보자. 먼저 요청 길이 선언부터.

# Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    while (true) {
        // accept
        struct sockaddr_in client_addr = {};
        socklen_t socklen = sizeof(client_addr);
        int connfd = accept(fd, (struct sockaddr *)&client_addr, &socklen);
        if (connfd < 0) {
            continue;   // error
        }

        // only serves one client connection at once
        while (true) {
            int32_t err = one_request(connfd);
            if (err) {
                break;
            }
        }
        close(connfd);
    }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 실제 request처리할 `one_request` 함수에서 사용할 헬퍼 함수 2개를 먼저 만들어 본다. 
static int32_t read_full(int fd, char *buf, size_t n) {
	// n byte가 될때까지 커널에서 읽기 
	// 데이터
    while (n > 0) {
        ssize_t rv = read(fd, buf, n); 
        if (rv <= 0) {
            return -1;  // error, or unexpected EOF
        }
        assert((size_t)rv <= n);
        n -= (size_t)rv;
        buf += rv;
    }
    return 0;
}

static int32_t write_all(int fd, const char *buf, size_t n) {
    // 버퍼가 가득차면 부분적으로만 데이터를 성공적으로 쓸 수 있기 때문에 
    // 우리가 필요한 것보다 적은 bytes를 반환하도록 해준다. 
    while (n > 0) {
        ssize_t rv = write(fd, buf, n);
        if (rv <= 0) {
            return -1;  // error
        }
        assert((size_t)rv <= n);
        n -= (size_t)rv;
        buf += rv;
    }
    return 0;
}

writeread 헬퍼 함수를 이용하여 다음과 같이 요청 처리 함수 one_request 를 작성할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const size_t k_max_msg = 4096;

static int32_t one_request(int connfd) {
    // 4 bytes header
    char rbuf[4 + k_max_msg + 1];
    errno = 0;
    int32_t err = read_full(connfd, rbuf, 4);
    if (err) {
        if (errno  0) {
            msg("EOF");
        } else {
            msg("read() error");
        }
        return err;
    }

    uint32_t len = 0;
    memcpy(&len, rbuf, 4);  // assume little endian
    if (len > k_max_msg) {
        msg("too long");
        return -1;
    }

    // request body
    err = read_full(connfd, &rbuf[4], len);
    if (err) {
        msg("read() error");
        return err;
    }

    // do something
    rbuf[4 + len] = '\0';
    printf("client says: %s\n", &rbuf[4]);

    // reply using the same protocol
    const char reply[] = "world";
    char wbuf[4 + sizeof(reply)];
    len = (uint32_t)strlen(reply);
    memcpy(wbuf, &len, 4);
    memcpy(&wbuf[4], reply, len);
    return write_all(connfd, wbuf, 4 + len);
}

# Client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static int32_t query(int fd, const char *text) {
    uint32_t len = (uint32_t)strlen(text);
    if (len > k_max_msg) {
        return -1;
    }

    char wbuf[4 + k_max_msg];
    memcpy(wbuf, &len, 4);  // assume little endian
    memcpy(&wbuf[4], text, len);
    if (int32_t err = write_all(fd, wbuf, 4 + len)) {
        return err;
    }

    // 4 bytes header
    char rbuf[4 + k_max_msg + 1];
    errno = 0;
    int32_t err = read_full(fd, rbuf, 4);
    if (err) {
        if (errno  0) {
            msg("EOF");
        } else {
            msg("read() error");
        }
        return err;
    }

    memcpy(&len, rbuf, 4);  // assume little endian
    if (len > k_max_msg) {
        msg("too long");
        return -1;
    }

    // reply body
    err = read_full(fd, &rbuf[4], len);
    if (err) {
        msg("read() error");
        return err;
    }

    // do something
    rbuf[4 + len] = '\0';
    printf("server says: %s\n", &rbuf[4]);
    return 0;
}

이 장에서는 아주 간단한 len 에 대한 프로토콜만 실습하여보았지만 실제 프로토콜은 훨씬 더 복잡하다. 또 바이너리 대신 텍스트를 사용하는데, 이는 사람이 읽기 쉬운 장점이 있지만, 파씽 부분에서 바이너리보다 더 신경써야 하는 부분이 많다. 또 프로토콜에 따라 구분 부호가 달라지거나 없을 수 있기 때문에 프로토콜 분석은 실습보다 더 어려울 수 있다.

코드 : https://github.com/jiyeonseo/build-your-own-redis-with-c-cpp/tree/main/04

build your own redis with c cpp 2 에 이어집니다.


# Source Code

# References