프레임워크 없이 미니멀한 Rust HTTP 서버 구축하기
Ethan Miller
Product Engineer · Leapcell

소개
활기찬 Rust 생태계에는 Axum, Actix-web, Warp와 같이 강력한 HTTP 서비스를 구축하는 간소화된 경로를 제공하는 다양한 웹 프레임워크가 있습니다. 이러한 프레임워크는 편리한 추상화, 검증된 미들웨어, 광범위한 기능 세트를 제공하여 개발 속도를 크게 높입니다. 그러나 HTTP 서버의 기본 메커니즘을 이해하려는 사람이나 극도의 제어와 최소한의 종속성이 필요한 프로젝트의 경우, 이러한 프레임워크를 건너뛰고 핵심 라이브러리에서 직접 구축하는 것이 매우 중요합니다. 이 탐구는 hyper를 HTTP 프로토콜에, tokio를 비동기 런타임 및 I/O에 활용하여 미니멀한 Rust HTTP 서버를 구축하는 과정을 자세히 설명합니다. 이러한 노력은 Rust에서의 비동기 프로그래밍에 대한 이해를 깊게 할 뿐만 아니라 핵심 라이브러리의 강력함과 유연성을 보여주어 효율적이고 고도로 사용자 정의 가능한 솔루션으로 이어집니다.
원시 HTTP 서버의 핵심 구성 요소
코드를 자세히 살펴보기 전에 사용할 주요 구성 요소를 간략하게 정의해 보겠습니다.
tokio: Rust를 위한 강력한 비동기 런타임입니다. 비차단 I/O, 작업 스케줄링 및 퓨처 관리에 필요한 도구를 제공합니다. 서버에서tokio는 수신 연결을 수신 대기하고 동시 요청을 효율적으로 관리하는 역할을 합니다.hyper: Rust로 작성된 빠르고 정확한 HTTP 구현입니다.hyper는 웹 프레임워크보다 낮은 수준에서 작동하며 HTTP/1.1 및 HTTP/2 요청 및 응답을 직접 처리합니다. 수신 HTTP 요청의 구문 분석과 송신 HTTP 응답의 형식 지정을 처리하여 HTTP 프로토콜 자체의 복잡성은 추상화하지만 애플리케이션의 요청 처리 로직은 추상화하지 않습니다.async/await: Rust의 비동기 코드 작성을 위한 내장 구문입니다. 동기 코드처럼 보이고 느껴지는 비차단 코드를 작성할 수 있도록 하여 비동기 프로그래밍을 더 인체공학적으로 만듭니다.
이러한 구성 요소들이 어떻게 함께 작동하는지 이해하는 것이 중요합니다. tokio는 비동기 실행 환경과 TCP 연결을 수락하는 수단을 제공합니다. 연결이 설정되면 hyper가 수신 바이트 스트림을 HTTP 요청으로 구문 분석하고 HTTP 응답을 구문 분석하여 네트워크를 통해 전송할 바이트 스트림으로 다시 직렬화하는 역할을 합니다. 애플리케이션 로직은 이 두 단계 사이에 위치하여 구문 분석된 요청을 처리하고 적절한 응답을 생성합니다.
미니멀 서버 구축
필요한 종속성을 사용하여 Cargo.toml을 설정하는 것부터 시작하겠습니다.
[package] name = "minimal-http-server" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1", features = ["full"] } # "full"은 런타임, net, 매크로 등을 포함합니다. hyper = { version = "0.14", features = ["full"] }
다음으로 서버의 Rust 코드를 작성해 보겠습니다. 목표는 127.0.0.1:3000에서 수신 대기하고 모든 요청에 간단한 "Hello, world!" 메시지로 응답하는 서버를 만드는 것입니다.
use std::{convert::Infallible, net::SocketAddr}; use hyper::{Body, Request, Response, Server}; use hyper::service::{make_service_fn, service_fn}; // 핵심 요청 처리 함수 async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, Infallible> { // 이 간단한 예제에서는 수신된 요청을 무시합니다. Ok(Response::new("Hello, world!".into())) } #[tokio::main] async fn main() { // 수신 대기할 주소 정의 let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // `Service`는 Request에서 Response로의 비동기 함수를 나타내는 트레이트입니다. // `make_service_fn`은 각 수신 연결에 대해 새 `Service`를 생성하는 데 사용됩니다. let make_svc = make_service_fn(|_conn| async { // `service_fn`은 `Fn`을 `Service`로 변환하는 헬퍼입니다. Ok::<_, Infallible>(service_fn(handle_request)) }); // 지정된 주소와 서비스 팩토리로 서버 생성 let server = Server::bind(&addr).serve(make_svc); println!("Listening on http://{{}}", addr); // 오류가 발생할 때까지 서버 실행 if let Err(e) = server.await { eprintln!("server error: {{}}", e); } }
이 코드를 분석해 보겠습니다.
use문:std,hyper,hyper::service에서 필요한 유형을 가져옵니다.Infallible은Err변형이 발생할 수 없는Result유형에 사용되어 기본 예제에서 오류 처리를 단순화합니다.handle_request함수: 이async함수는 서버의 핵심 로직입니다.Request<Body>를 입력으로 받아Result<Response<Body>, Infallible>을 반환합니다. 이 예제에서는 수신된 요청을 무시하고 항상 "Hello, world!"를 본문으로 하는HTTP 200 OK응답을 반환합니다.#[tokio::main]속성:tokio의 이 매크로는 Tokio 런타임을 설정하는 것을 단순화합니다.main함수를 비동기 진입점으로 변환하고 런타임 초기화를 처리합니다.SocketAddr::from(([127, 0, 0, 1], 3000)): 서버가 수신 대기할 IP 주소와 포트를 정의합니다.make_service_fn: 이 함수는hyper서버에 중요합니다. 각 새 TCP 연결에 대해Service인스턴스를 반환하는 클로저를 받습니다. 이 패턴은 필요한 경우 연결별 상태를 허용하지만 간단한 경우에는 그렇지 않습니다. 클로저도async여야 합니다.service_fn(handle_request):make_service_fn내부에서service_fn은handle_requestasync함수를 서버가 예상하는Service트레이트 구현으로 조정하는 데 사용됩니다.Server::bind(&addr).serve(make_svc): 이 줄은hyper서버를 생성하고 구성합니다.bind는 수신 대기할 주소를 지정하고serve는make_svc팩토리를 받아 수신 연결을 처리합니다.server.await: 이것이 서버를 시작합니다.await키워드는 서버가 정상적으로 종료되거나 오류가 발생하는 동안main의 실행을 일시 중지합니다.
실행 및 테스트
이 서버를 실행하려면 터미널에서 프로젝트 디렉토리로 이동하고 다음을 실행합니다.
cargo run
"Listening on http://127.0.0.1:3000"이 표시되어야 합니다. 이제 웹 브라우저를 열거나 curl과 같은 도구를 사용하여 서버에 액세스합니다.
curl http://127.0.0.1:3000
응답으로: Hello, world!를 받아야 합니다.
이 간단한 예제는 기본 구성 요소를 보여줍니다. handle_request를 확장하여 다음을 수행할 수 있습니다.
- 요청 경로 구문 분석: 
req.uri().path() - 요청 헤더 읽기: 
req.headers() - 요청 본문 읽기: 
hyper::body::to_bytes(req.into_body()).await - 동적 응답 생성: 다른 상태, 헤더 및 본문 내용으로 
Response<Body>구성. 
결론
hyper와 tokio를 직접 활용하여 상위 수준 프레임워크에 의존하지 않고 Rust에서 미니멀한 HTTP 서버를 성공적으로 구축했습니다. 이 실습 접근 방식은 비동기 프로그래밍, HTTP 프로토콜 및 성능 네트워크 서비스를 구축하는 데 관련된 아키텍처 선택에 대한 심오한 통찰력을 제공합니다. Rust가 안전성과 동시성을 유지하면서 고성능의 저수준 네트워크 애플리케이션을 제공하는 능력을 강조하며, 개발자가 처음부터 사용자 정의되고 효율적인 서버 솔루션을 구축할 수 있도록 지원합니다.