Rust로 웹서버를 구현하는 방법을 단계별로 설명합니다. 이번 예제에서는 hyper 크레이트를 사용하여 간단한 웹서버를 구현합니다.
먼저 새로운 Rust 프로젝트를 생성합니다.
cargo new rust_web_server
cd rust_web_server
Cargo.toml 파일에 hyper 크레이트를 추가합니다.
[dependencies]
hyper = "0.14"
tokio = { version = "1", features = ["full"] }
hyper는 HTTP 서버와 클라이언트를 위한 라이브러리이며, tokio는 비동기 런타임을 제공합니다.
src/main.rs 파일을 열고, 기본 웹서버를 구현합니다.
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use std::convert::Infallible;
use std::net::SocketAddr;
async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
Ok(Response::new(Body::from("Hello, World!")))
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let make_svc = make_service_fn(|_conn| {
async { 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);
}
}
위 코드에서 handle_request 함수는 HTTP 요청을 처리하고, "Hello, World!" 응답을 반환합니다. main 함수는 서버를 설정하고, 요청을 처리할 서비스 함수를 지정합니다.
프로젝트 디렉토리에서 다음 명령어를 실행하여 서버를 시작합니다.
cargo run
브라우저에서 http://127.0.0.1:3000에 접속하면 "Hello, World!" 메시지를 볼 수 있습니다.
다양한 경로를 처리하도록 서버를 확장합니다.
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use std::convert::Infallible;
use std::net::SocketAddr;
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
let response = match req.uri().path() {
"/" => Response::new(Body::from("Hello, World!")),
"/hello" => Response::new(Body::from("Hello, Rust!")),
_ => Response::new(Body::from("404 Not Found")),
};
Ok(response)
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let make_svc = make_service_fn(|_conn| {
async { 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);
}
}
위 코드에서 handle_request 함수는 요청 경로에 따라 다른 응답을 반환합니다. "/" 경로는 "Hello, World!"를, "/hello" 경로는 "Hello, Rust!"를, 그 외의 경로는 "404 Not Found"를 반환합니다.
JSON 응답을 반환하도록 서버를 확장합니다.
전체 웹서버 코드
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use serde_json::json;
use std::convert::Infallible;
use std::net::SocketAddr;
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
let response = match req.uri().path() {
"/" => Response::new(Body::from("Hello, World!")),
"/hello" => Response::new(Body::from("Hello, Rust!")),
"/json" => {
let data = json!({
"message": "Hello, JSON!",
"status": "success"
});
Response::new(Body::from(data.to_string()))
}
_ => Response::new(Body::from("404 Not Found")),
};
Ok(response)
}
#[tokio::main]
async fn main() {
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let make_svc = make_service_fn(|_conn| {
async { 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);
}
}
위 코드에서 /json 경로는 JSON 데이터를 반환합니다. serde_json 크레이트를 사용하여 JSON 데이터를 생성합니다.
TCP 연결 수신
TcpListener를 사용한 서버 바인딩
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("연결 성공!");
}
}
클라이언트 연결 처리
use std::io::{Read, Write};
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
HTTP 요청 처리
HTTP 요청 파싱 구현
fn parse_request(buffer: &[u8]) -> Request {
let request = String::from_utf8_lossy(buffer);
let mut lines = request.lines();
let request_line = lines.next().unwrap();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap();
let path = parts.next().unwrap();
Request { method: method.to_string(), path: path.to_string() }
}
라우팅 시스템 구축
fn router(request: &Request) -> Response {
match request.path.as_str() {
"/" => Response::new(200, "Hello World"),
"/about" => Response::new(200, "About Page"),
_ => Response::new(404, "Not Found")
}
}
스레드풀 구현
Worker 스레드 생성
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
}
작업 실행
impl ThreadPool {
pub fn execute<F>(&self, f: F)
where F: FnOnce() + Send + 'static {
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
정상적인 종료와 정리
시그널 핸들링
use ctrlc;
fn setup_signal_handler(pool: ThreadPool) {
ctrlc::set_handler(move || {
println!("서버를 종료합니다...");
drop(pool);
std::process::exit(0);
}).expect("Error setting Ctrl-C handler");
}
리소스 정리
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Worker {} 종료중", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
최종 코드 예시
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::io::prelude::*;
use std::net::{TcpListener, TcpStream};
use std::fs;
// Worker 구조체 정의
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {} got a job; executing.", id);
job();
});
Worker {
id,
thread: Some(thread),
}
}
}
// ThreadPool 구조체와 Job 타입 정의
type Job = Box<dyn FnOnce() + Send + 'static>;
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where F: FnOnce() + Send + 'static {
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Worker {} 종료중", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
// 메인 함수
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
// 시그널 핸들러 설정
let pool_clone = pool;
ctrlc::set_handler(move || {
println!("서버를 종료합니다...");
drop(pool_clone);
std::process::exit(0);
}).expect("Error setting Ctrl-C handler");
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
// 연결 처리 함수
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK", "index.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let response = format!(
"{}\r\nContent-Length: {}\r\n\r\n{}",
status_line,
contents.len(),
contents
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
참조: https://doc.rust-lang.org/book/ch20-00-final-project-a-web-server.html