20장. 웹서버 구현
Rust로 웹서버를 구현하는 방법을 단계별로 설명합니다. 이번 예제에서는 hyper
크레이트를 사용하여 간단한 웹서버를 구현합니다.
1. 프로젝트 생성
먼저 새로운 Rust 프로젝트를 생성합니다.
cargo new rust_web_server
cd rust_web_server
2. 의존성 추가
Cargo.toml
파일에 hyper
크레이트를 추가합니다.
[dependencies]
hyper = "0.14"
tokio = { version = "1", features = ["full"] }
hyper
는 HTTP 서버와 클라이언트를 위한 라이브러리이며, tokio
는 비동기 런타임을 제공합니다.
3. 기본 웹서버 구현
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
함수는 서버를 설정하고, 요청을 처리할 서비스 함수를 지정합니다.
4. 서버 실행
프로젝트 디렉토리에서 다음 명령어를 실행하여 서버를 시작합니다.
cargo run
브라우저에서 http://127.0.0.1:3000
에 접속하면 "Hello, World!" 메시지를 볼 수 있습니다.
5. 경로 처리
다양한 경로를 처리하도록 서버를 확장합니다.
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"를 반환합니다.
6. JSON 응답
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 데이터를 생성합니다.
웹서버 구현2
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(); }
Rust Language Programming에 대한 지식 학습을 통해 다음 포스트는 실무 응용 프로그래밍에 대해 프로젝트를 진행할 계획입니다.
참조: https://doc.rust-lang.org/book/ch20-00-final-project-a-web-server.html
'IT Best Practise > Rust' 카테고리의 다른 글
19. 고급 기능 (4) | 2024.11.07 |
---|---|
18. 패턴과 매칭 (0) | 2024.11.07 |
17. 객체지향 프로그래밍 (0) | 2024.11.07 |
16. 동시성 (0) | 2024.11.07 |
15. 스마트 포인터 (0) | 2024.11.07 |