C++20 기반 고성능 HTTP 라이브러리: Cinatra

소개

Cinatra는 C++20 스택리스 코루틴을 기반으로 한 크로스플랫폼, 헤더 온리, 고성능이면서 사용하기 쉬운 HTTP/HTTPS 라이브러리(HTTP 1.1)입니다. HTTP 서버와 클라이언트 모두를 포함하며, 기본적인 GET/POST 요청부터 RESTful API, WebSocket, 청크 전송, 범위 요청, 멀티파트, 정적 파일 서비스, 리버스 프록시 등 다양한 기능을 지원합니다.

기본 HTTP 요청

HTTP 서버 시작

#include <include/cinatra.hpp>
using namespace cinatra;

void start_http_server() {
  coro_http_server server(/* 스레드 수 */ std::thread::hardware_concurrency(), 9001);
  server.set_http_handler<GET>(
      "/", [](coro_http_request &req, coro_http_response &resp) {
        resp.set_status_and_content(status_type::ok, "성공"); // IO 스레드에서 응답
      });
  server.set_http_handler<GET, POST>(
      "/thread_pool",
      [](coro_http_request &req,
         coro_http_response &resp) -> async_simple::coro::Lazy<void> {
        // 스레드 풀에서 처리
        co_await coro_io::post([&]() {
          resp.set_status_and_content(status_type::ok, "스레드 풬에서 성공");
        });
      });
   server.sync_start();
}

몇 줄의 코드로 HTTP 서버를 생성할 수 있습니다. 먼저 HTTP 서버의 작업 스레드 수와 포트를 설정합니다. 그 다음 HTTP 서비스의 URL, HTTP 메서드 및 해당 처리 함수를 설정합니다. HTTP 요청은 IO 스레드 또는 스레드 풀에서 처리할 수 있습니다.

클라이언트 요청

async_simple::coro::Lazy<void> send_request() {
  coro_http_client client{};
  auto result = co_await client.async_get("http://127.0.0.1:9001/");
  assert(result.status == 200);
  assert(result.resp_body == "성공");
  for (auto [key, val] : result.resp_headers) {
    std::cout << key << ": " << val << "\n";
  }
  result = co_await client.async_get("/thread_pool");
  assert(result.status == 200);
}

HTTP 클라이언트는 비동기적으로 서버에 요청을 보내며, 반환 결과에는 서버 응답 상태 코드, HTTP 콘텐츠 및 HTTP 헤더가 포함됩니다. 네트워크 오류가 발생한 경우 result.net_err에서 오류 코드와 메시지를 가져올 수 있습니다.

RESTful API

coro_http_server server(/* 스레드 수 */ std::thread::hardware_concurrency(), 9001);
server.set_http_handler<cinatra::GET, cinatra::POST>(
    "/test/{}/detail/{}",
    [](coro_http_request &req,
       coro_http_response &resp) -> async_simple::coro::Lazy<void> {
      co_await coro_io::post([&]() {
        CHECK(req.matches_.str(1) == "user");
        CHECK(req.matches_.str(2) == "info");
        resp.set_status_and_content(cinatra::status_type::ok, "안녕하세요");
      });
      co_return;
    });
server.set_http_handler<cinatra::GET, cinatra::POST>(
    R"(/data/(\d+)/item/(\d+))",
    [](coro_http_request &req, coro_http_response &response) {
      CHECK(req.matches_.str(1) == "100");
      CHECK(req.matches_.str(2) == "50");
      response.set_status_and_content(status_type::ok, "숫자 정규식 성공");
    });
server.set_http_handler<cinatra::GET, cinatra::POST>(
    "/member/:id", [](coro_http_request &req, coro_http_response &response) {
      CHECK(req.params_["id"] == "member123");
      response.set_status_and_content(status_type::ok, "성공");
    });
server.set_http_handler<cinatra::GET, cinatra::POST>(
    "/member/:id/subscription",
    [](coro_http_request &req, coro_http_response &response) {
      CHECK(req.params_["id"] == "sub456");
      response.set_status_and_content(status_type::ok, "성공");
    });
server.async_start();
coro_http_client client;
client.get("http://127.0.0.1:9001/test/user/detail/info");
client.get("http://127.0.0.1:9001/data/100/item/50");
client.get("http://127.0.0.1:9001/member/member123");
client.get("http://127.0.0.1:9001/member/sub456/subscription");

HTTPS 접속

#ifdef CINATRA_ENABLE_SSL
  coro_http_client client{};
  result = co_await client.async_get("https://www.example.com");
  assert(result.status == 200);
