JSON-RPC 심플 구현

C++을 사용하여 최대한 간단하게 자체 JSON-RPC를 심플하게 구현해 보았다.

머릿말

RPC(Remote Procedure Call)는 두 프로세스 간의 함수 호출을 뜻하는데, 소켓 통신과 같은 IPC(Inter-Process Communication)를 이용하여 구현할 수 있다.
예전에 JSON-RPC 정리 글에서 JSON-RPC를 소개하고, C/C++로 오픈소스 패키지를 이용한 JSON-RPC 사용법을 간단히 다루었다.
이번 글에서는 단순한 파라미터로 함수 몇 개만 호출하는 경우에 적합한 용도로, JSON-RPC 외부 패키지를 사용하지 않고, JSON 라이브러리만 사용하여 간단하게 JSON-RPC를 직접 구현해 보았다.

여기서는 AF_INET 소켓을 이용하여 JSON RPC를 구현하였다. AF_INET 소켓은 포트를 이용하므로, 시스템이 사용 중이지 않은 포트를 사용해야 한다. 만약에 AF_UNIX 소켓을 사용하려는 경우에는 포트 대신에 소켓 파일을 사용해야 하는데, 이 방법은 AF_INET 소켓을 이용하는 예제 이후에, 필요한 변경 사항만 간략히 언급하겠다.

Makefile 파일

아래와 같이 Makefile 파일을 작성한다. 즉, make 명령을 실행하면 예제 RPC client, server 프로그램이 빌드된다.

CXX = g++
CXXFLAGS = -Wall
LDFLAGS = -ljsoncpp

TARGET_CLIENT = client
TARGET_SERVER = server

CLIENT_SRCS = Client.cpp JsonRpc.cpp
CLIENT_OBJS = $(CLIENT_SRCS:%.c=%.o)

SERVER_SRCS = Server.cpp JsonRpc.cpp
SERVER_OBJS = $(SERVER_SRCS:%.c=%.o)

all: $(TARGET_CLIENT) $(TARGET_SERVER)

$(TARGET_CLIENT): $(CLIENT_OBJS)
	$(CXX) -o $@ $^ $(LDFLAGS)

$(TARGET_SERVER): $(SERVER_OBJS)
	$(CXX) -o $@ $^ $(LDFLAGS)

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

clean:
	@rm -rf *.o $(TARGET_CLIENT) $(TARGET_SERVER)

RPC server 헤더 파일

RPC server와 client가 공통으로 사용하는 JsonRpc.h 헤더 파일을 아래와 같이 작성한다. (예로 12345 포트 번호 사용)

#pragma once

#include <json/json.h>

#define PORT    12345

bool SendJsonRpc(int fd, const Json::Value &root);
bool RecvJsonRpc(int fd, Json::Value &outRoot);

JSON-RPC 구현

RPC server와 client가 공통으로 사용하는 JsonRpc.cpp 파일을 아래와 같이 작성한다.
JSON-RPC 수신은 입력 소켓 파일 디스크립터를 read 해서 JSON 데이터로 파싱하고, JSON-RPC 송신은 입력 JSON 데이터를 입력 소켓 파일 디스크립터로 write 한다.

#include <arpa/inet.h>
#include <unistd.h>

#include <json/json.h>

bool RecvJsonRpc(int fd, Json::Value &outRoot) {
    Json::Reader reader;
    uint32_t payloadLen = 0;

    if (read(fd, &payloadLen, sizeof(payloadLen)) != sizeof(payloadLen)) {
        return false;
    }

    uint32_t len = ntohl(payloadLen);
    std::string payload(len, '\0');
    if (read(fd, &payload[0], len) != (size_t)len) {
        return false;
    }
    if (!reader.parse(payload, outRoot, false)) {
        return false;
    }
    return true;
}

bool SendJsonRpc(int fd, const Json::Value &root) {
    Json::FastWriter w;
    std::string payload = w.write(root);
    uint32_t len = htonl(static_cast<uint32_t>(payload.size()));

    if (len == 0) {
        return false;
    }
    if (write(fd, &len, sizeof(len)) != sizeof(len)) {
        return false;
    }
    if (write(fd, payload.data(), payload.size()) != payload.size()) {
        return false;
    }
    return true;
}

RPC server

