소개
단일 스레드 웹 서버 구축에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 HTTP 및 TCP 프로토콜을 사용하여 클라이언트 요청을 처리하고 응답을 제공하는 단일 스레드 웹 서버를 구축할 것입니다.
단일 스레드 웹 서버 구축
단일 스레드 웹 서버를 작동시키는 것부터 시작합니다. 시작하기 전에 웹 서버 구축에 관련된 프로토콜에 대한 간략한 개요를 살펴보겠습니다. 이러한 프로토콜의 세부 사항은 이 책의 범위를 벗어나지만, 간략한 개요를 통해 필요한 정보를 얻을 수 있습니다.
웹 서버와 관련된 두 가지 주요 프로토콜은 Hypertext Transfer Protocol (HTTP, 하이퍼텍스트 전송 프로토콜) 와 Transmission Control Protocol (TCP, 전송 제어 프로토콜) 입니다. 두 프로토콜 모두 request-response (요청 - 응답) 프로토콜로, client (클라이언트) 가 요청을 시작하고 server (서버) 가 요청을 수신하여 클라이언트에게 응답을 제공합니다. 이러한 요청과 응답의 내용은 프로토콜에 의해 정의됩니다.
TCP 는 정보가 한 서버에서 다른 서버로 어떻게 전달되는지에 대한 세부 사항을 설명하는 하위 레벨 프로토콜이지만, 해당 정보가 무엇인지 지정하지는 않습니다. HTTP 는 TCP 를 기반으로 구축되어 요청 및 응답의 내용을 정의합니다. 기술적으로는 다른 프로토콜과 함께 HTTP 를 사용하는 것이 가능하지만, 대부분의 경우 HTTP 는 TCP 를 통해 데이터를 전송합니다. 우리는 TCP 및 HTTP 요청과 응답의 원시 바이트 (raw bytes) 로 작업할 것입니다.
TCP 연결 수신
웹 서버는 TCP 연결을 수신해야 하므로, 이것이 우리가 작업할 첫 번째 부분입니다. 표준 라이브러리는 이를 수행할 수 있는 std::net 모듈을 제공합니다. 평소와 같이 새 프로젝트를 만들어 보겠습니다.
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
이제 src/main.rs에 Listing 20-1 의 코드를 입력하여 시작합니다. 이 코드는 로컬 주소 127.0.0.1:7878에서 들어오는 TCP 스트림을 수신합니다. 들어오는 스트림을 받으면 "Connection established!"를 출력합니다.
Filename: src/main.rs
use std::net::TcpListener;
fn main() {
1 let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
2 for stream in listener.incoming() {
3 let stream = stream.unwrap();
4 println!("Connection established!");
}
}
Listing 20-1: 들어오는 스트림을 수신하고 스트림을 수신할 때 메시지를 출력
TcpListener를 사용하여 주소 127.0.0.1:7878 [1]에서 TCP 연결을 수신할 수 있습니다. 주소에서 콜론 앞의 부분은 컴퓨터를 나타내는 IP 주소입니다 (이것은 모든 컴퓨터에서 동일하며 작성자의 컴퓨터를 특별히 나타내는 것은 아닙니다). 7878은 포트입니다. 우리는 이 포트를 두 가지 이유로 선택했습니다. HTTP 는 일반적으로 이 포트에서 허용되지 않으므로 서버가 컴퓨터에서 실행 중일 수 있는 다른 웹 서버와 충돌할 가능성이 낮고, 7878 은 전화 키패드에서 rust를 입력한 것입니다.
이 시나리오에서 bind 함수는 새로운 TcpListener 인스턴스를 반환한다는 점에서 new 함수와 유사하게 작동합니다. 이 함수가 bind라고 불리는 이유는 네트워킹에서 포트에 연결하여 수신하는 것을 "포트에 바인딩"이라고 하기 때문입니다.
bind 함수는 Result<T, E>를 반환하며, 이는 바인딩이 실패할 수 있음을 나타냅니다. 예를 들어, 포트 80 에 연결하려면 관리자 권한이 필요합니다 (비관리자는 1023 보다 높은 포트에서만 수신할 수 있음). 따라서 관리자가 아닌 상태에서 포트 80 에 연결하려고 하면 바인딩이 작동하지 않습니다. 또한, 예를 들어 프로그램의 두 인스턴스를 실행하여 두 프로그램이 동일한 포트를 수신하는 경우에도 바인딩이 작동하지 않습니다. 우리는 학습 목적으로 기본적인 서버를 작성하고 있으므로 이러한 종류의 오류를 처리하는 것에 대해 걱정하지 않을 것입니다. 대신, 오류가 발생하면 unwrap을 사용하여 프로그램을 중지합니다.
TcpListener의 incoming 메서드는 스트림 시퀀스 [2] (더 구체적으로는 TcpStream 유형의 스트림) 를 제공하는 반복자를 반환합니다. 단일 stream (스트림) 은 클라이언트와 서버 간의 열린 연결을 나타냅니다. connection (연결) 은 클라이언트가 서버에 연결하고, 서버가 응답을 생성하고, 서버가 연결을 닫는 전체 요청 및 응답 프로세스의 이름입니다. 따라서 우리는 TcpStream에서 읽어 클라이언트가 보낸 내용을 확인한 다음, 응답을 스트림에 써서 데이터를 클라이언트에 다시 보낼 것입니다. 전반적으로 이 for 루프는 각 연결을 차례로 처리하고 처리할 일련의 스트림을 생성합니다.
현재로서는 스트림 처리가 스트림에 오류가 있는 경우 프로그램을 종료하기 위해 unwrap을 호출하는 것으로 구성됩니다 [3]; 오류가 없으면 프로그램이 메시지를 출력합니다 [4]. 다음 목록에서 성공적인 경우에 대한 더 많은 기능을 추가할 것입니다. 클라이언트가 서버에 연결할 때 incoming 메서드에서 오류를 수신할 수 있는 이유는 실제로 연결을 반복하지 않기 때문입니다. 대신, connection attempts (연결 시도) 를 반복하고 있습니다. 연결은 여러 가지 이유로 성공하지 못할 수 있으며, 그 중 많은 부분이 운영 체제에 따라 다릅니다. 예를 들어, 많은 운영 체제는 지원할 수 있는 동시 열린 연결 수에 제한이 있습니다. 해당 수를 초과하는 새로운 연결 시도는 일부 열린 연결이 닫힐 때까지 오류를 생성합니다.
이 코드를 실행해 봅시다! 터미널에서 cargo run을 호출한 다음 웹 브라우저에서 127.0.0.1:7878을 로드합니다. 브라우저는 "Connection reset"과 같은 오류 메시지를 표시해야 합니다. 서버가 현재 데이터를 다시 보내지 않기 때문입니다. 그러나 터미널을 보면 브라우저가 서버에 연결될 때 출력된 여러 메시지를 볼 수 있습니다!
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
때로는 하나의 브라우저 요청에 대해 여러 메시지가 출력되는 것을 볼 수 있습니다. 그 이유는 브라우저가 페이지에 대한 요청뿐만 아니라 브라우저 탭에 나타나는 favicon.ico 아이콘과 같은 다른 리소스에 대한 요청을 하기 때문일 수 있습니다.
또한 서버가 데이터를 응답하지 않기 때문에 브라우저가 서버에 여러 번 연결을 시도하는 것일 수도 있습니다. stream이 범위를 벗어나 루프의 끝에서 삭제되면 drop 구현의 일부로 연결이 닫힙니다. 브라우저는 때때로 문제가 일시적일 수 있으므로 닫힌 연결을 재시도하여 처리합니다. 중요한 요소는 TCP 연결에 대한 핸들을 성공적으로 얻었다는 것입니다!
특정 버전의 코드를 실행한 후에는 ctrl-C 를 눌러 프로그램을 중지하는 것을 잊지 마십시오. 그런 다음 각 코드 변경 사항을 만든 후 cargo run 명령을 호출하여 프로그램을 다시 시작하여 최신 코드를 실행하고 있는지 확인하십시오.
요청 읽기
브라우저에서 요청을 읽는 기능을 구현해 봅시다! 먼저 연결을 얻은 다음 연결에 대한 작업을 수행하는 문제를 분리하기 위해 연결을 처리하기 위한 새로운 함수를 시작합니다. 이 새로운 handle_connection 함수에서 TCP 스트림에서 데이터를 읽고 출력하여 브라우저에서 전송되는 데이터를 볼 수 있습니다. 코드를 Listing 20-2 와 같이 변경합니다.
Filename: src/main.rs
1 use std::{
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
2 handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
3 let buf_reader = BufReader::new(&mut stream);
4 let http_request: Vec<_> = buf_reader
5 .lines()
6 .map(|result| result.unwrap())
7 .take_while(|line| !line.is_empty())
.collect();
8 println!("Request: {:#?}", http_request);
}
Listing 20-2: TcpStream에서 읽고 데이터를 출력
스트림에서 읽고 쓰기 위한 트레이트 (traits) 와 타입을 얻기 위해 std::io::prelude 및 std::io::BufReader를 범위 내로 가져옵니다 [1]. main 함수의 for 루프에서 연결을 만들었다는 메시지를 출력하는 대신, 이제 새로운 handle_connection 함수를 호출하고 stream을 전달합니다 [2].
handle_connection 함수에서 stream에 대한 가변 참조를 래핑하는 새로운 BufReader 인스턴스를 생성합니다 [3]. BufReader는 std::io::Read 트레이트 메서드에 대한 호출을 관리하여 버퍼링을 추가합니다.
브라우저가 서버로 보내는 요청의 줄을 수집하기 위해 http_request라는 변수를 생성합니다. Vec<_> 타입 주석 [4]을 추가하여 이러한 줄을 벡터로 수집하려는 것을 나타냅니다.
BufReader는 std::io::BufRead 트레이트를 구현하며, 이는 lines 메서드 [5]를 제공합니다. lines 메서드는 줄 바꿈 바이트를 볼 때마다 데이터 스트림을 분할하여 Result<String, std::io::Error>의 반복자를 반환합니다. 각 String을 얻기 위해 각 Result를 매핑하고 unwrap합니다 [6]. 데이터가 유효한 UTF-8 이 아니거나 스트림에서 읽는 데 문제가 있는 경우 Result가 오류일 수 있습니다. 다시 말하지만, 프로덕션 프로그램은 이러한 오류를 더 적절하게 처리해야 하지만, 단순성을 위해 오류가 발생한 경우 프로그램을 중지하도록 선택했습니다.
브라우저는 HTTP 요청의 끝을 연속으로 두 개의 줄 바꿈 문자를 전송하여 신호를 보냅니다. 따라서 스트림에서 하나의 요청을 얻기 위해 빈 문자열인 줄을 얻을 때까지 줄을 가져옵니다 [7]. 줄을 벡터로 수집했으면, 웹 브라우저가 서버로 보내는 지침을 살펴볼 수 있도록 예쁜 디버그 형식 [8]을 사용하여 출력합니다.
이 코드를 사용해 봅시다! 프로그램을 시작하고 웹 브라우저에서 다시 요청을 보냅니다. 브라우저에서 여전히 오류 페이지가 표시되지만 터미널의 프로그램 출력은 다음과 유사하게 표시됩니다.
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0)
Gecko/20100101 Firefox/99.0",
"Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*
;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
브라우저에 따라 약간 다른 출력을 얻을 수 있습니다. 이제 요청 데이터를 출력하고 있으므로 요청의 첫 번째 줄에서 GET 뒤의 경로를 보면 하나의 브라우저 요청에서 여러 연결을 얻는 이유를 알 수 있습니다. 반복되는 연결이 모두 */*를 요청하는 경우, 브라우저가 프로그램에서 응답을 받지 못하므로 */*를 반복적으로 가져오려고 한다는 것을 알 수 있습니다.
브라우저가 프로그램에 요청하는 내용을 이해하기 위해 이 요청 데이터를 분석해 보겠습니다.
HTTP 요청 자세히 살펴보기
HTTP 는 텍스트 기반 프로토콜이며, 요청은 다음과 같은 형식을 취합니다.
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
첫 번째 줄은 클라이언트가 요청하는 정보에 대한 정보를 담고 있는 요청 라인입니다. 요청 라인의 첫 번째 부분은 클라이언트가 이 요청을 어떻게 하는지 설명하는 GET 또는 POST와 같은 메서드를 나타냅니다. 우리 클라이언트는 정보를 요청하는 GET 요청을 사용했습니다.
요청 라인의 다음 부분은 */*이며, 클라이언트가 요청하는 uniform resource identifier *(URI)*를 나타냅니다. URI 는 uniform resource locator *(URL)*와 거의 같지만 완전히 같지는 않습니다. 이 장에서 URI 와 URL 의 차이점은 중요하지 않지만, HTTP 사양에서는 URI라는 용어를 사용하므로 여기에서 URI 대신 URL을 정신적으로 대체할 수 있습니다.
마지막 부분은 클라이언트가 사용하는 HTTP 버전이며, 요청 라인은 CRLF 시퀀스로 끝납니다. (CRLF 는 carriage return 및 line feed의 약자이며, 이는 타자기 시대의 용어입니다!) CRLF 시퀀스는 \r\n으로도 작성할 수 있으며, 여기서 \r은 carriage return 이고 \n은 line feed 입니다. CRLF 시퀀스는 요청 라인을 나머지 요청 데이터와 구분합니다. CRLF 가 출력될 때 \r\n 대신 새 줄이 시작되는 것을 볼 수 있습니다.
지금까지 프로그램을 실행하여 받은 요청 라인 데이터를 살펴보면, GET이 메서드이고, */*가 요청 URI 이며, HTTP/1.1이 버전임을 알 수 있습니다.
요청 라인 다음, Host:부터 시작하는 나머지 줄은 헤더입니다. GET 요청에는 본문이 없습니다.
다른 브라우저에서 요청하거나 127.0.0.1:7878/test와 같은 다른 주소를 요청하여 요청 데이터가 어떻게 변경되는지 확인해 보십시오.
이제 브라우저가 무엇을 요청하는지 알았으니, 데이터를 다시 보내 봅시다!
응답 작성
클라이언트 요청에 대한 응답으로 데이터를 전송하는 것을 구현할 것입니다. 응답은 다음과 같은 형식을 갖습니다.
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
첫 번째 줄은 응답에 사용된 HTTP 버전, 요청 결과를 요약하는 숫자 상태 코드, 상태 코드에 대한 텍스트 설명을 제공하는 이유 구문을 포함하는 상태 라인입니다. CRLF 시퀀스 다음에는 헤더, 다른 CRLF 시퀀스 및 응답 본문이 있습니다.
다음은 HTTP 버전 1.1 을 사용하고, 상태 코드 200, OK 이유 구문, 헤더 없음, 본문 없음이 있는 응답 예시입니다.
HTTP/1.1 200 OK\r\n\r\n
상태 코드 200 은 표준 성공 응답입니다. 텍스트는 작은 성공적인 HTTP 응답입니다. 성공적인 요청에 대한 응답으로 스트림에 이것을 작성해 봅시다! handle_connection 함수에서 요청 데이터를 출력하던 println!을 제거하고 Listing 20-3 의 코드로 바꿉니다.
Filename: src/main.rs
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
1 let response = "HTTP/1.1 200 OK\r\n\r\n";
2 stream.write_all(response.3 as_bytes()).unwrap();
}
Listing 20-3: 스트림에 작은 성공적인 HTTP 응답을 작성
첫 번째 새 줄은 성공 메시지의 데이터를 담고 있는 response 변수를 정의합니다 [1]. 그런 다음 문자열 데이터를 바이트로 변환하기 위해 response에서 as_bytes를 호출합니다 [3]. stream의 write_all 메서드는 &[u8]을 가져와 해당 바이트를 연결로 직접 보냅니다 [2]. write_all 작업이 실패할 수 있으므로 이전과 마찬가지로 모든 오류 결과에 대해 unwrap을 사용합니다. 다시 말하지만, 실제 애플리케이션에서는 여기에 오류 처리를 추가합니다.
이러한 변경 사항을 적용하여 코드를 실행하고 요청을 해 봅시다. 더 이상 터미널에 데이터를 출력하지 않으므로 Cargo 의 출력 외에는 어떤 출력도 볼 수 없습니다. 웹 브라우저에서 127.0.0.1:7878을 로드하면 오류 대신 빈 페이지가 표시됩니다. 방금 HTTP 요청을 수신하고 응답을 보내는 것을 직접 코딩했습니다!
실제 HTML 반환
빈 페이지 이상을 반환하는 기능을 구현해 보겠습니다. 프로젝트 디렉토리의 루트 ( src 디렉토리 아님) 에 새 파일 hello.html을 만듭니다. 원하는 HTML 을 입력할 수 있습니다. Listing 20-4 는 한 가지 가능성을 보여줍니다.
Filename: hello.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
Listing 20-4: 응답으로 반환할 샘플 HTML 파일
이것은 제목과 텍스트가 있는 최소한의 HTML5 문서입니다. 요청을 받으면 서버에서 이 문서를 반환하기 위해 Listing 20-5 에 표시된 대로 handle_connection을 수정하여 HTML 파일을 읽고, 본문으로 응답에 추가하고, 전송합니다.
Filename: src/main.rs
use std::{
1 fs,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
};
--snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
2 let response = format!(
"{status_line}\r\n\
Content-Length: {length}\r\n\r\n\
{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
Listing 20-5: hello.html의 내용을 응답 본문으로 전송
표준 라이브러리의 파일 시스템 모듈을 범위 내로 가져오기 위해 use 문에 fs를 추가했습니다 [1]. 파일의 내용을 문자열로 읽는 코드는 익숙해야 합니다. Listing 12-4 에서 I/O 프로젝트에 대한 파일의 내용을 읽을 때 사용했습니다.
다음으로, format!을 사용하여 파일의 내용을 성공 응답의 본문으로 추가합니다 [2]. 유효한 HTTP 응답을 보장하기 위해 응답 본문의 크기, 즉 이 경우 hello.html의 크기로 설정된 Content-Length 헤더를 추가합니다.
cargo run으로 이 코드를 실행하고 브라우저에서 127.0.0.1:7878을 로드하면 HTML 이 렌더링되는 것을 볼 수 있습니다!
현재 http_request의 요청 데이터를 무시하고 HTML 파일의 내용을 무조건 다시 보내고 있습니다. 즉, 브라우저에서 127.0.0.1:7878/something-else를 요청하려고 하면 동일한 HTML 응답을 받게 됩니다. 현재 서버는 매우 제한적이며 대부분의 웹 서버가 수행하는 작업을 수행하지 않습니다. 요청에 따라 응답을 사용자 정의하고 */*에 대한 올바른 형식의 요청에 대해서만 HTML 파일을 다시 보내고 싶습니다.
요청 유효성 검사 및 선택적 응답
현재 웹 서버는 클라이언트가 요청한 내용에 관계없이 파일의 HTML 을 반환합니다. 브라우저가 HTML 파일을 반환하기 전에 */*를 요청하는지 확인하고, 브라우저가 다른 것을 요청하는 경우 오류를 반환하는 기능을 추가해 보겠습니다. 이를 위해 Listing 20-6 에 표시된 대로 handle_connection을 수정해야 합니다. 이 새로운 코드는 수신된 요청의 내용을 */*에 대한 요청이 어떻게 보이는지 알고 있는 내용과 비교하고, 요청을 다르게 처리하기 위해 if 및 else 블록을 추가합니다.
Filename: src/main.rs
--snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
1 let request_line = buf_reader
.lines()
.next()
.unwrap()
.unwrap();
2 if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\n\
Content-Length: {length}\r\n\r\n\
{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
3 } else {
// some other request
}
}
Listing 20-6: 다른 요청과 다르게 */*에 대한 요청 처리
HTTP 요청의 첫 번째 줄만 볼 것이므로 전체 요청을 벡터로 읽는 대신 next를 호출하여 반복자에서 첫 번째 항목을 가져옵니다 [1]. 첫 번째 unwrap은 Option을 처리하고 반복자에 항목이 없으면 프로그램을 중지합니다. 두 번째 unwrap은 Result를 처리하며 Listing 20-2 에 추가된 map의 unwrap과 동일한 효과를 갖습니다.
다음으로, request_line이 / 경로에 대한 GET 요청의 요청 줄과 같은지 확인합니다 [2]. 그렇다면 if 블록은 HTML 파일의 내용을 반환합니다.
request_line이 / 경로에 대한 GET 요청과 같지 않으면 다른 요청을 받았다는 의미입니다. 잠시 후 else 블록 [3]에 코드를 추가하여 다른 모든 요청에 응답합니다.
지금 이 코드를 실행하고 127.0.0.1:7878을 요청하면 hello.html의 HTML 을 얻을 수 있습니다. 127.0.0.1:7878/something-else와 같은 다른 요청을 하면 Listing 20-1 및 Listing 20-2 에서 코드를 실행할 때 보았던 것과 같은 연결 오류가 발생합니다.
이제 Listing 20-7 의 코드를 else 블록에 추가하여 요청에 대한 콘텐츠를 찾을 수 없음을 나타내는 상태 코드 404 로 응답을 반환해 보겠습니다. 또한 최종 사용자에게 응답을 나타내는 브라우저에서 렌더링할 페이지에 대한 일부 HTML 을 반환합니다.
Filename: src/main.rs
--snip--
} else {
1 let status_line = "HTTP/1.1 404 NOT FOUND";
2 let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\n\
Content-Length: {length}\r\n\r\n
{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
Listing 20-7: / 이외의 요청이 있는 경우 상태 코드 404 및 오류 페이지로 응답
여기서 응답에는 상태 코드 404 와 이유 구문 NOT FOUND가 있는 상태 라인이 있습니다 [1]. 응답 본문은 파일 404.html의 HTML 이 됩니다 [1]. 오류 페이지에 대해 hello.html 옆에 404.html 파일을 만들어야 합니다. 다시 한 번 원하는 HTML 을 자유롭게 사용하거나 Listing 20-8 의 예제 HTML 을 사용하십시오.
Filename: 404.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
Listing 20-8: 404 응답과 함께 다시 보낼 페이지의 샘플 콘텐츠
이러한 변경 사항을 적용하여 서버를 다시 실행합니다. 127.0.0.1:7878을 요청하면 hello.html의 내용이 반환되고, 127.0.0.1:7878/foo와 같은 다른 요청은 404.html의 오류 HTML 을 반환해야 합니다.
약간의 리팩토링
현재 if 및 else 블록에는 많은 중복이 있습니다. 둘 다 파일을 읽고 파일의 내용을 스트림에 쓰고 있습니다. 유일한 차이점은 상태 라인과 파일 이름입니다. 이러한 차이점을 별도의 if 및 else 라인으로 분리하여 코드를 더 간결하게 만들어 상태 라인과 파일 이름의 값을 변수에 할당할 수 있습니다. 그런 다음 해당 변수를 사용하여 파일을 읽고 응답을 쓰는 코드를 무조건 사용할 수 있습니다. Listing 20-9 는 큰 if 및 else 블록을 대체한 후의 결과 코드를 보여줍니다.
Filename: src/main.rs
--snip--
fn handle_connection(mut stream: TcpStream) {
--snip--
let (status_line, filename) =
if request_line == "GET / HTTP/1.1" {
("HTTP/1.1 200 OK", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\n\
Content-Length: {length}\r\n\r\n\
{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
Listing 20-9: 두 경우의 차이점만 포함하도록 if 및 else 블록 리팩토링
이제 if 및 else 블록은 튜플에서 상태 라인과 파일 이름에 대한 적절한 값만 반환합니다. 그런 다음 18 장에서 설명한 대로 let 문에서 패턴을 사용하여 이러한 두 값을 status_line 및 filename에 할당하기 위해 구조 분해를 사용합니다.
이전에 중복된 코드는 이제 if 및 else 블록 외부에 있으며 status_line 및 filename 변수를 사용합니다. 이렇게 하면 두 경우의 차이점을 쉽게 확인할 수 있으며, 파일 읽기 및 응답 쓰기 작동 방식을 변경하려는 경우 코드를 업데이트할 위치가 하나만 있다는 의미입니다. Listing 20-9 의 코드 동작은 Listing 20-8 의 코드 동작과 동일합니다.
훌륭합니다! 이제 약 40 줄의 Rust 코드로 된 간단한 웹 서버가 있어 하나의 요청에 콘텐츠 페이지로 응답하고 다른 모든 요청에 404 응답으로 응답합니다.
현재 서버는 단일 스레드에서 실행되므로 한 번에 하나의 요청만 처리할 수 있습니다. 몇 가지 느린 요청을 시뮬레이션하여 문제가 될 수 있는 방식을 살펴보겠습니다. 그런 다음 서버가 한 번에 여러 요청을 처리할 수 있도록 수정하겠습니다.
요약
축하합니다! 단일 스레드 웹 서버 구축 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 기술을 향상시킬 수 있습니다.