#endif

HTTPS 웹사이트에 접속할 때는 OpenSSL이 설치되어 있고 ENABLE_SSL이 활성화되어 있는지 확인하세요.

WebSocket

cinatra::coro_http_server server(1, 9001);
server.set_http_handler<cinatra::GET>(
    "/ws_echo",
    [](cinatra::coro_http_request &req,
       cinatra::coro_http_response &resp) -> async_simple::coro::Lazy<void> {
      cinatra::websocket_result result{};
      while (true) {
        result = co_await req.get_conn()->read_websocket();
        if (result.ec) {
          break;
        }
        if (result.type == cinatra::ws_frame_type::WS_CLOSE_FRAME) {
          REQUIRE(result.data == "테스트 종료");
          break;
        }
        auto ec = co_await req.get_conn()->write_websocket(result.data);
        if (ec) {
          break;
        }
      }
    });
server.sync_start();

코루틴 처리 함수에서 while 루프를 통해 WebSocket 데이터를 비동기적으로 읽고 쓸 수 있습니다.

클라이언트 측

cinatra::coro_http_client client{};
std::string message(100, 'x');
client.on_ws_close([](std::string_view reason) {
    std::cout << "WebSocket 닫힘: " << reason << std::endl;
});
client.on_ws_msg([message](cinatra::resp_data data) {
  if (data.net_err) {
    std::cout << "WebSocket 메시지 네트워크 오류: " << data.net_err.message() << "\n";
    return;
  }
  std::cout << "WebSocket 메시지 길이: " << data.resp_body.size() << std::endl;
  REQUIRE(data.resp_body == message);
});
co_await client.async_ws_connect("ws://127.0.0.1:9001/ws_echo");
co_await client.async_send_ws(message);
co_await client.async_send_ws_close("테스트 종료");

클라이언트는 응답 콜백과 close 콜백을 설정하여 수신한 WebSocket 메시지와 WebSocket 종료 메시지를 각각 처리합니다.

정적 파일 서비스

std::string filename = "temp.txt";
create_file(filename, 64);
coro_http_server server(1, 9001);
std::string virtual_path = "download";
std::string files_root_path = "";  // 현재 경로
server.set_static_res_dir(
    virtual_path,
    files_root_path); // 서버 시작 전에 설정, 새 파일 추가 시 서버 재시작 필요
server.async_start();
coro_http_client client{};
auto result =
    co_await client.async_get("http://127.0.0.1:9001/download/temp.txt");
assert(result.status == 200);
assert(result.resp_body.size() == 64);

서버는 가상 경로와 실제 파일 경로를 설정하며, 다운로드 시 가상 경로와 실제 경로의 파일명을 입력하여 다운로드를 구현합니다.

리버스 프록시

3개의 서버를 프록시해야 한다고 가정해 보겠습니다. 프록시 서버는 로드 밸런싱 알고리즘을 사용하여 그 중 하나를 선택하여 접속하고 결과를 클라이언트에게 반환합니다.

3개의 프록시 대상 서버 시작

cinatra::coro_http_server server_one(1, 9001);
server_one.set_http_handler<cinatra::GET, cinatra::POST>(
    "/",
    [](coro_http_request &req,
       coro_http_response &response) -> async_simple::coro::Lazy<void> {
      co_await coro_io::post([&]() {
        response.set_status_and_content(status_type::ok, "서버1");
      });
    });
server_one.async_start();

cinatra::coro_http_server server_two(1, 9002);
server_two.set_http_handler<cinatra::GET, cinatra::POST>(
    "/",
    [](coro_http_request &req,
       coro_http_response &response) -> async_simple::coro::Lazy<void> {
      co_await coro_io::post([&]() {
        response.set_status_and_content(status_type::ok, "서버2");
      });
    });
server_two.async_start();

cinatra::coro_http_server server_three(1, 9003);
server_three.set_http_handler<cinatra::GET, cinatra::POST>(
    "/", [](coro_http_request &req, coro_http_response &response) {
      response.set_status_and_content(status_type::ok, "서버3");
    });
server_three.async_start();

프록시 서버 시작

라운드 로빈 전략의 프록시 서버 설정:

coro_http_server proxy_rr(2, 8091);
proxy_rr.set_http_proxy_handler<GET, POST>(
    "/rr", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"},
    coro_io::load_blance_algorithm::RR);
proxy_rr.sync_start();

랜덤 전략의 프록시 서버 설정:

coro_http_server proxy_random(2, 8092);
proxy_random.set_http_proxy_handler<GET, POST>(
    "/random", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"});
proxy_random.sync_start();

가중치 라운드 로빈 전략의 프록시 서버 설정:

coro_http_server proxy_wrr(2, 8090);
proxy_wrr.set_http_proxy_handler<GET, POST>(
    "/wrr", {"127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"},
    coro_io::load_blance_algorithm::WRR, {10, 5, 5});
proxy_wrr.sync_start();  

클라이언트가 프록시 서버에 요청

coro_http_client client_rr;
resp_data resp_rr = client_rr.get("http://127.0.0.1:8091/rr");
assert(resp_rr.resp_body == "서버1");
resp_rr = client_rr.get("http://127.0.0.1:8091/rr");
assert(resp_rr.resp_body == "서버2");
resp_rr = client_rr.get("http://127.0.0.1:8091/rr");
assert(resp_rr.resp_body == "서버3");

coro_http_client client_wrr;
resp_data resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버1");
resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버1");
resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버2");
resp = client_wrr.get("http://127.0.0.1:8090/wrr");
assert(resp.resp_body == "서버3");  

관심사 분리 (Aspect)

임의의 관심사 생성

struct 로그_처리 {
  bool before(coro_http_request &, coro_http_response &) {
    std::cout << "처리 전 로그" << std::endl;
    return true;
  }
  bool after(coro_http_request &, coro_http_response &res) {
    std::cout << "처리 후 로그" << std::endl;
    res.add_header("추가헤더", "값");
    return true;
  }
};

struct 데이터_가져오기 {
  bool before(coro_http_request &req, coro_http_response &res) {
    req.set_aspect_data("안녕하세요");
    return true;
  }
};

관심사는 before 또는 after 함수를 구현하는 클래스입니다.

관심사 적용

async_simple::coro::Lazy<void> apply_aspects() {
  coro_http_server server(1, 9001);
  server.set_http_handler<GET>(
      "/test",
      [](coro_http_request &req, coro_http_response &resp) {
        auto val = req.get_aspect_data();
        assert(val[0] == "안녕하세요");
        resp.set_status_and_content(status_type::ok, "성공");
      },
      로그_처리{}, 데이터_가져오기{}); // 두 개의 관심사 설정
  server.async_start();
  coro_http_client client{};
  auto result = co_await client.async_get("http://127.0.0.1:9001/test");
  assert(result.status == 200);
}

HTTP 핸들러를 등록할 때 두 개의 관심사를 설정했습니다. 해당 URL의 처리는 먼저 관심사로 들어가며, 관심사가 true를 반환해야만 비즈니스 로직 계속 실행됩니다. false를 반환하면 후속 로직이 실행되지 않으며, false를 반환할 때는 관심사에서 resp.set_status_and_content를 호출하여 상태 코드와 반환 콘텐츠를 설정해야 합니다.

청크 전송, 범위 요청, 멀티파트

청크 전송 업로드/다운로드

청크 전송 프로토콜은 대용량 파일 업로드 및 다운로드에 적합합니다:

async_simple::coro::Lazy<void> chunked_upload_download() {
  coro_http_server server(1, 9001);
  server.set_http_handler<GET, POST>(
      "/chunked",
      [](coro_http_request &req,
         coro_http_response &resp) -> async_simple::coro::Lazy<void> {
        assert(req.get_content_type() == content_type::chunked);
        chunked_result result{};
        std::string content;
        while (true) {
          result = co_await req.get_conn()->read_chunked();
          if (result.ec) {
            co_return;
          }
          if (result.eof) {
            break;
          }
          content.append(result.data);
        }
        std::cout << "콘텐츠 크기: " << content.size() << "\n";
        std::cout << content << "\n";
        resp.set_format_type(format_type::chunked);
        resp.set_status_and_content(status_type::ok, "청크 전송 성공");
      });
  server.set_http_handler<GET, POST>(
      "/write_chunked",
      [](coro_http_request &req,
         coro_http_response &resp) -> async_simple::coro::Lazy<void> {
        resp.set_format_type(format_type::chunked);
        bool ok;
        if (ok = co_await resp.get_conn()->begin_chunked(); !ok) {
          co_return;
        }
        std::vector<std::string> vec{"안녕", "하세요", "성공"};
        for (auto &str : vec) {
          if (ok = co_await resp.get_conn()->write_chunked(str); !ok) {
            co_return;
          }
        }
        ok = co_await resp.get_conn()->end_chunked();
      });
  server.sync_start();
  result = co_await client.async_get("http://127.0.0.1:9001/write_chunked");
  assert(result.status == 200);
  assert(result.resp_body == "안녕하세요성공");
}

클라이언트 청크 업로드

coro_http_client client{};
std::string filename = "테스트.txt";
create_file(filename, 1010);
coro_io::coro_file file{};
co_await file.async_open(filename, coro_io::flags::read_only);
std::string buf;
detail::resize(buf, 100);
auto fn = [&file, &buf]() -> async_simple::coro::Lazy<read_result> {
  auto [ec, size] = co_await file.async_read(buf.data(), buf.size());
  co_return read_result{buf, file.eof(), ec};
};
auto result = co_await client.async_upload_chunked(
    "http://127.0.0.1:9001/chunked"sv, http_method::POST, std::move(fn));

클라이언트는 파일 읽기부터 청크 업로드까지 전 과정이 비동기식입니다.

클라이언트 청크 다운로드

메모리로 다운로드:

auto result = co_await client.async_get("http://127.0.0.1:9001/write_chunked");
assert(result.status == 200);
assert(result.resp_body == "안녕하세요성공");

파일로 다운로드:

auto result = co_await client.async_download(
      "http://127.0.0.1:9001/write_chunked", "다운로드.txt");
CHECK(std::filesystem::file_size("다운로드.txt")==1010);

범위 다운로드

async_simple::coro::Lazy<void> byte_ranges_download() {
  create_file("범위테스트.txt", 64);
  coro_http_server server(1, 8090);
  server.set_static_res_dir("", "");
  server.async_start();
  std::this_thread::sleep_for(200ms);
  std::string uri = "http://127.0.0.1:8090/범위테스트.txt";
  {
    std::string filename = "테스트1.txt";
    std::error_code ec{};
    std::filesystem::remove(filename, ec);
    coro_http_client client{};
    resp_data result = co_await client.async_download(uri, filename, "1-10");
    assert(result.status == 206);
    assert(std::filesystem::file_size(filename) == 10);
    filename = "테스트2.txt";
    std::filesystem::remove(filename, ec);
    result = co_await client.async_download(uri, filename, "10-15");
    assert(result.status == 206);
    assert(std::filesystem::file_size(filename) == 6);
  }
  {
    coro_http_client client{};
    std::string uri = "http://127.0.0.1:8090/범위테스트.txt";
    client.add_header("Range", "bytes=1-10,20-30");
    auto result = co_await client.async_get(uri);
    assert(result.status == 206);
    assert(result.resp_body.size() == 21);
    std::string filename = "범위다운로드.txt";
    client.add_header("Range", "bytes=0-10,21-30");
    result = co_await client.async_download(uri, filename);
    assert(result.status == 206);
    assert(fs::file_size(filename) == 21);
  }
}

멀티파트 업로드/다운로드

coro_http_server server(1, 8090);
server.set_http_handler<cinatra::PUT, cinatra::POST>(
    "/multipart_upload",
    [](coro_http_request &req,
       coro_http_response &resp) -> async_simple::coro::Lazy<void> {
      assert(req.get_content_type() == content_type::multipart);
      auto boundary = req.get_boundary();
      multipart_reader_t multipart(req.get_conn());
      while (true) {
        auto part_head = co_await multipart.read_part_head();
        if (part_head.ec) {
          co_return;
        }
        std::cout << part_head.name << "\n";
        std::cout << part_head.filename << "\n";
        std::shared_ptr<coro_io::coro_file> file;
        std::string filename;
        if (!part_head.filename.empty()) {
          file = std::make_shared<coro_io::coro_file>();
          filename = std::to_string(
              std::chrono::system_clock::now().time_since_epoch().count());
          size_t pos = part_head.filename.rfind('.');
          if (pos != std::string::npos) {
            auto extent = part_head.filename.substr(pos);
            filename += extent;
          }
          std::cout << filename << "\n";
          co_await file->async_open(filename, coro_io::flags::create_write);
          if (!file->is_open()) {
            resp.set_status_and_content(status_type::internal_server_error, "파일 열기 실패");
            co_return;
          }
        }
        auto part_body = co_await multipart.read_part_body(boundary);
        if (part_body.ec) {
          co_return;
        }
        if (!filename.empty()) {
          auto ec = co_await file->async_write(part_body.data.data(), part_body.data.size());
          if (ec) {
            co_return;
          }
          file->close();
          CHECK(fs::file_size(filename) == 1024);
        }
        else {
          std::cout << part_body.data << "\n";
        }
        if (part_body.eof) {
          break;
        }
      }
      resp.set_status_and_content(status_type::ok, "성공");
      co_return;
    });
server.async_start();
std::string filename = "테스트_1024.txt";
create_file(filename);
coro_http_client client{};
std::string uri = "http://127.0.0.1:8090/multipart_upload";
client.add_str_part("테스트", "테스트 값");
client.add_file_part("테스트 파일", filename);
auto result =
    async_simple::coro::syncAwait(client.async_upload_multipart(uri));
CHECK(result.status == 200);

벤치마크 코드

Brpc HTTP 벤치마크 코드

DEFINE_int32(포트, 9001, "서버의 TCP 포트");
DEFINE_int32(유휴_타임아웃_초, -1,
             "마지막 `유휴_타임아웃_초` 동안 읽기/쓰기 작업이 없으면 "
             "연결이 닫힙니다");
class HttpServiceImpl : public HttpService {
public:
  HttpServiceImpl() {}
  virtual ~HttpServiceImpl() {}
  void Echo(google::protobuf::RpcController *cntl_base, const HttpRequest *,
            HttpResponse *, google::protobuf::Closure *done) {
    brpc::ClosureGuard done_guard(done);
    brpc::Controller *cntl = static_cast<brpc::Controller *>(cntl_base);
    std::string 날짜_문자열{get_gmt_time_str()};
    cntl->http_response().SetHeader("Date", 날짜_문자열);
    cntl->http_response().SetHeader("Server", "brpc");
    cntl->http_response().set_content_type("text/plain");
    butil::IOBufBuilder os;
    os << "안녕하세요, 세계!";
    os.move_to(cntl->response_attachment());
  }
};
int main(int argc, char *argv[]) {
  GFLAGS_NS::ParseCommandLineFlags(&argc, &argv, true);
  brpc::Server server;
  example::HttpServiceImpl http_svc;
  if (server.AddService(&http_svc, brpc::SERVER_DOESNT_OWN_SERVICE) != 0) {
    LOG(ERROR) << "http_svc 추가 실패";
    return -1;
  }
  brpc::ServerOptions options;
  options.idle_timeout_sec = FLAGS_유휴_타임아웃_초;
  if (server.Start(FLAGS_포트, &options) != 0) {
    LOG(ERROR) << "HttpServer 시작 실패";
    return -1;
  }
  server.RunUntilAskedToQuit();
  return 0;
}

Drogon 벤치마크 코드

#include <drogon/drogon.h>
using namespace drogon;
int main() {
    app()
    .setLogPath("./")
    .setLogLevel(trantor::Logger::kWarn)
    .addListener("0.0.0.0", 9001)
    .setThreadNum(0)
    .registerSyncAdvice([](const HttpRequestPtr &req) -> HttpResponsePtr {
        auto response = HttpResponse::newHttpResponse();
        response->setBody("안녕하세요, 세계!");
        return response;
    })
    .run();
}

Nginx HTTP 설정

user nginx;
worker_processes auto;
worker_cpu_affinity auto;
error_log stderr error;
#worker_rlimit_nofile 1024000;
timer_resolution 1s;
daemon off;
events {
    worker_connections 32768;
    multi_accept off; # 기본값
}
http {
    include /etc/nginx/mime.types;
    access_log off;
    server_tokens off;
    msie_padding off;
    sendfile off; # 기본값
    tcp_nopush off; # 기본값
    tcp_nodelay on; # 기본값
    keepalive_timeout 65;
    keepalive_disable none; # 기본값 msie6
    keepalive_requests 300000; # 기본값 100
    server {
        listen 9001 default_server reuseport deferred fastopen=4096;
        root /;
        location = /plaintext {
            default_type text/plain;
            return 200 "안녕하세요, 세계!";
        }
    }
}

Cinatra 벤치마크 코드

#include <cinatra.hpp>
using namespace cinatra;
using namespace std::chrono_literals;
int main() {
  coro_http_server server(std::thread::hardware_concurrency(), 8090);
  server.set_http_handler<GET>(
      "/plaintext", [](coro_http_request& req, coro_http_response& resp) {
        resp.get_conn()->set_multi_buf(false);
        resp.set_content_type<resp_content_type::txt>();
        resp.set_status_and_content(status_type::ok, "안녕하세요, 세계!");
      });
  server.sync_start();
}

태그: C++20 코루틴 HTTP websocket Cinatra

7월 8일 19:54에 게시됨