RPC server 프로그램으로 Server.cpp 파일을 아래와 같이 작성한다.
이 server 프로그램은 AF_INET 소켓을 생성한 후에 소켓으로 들어온 입력 JSON 데이터를 파싱하여 해당 메쏘드를 처리하고, 그 처리 결과를 JSON 데이터로 만들어서 다시 client로 보낸다. 아래에서는 메쏘드 예로 “add”와 “multiply”를 구현하였다.

#include <arpa/inet.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>

#include "JsonRpc.h"

static Json::Value ProcessMethod(const Json::Value &req) {
    Json::Value resp(Json::objectValue);

    if (!req.isMember("method") || !req["method"].isString()) {
        resp["ok"] = false;
        resp["error"] = "missing_method";
        return resp;
    }
    const std::string method = req["method"].asString();
    const Json::Value params = req.get("params", Json::Value(Json::objectValue));

    if (method == "add") {
        int arg1 = params.get("arg1", 0).asInt();
        int arg2 = params.get("arg2", 0).asInt();
        printf("server: method=%s, arg1=%d, arg2=%d\n", method.c_str(), arg1, arg2);
        resp["result"] = arg1 + arg2;
        resp["ok"] = true;
        return resp;
    }

    if (method == "multiply") {
        int arg1 = params.get("arg1", 0).asInt();
        int arg2 = params.get("arg2", 0).asInt();
        printf("server: method=%s, arg1=%d, arg2=%d\n", method.c_str(), arg1, arg2);
        resp["result"] = arg1 * arg2;
        resp["ok"] = true;
        return resp;
    }

    resp["ok"] = false;
    resp["error"] = "unknown_method";
    return resp;
}

int JsonRpcServer() {
    struct sockaddr_in addr;

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        printf("%s(): Failed to create socket\n", __func__);
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("%s(): Failed to bind\n", __func__);
        close(server_fd);
        return -1;
    }

    if (listen(server_fd, 1) < 0) {
        printf("%s(): Failed to listen\n", __func__);
        close(server_fd);
        return -1;
    }

    int client_fd = accept(server_fd, nullptr, nullptr);
    if (client_fd < 0) {
        printf("%s(): Failed to accept\n", __func__);
        close(server_fd);
        return -1;
    }

    for (;;) {
        Json::Value req, resp;
        if (!RecvJsonRpc(client_fd, req)) {
            break;
        }
        resp = ProcessMethod(req);
        if (!SendJsonRpc(client_fd, resp)) {
            printf("%s(): Failed to send JSON RPC\n", __func__);
            break;
        }
    }

    close(client_fd);
    close(server_fd);
    return 0;
}

int main() {
    JsonRpcServer();

    return 0;
}

RPC client

RPC client 프로그램으로 Client.cpp 파일을 아래와 같이 작성한다.
이 client 프로그램은 AF_INET 소켓을 사용하여 RPC server에 연결한 후에, “add”와 “multiply” 메쏘드를 파라미터와 함께 JSON RPC로 보내고, server로부터 응답을 받아서 결과를 출력한다.

#include <arpa/inet.h>
#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>

#include "JsonRpc.h"

int ConnectJsonRpcServer(void) {
    struct sockaddr_in addr;
    int sock_fd;

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        printf("%s(): Failed to create socket\n", __func__);
        return -1;
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("%s(): Failed to connect\n", __func__);
        close(sock_fd);
        return -1;
    }

    return sock_fd;
}

bool CallJsonRpc(int fd, Json::Value req, Json::Value &resp) {
    if (!req.isMember("method") || !req["method"].isString()) {
        resp["error"] = "missing_method";
        return false;
    }
    const std::string strMethod = req["method"].asString();

    if (!SendJsonRpc(fd, req)) {
        printf("%s(): Failed to send json frame\n", __func__);
        return false;
    }
    if (!RecvJsonRpc(fd, resp)) {
        printf("%s(): Failed to receive json frame\n", __func__);
        return false;
    }
    return resp.isMember("ok") && resp["ok"].asBool();
}

bool JsonRpc_add(int fd, int arg1, int arg2, int &result) {
    Json::Value req, resp;

    req["method"] = "add";
    req["params"]["arg1"] = arg1;
    req["params"]["arg2"] = arg2;
    if (CallJsonRpc(fd, req, resp) == false) {
        printf("%s(): Failed, error=%s\n", __func__, resp["error"].asString().c_str());
        return false;
    }
    result = resp["result"].asInt();
    return true;
}

