단일 스레드 웹서버를 러스트로 구현하는 과정을 정리합니다.
시작하기 전에
본격적으로 시작하기 전에 알아야 할 개념들을 간략히 정리해보자.
단어 | 설명 |
---|---|
스레드 | 프로세스가 할당받은 자원을 이용하는 실행의 단위. |
웹서버 | 웹 브라우저와 같은 클라이언트로부터 HTTP 요청을 받아들이고, HTML 문서와 같은 웹 페이지를 반환하는 컴퓨터 프로그램. |
HTTP | HyperText Transfer Protocol의 약자로 클라이언트와 서버 사이에 이루어지는 요청/응답(request/response) 프로토콜. HTTP를 통해 전달되는 자료는 http:로 시작하는 URL로 조회할 수 있다. |
TCP | Transmission Control Protocol의 약자로 네트워크의 정보 전달을 통제하여 인터넷을 이루는 핵심 프로토콜. 웹 브라우저들이 서버에 연결할 때 사용되거나 이메일 및 파일 전송에도 사용된다. |
소켓 | 네트워크로 데이터를 내보내거나 받을 때 사용되는 창구. 소켓은 크게 프로토콜, IP, 포트로 구성되며 프로세스가 데이터를 주고받기 위해서는 반드시 소켓을 열어야 한다. |
웹서버 작동원리
웹서버를 구현하기 위해서는 웹서버가 어떤 순서로 일을 진행하는지를 알아야 한다. 웹서버는 개념에서 간단하게 살펴봤듯이 클라이언트와 연결되어 요청을 받고 이에 맞는 응답을 제공해주는 프로그램이다. 웹서버가 작동하는 순서는 아래와 같다.
- 소켓으로 TCP 연결 대기하기
- HTTP 요청 읽기
- HTTP 요청 분석 및 검증하기
- 요청 분석에 따른 응답 작성하기
- 응답 보내기
이제 위 순서대로 하나씩 러스트로 구현해보자.
시작 전 준비
먼저 이번에 사용할 새로운 프로젝트를 생성하자. 원하는 경로에서 아래의 코드를 입력하여 새로운 프로젝트를 만든다.
1 | cargo new <project name> |
그러면 아래와 같은 디렉토리 구조를 가진 프로젝트 폴더가 생성된다.
1 | Cargo.lock |
웹서버 개발을 위한 모든 준비가 완료되었다.
소켓으로 TCP 연결 대기하기
웹서버는 클라이언트에서 TCP 연결을 시도할때까지 대기해야 한다. 해당 코드는 모듈 std::net
을 사용하면 쉽게 구현할 수 있다. 아래의 코드를 src/main.rs
에 작성해보자.
1 | use std::net::TcpListener; |
위 코드는 “127.0.0.1:7878” 주소로 Tcp 연결을 받을 수 있도록 작성한 것이다. 먼저 TcpListener
의 Bind()
를 사용하여 연결 요청을 기다린다. 네트워크에서 바인드(Bind)란 특정 포트에서 대기하다가 들어오는 요청을 서버 프로그램에 연결해주는 것을 의미하며, 여기서도 동일한 기능을 하는 함수이다. listener
의 값을 출력해보면 아래와 같은 TcpListener 인스턴스를 생성한 것을 알 수 있다.
1 | TcpListener { addr: 127.0.0.1:7878, fd: 3 } |
문제없이 포트에 바인딩에 성공했다면 위와 같이 TcpListener 인스턴스가 생성되어 클라이언트의 연결을 대기한다.
이제 클라이언트에서 연결을 시도해보자. 웹 브라우저에 접속해서 URL 창에 “127.0.0.1:7878”을 입력한다. 그러면 브라우저에서는 “사이트에 연결할 수 없음”이라고 나오지만 우리가 만든 프로그램에서는 “connected”가 계속 출력될 것이다.
1 | connected |
클라이언트에 연결이 시도되면 TcpListener 인스턴스는 incoming
이라는 메서드를 통해 스트림을 읽어온다. 여기서 스트림은 클라이언트가 요청하고 이를 서버가 읽어 적합한 응답을 생성한 뒤 보내는 일련의 과정을 통칭한다. 즉, 위 코드에서 보자면 for loop 한번이 스트림 하나라고 보면 된다. 따라서 현재 우리가 만든 프로그램은 연결 요청은 받았지만 응답을 보내지 않기 때문에 웹 브라우저에서는 연결 이슈로 나오는 것이다. incoming
메서드로 읽어온 스트림은 아래와 같다.
1 | TcpStream { addr: 127.0.0.1:7878, peer: 127.0.0.1:53614, fd: 4 } |
웹 브라우저에서 접속을 한번 시도했음에도 계속 연결을 시도하는 이유는 여러가지가 있을 수 있다. 클라이언트가 서버에게 여러 자원에 대한 요청을 해야하는 경우도 있고, 또 연결에 실패했을 경우 재시도하도록 설계된 경우도 있다.
HTTP 요청 읽기
이제 스트림의 요청을 읽어보는 코드를 추가해보자. handle_connection()
이라는 함수를 작성하고 이를 main()
에 추가한다.
1 | use std::{net::{TcpListener, TcpStream}, io::Read}; |
handle_connection()
은 TcpStream 데이터를 출력하는 함수이다. 첫번째 줄에서는 buffer
라는 변수를 만들고, 두번째 줄에서는 해당 변수에 스트림 데이터를 입력한다. 여기서 스트림 데이터는 모두 바이트 타입의 데이터이다. 마지막으로 String::ffrom_utf8_lossy()
을 사용하여 스트림 데이터를 문자열로 변환하여 출력한다. 출력한 결과는 다음과 같다.
1 | connected |
위 요청이 정확히 무엇인지는 다음 글에서 더 자세히 살펴보자.