bool JsonRpc_multiply(int fd, int arg1, int arg2, int &result) {
    Json::Value req, resp;

    req["method"] = "multiply";
    req["params"]["arg1"] = arg1;
    req["params"]["arg2"] = arg2;
    if (CallJsonRpc(fd, req, resp) == false) {
        printf("%s(): Failed, error=%s\n", __func__, resp["error"].asString().c_str());
        return false;
    }
    result = resp["result"].asInt();
    return true;
}

int main(void) {
    int result;
    int fd = ConnectJsonRpcServer();
    if (fd == -1) {
        printf("Failed to connect JSON-RPC server\n");
        return 1;
    }

    if (JsonRpc_add(fd, 2, 3, result)) {
        printf("client: add(2, 3) = %d\n", result);
    } else {
        printf("Failed to call JSON-RPC add\n");
    }

    if (JsonRpc_multiply(fd, 4, 5, result)) {
        printf("client: multiply(4, 5) = %d\n", result);
    } else {
        printf("Failed to call JSON-RPC multiply\n");
    }

    close(fd);
    return 0;
}

테스트 실행

먼저 server 프로그램을 실행시키고, 터미널을 1개 더 열어서 client를 실행시킨다.
결과로 아래와 같이 정상적인 결과가 출력된다.

  • server 로그
    server: method=add, arg1=2, arg2=3
    server: method=multiply, arg1=4, arg2=5
    
  • client 로그
    client: add(2, 3) = 5
    client: multiply(4, 5) = 20
    

AF_UNIX 소켓 사용하기

만약에 AF_INET 소켓 대신에 AF_UNIX 소켓을 사용하고자 하는 경우에는 다음과 같은 내용들만 변경하면 된다.

JsonRpc.h 파일에서 PORT 대신에 아래 예와 같이 소켓 파일의 경로를 지정한다.

#define SOCKET_PATH "/tmp/ipc.sock"


Server.cpp 파일에서 <arpa/inet.h> 헤더는 제거하고, 아래 헤더 파일을 include 한다.

#include <sys/un.h>

또, JsonRpcServer() 함수는 아래와 같이 변경한다.

int JsonRpcServer() {
    struct sockaddr_un addr;

    int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd < 0) {
        printf("%s(): Failed to create socket\n", __func__);
        return -1;
    }

    if (access(SOCKET_PATH, F_OK) == 0) {
        unlink(SOCKET_PATH);
    }

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("%s(): Failed to bind\n", __func__);
        close(server_fd);
        return -1;
    }
    chmod(SOCKET_PATH, 0660);

    if (listen(server_fd, 1) < 0) {
        printf("%s(): Failed to listen\n", __func__);
        close(server_fd);
        return -1;
    }

    int client_fd = accept(server_fd, nullptr, nullptr);
    if (client_fd < 0) {
        printf("%s(): Failed to accept\n", __func__);
        close(server_fd);
        return -1;
    }

    for (;;) {
        Json::Value req, resp;
        if (!RecvJsonRpc(client_fd, req)) {
            break;
        }
        resp = ProcessMethod(req);
        if (!SendJsonRpc(client_fd, resp)) {
            printf("%s(): Failed to send JSON RPC\n", __func__);
            break;
        }
    }

    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH);
    return 0;
}


Client.cpp 파일에서도 마찬가지로 <arpa/inet.h> 헤더는 제거하고, 아래 헤더 파일을 include 한다.

#include <sys/un.h>

또, ConnectJsonRpcServer() 함수는 아래와 같이 변경한다.

int ConnectJsonRpcServer(void) {
    struct sockaddr_un addr;
    int sock_fd;

    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        printf("%s(): Failed to create socket\n", __func__);
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1);
    if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        printf("%s(): Failed to connect\n", __func__);
        close(sock_fd);
        return -1;
    }

    return sock_fd;
}


빌드해서 실행해보면 이전의 AF_INET 소켓을 사용했던 결과와 동일한 결과를 얻을 수 있다.
참고로 server에서 생성한 소켓 파일의 속성을 보면, 아래와 같이 socket으로 나옴을 확인할 수 있다.

$ file /tmp/ipc.sock
/tmp/ipc.sock: socket

맺음말

위와 같이 기본 형태만 구현하여, 두 프로세스 간의 함수 호출을 구현해 보았다.
물론 추가로 서버에서의 RPC 함수 호출과 클라이언트에서의 함수 부분을 구조화할 수 있겠으나, 여기서는 의도적으로 가장 간단한 형태로 구현하였고, 추후 참조용으로 남긴다.

카테고리:

업데이트: