Введение
Rust - высокоэффективный язык программирования, стоящий на самом любимом языке 7 лет подряд. Это связано с тем, что он является производительным, безопасным для памяти и очень гибким. Сегодня мы узнаем о создании HTTP-сервера на rust и изучим несколько концепций языка.
Начнем, убедитесь, что в вашей системе установлен rust, и давайте создадим новый проект.
cargo new my_web_server && cd my_web_server
Построение TCP-сервера
Создание TCP-сервера
Протокол HTTP работает поверх протокола TCP уровня L4 (сетевой уровень). При создании сервера фактически создается сервер TCP, и данные отправляются в формате HTTP, т.е. TCP указывает, как происходит связь, а HTTP указывает схему данных, которые должны быть отправлены по этому соединению.
Откройте main.rs
в любимой среде IDE и добавьте в нее следующий код:
#[allow(unused)]
use std::{net::{TcpListener}, io::Read};
fn main() {
let listener = TcpListener::bind("127.0.0.1:8477").unwrap();
println!("Сервер прослушивает порт 8477");
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Подключение произошло");
}
}
Давайте перейдем к разбору кода.
`#[allow(unused)]`
Это макрос rust, который сообщает компилятору, что мы можем создать переменные, которые мы можем не использовать. Это упрощает обработку некоторых распространенных ошибок в процессе обучения, но это не следует использовать в производственной кодовой базе.
`TcpListener::bind("127.0.0.1:8477").unwrap()`
В основном bind возвращает функцию в перечислении типа Result , которое является способом выразить, что вышеупомянутая функция может или ответить паникой или результатом (здесь TcpListener
), и мы хотим отказаться продолжать выполнение при ошибке, в другом случае продолжить программу.
Это имеет смысл использовать, если возникновение исключения может привести к остановке программы и она не сможет продолжать выполнение дальше, есть лучшие способы обработки такого исключения.
println!("Сервер прослушивает порт 8477");
В имени функции println
есть восклицательный знак, потому что она не является функцией и является макросом.
for stream in listener.incoming() {
Метод incoming()
, возвращает итератор потоков (в частности, потоков с типом, определяемым, например, TcpStream
. Один поток представляет открытое соединение между клиентом и сервером, так что здесь мы в основном обходим по каждому соединению через цикл for
.
Это может показаться тривиальным для наивного разработчика, но опытный человек может знать, что для обработки новых соединений в большинстве языков потребуется логика, управляемая событиями, что технически происходит и здесь, но это ленивый итератор, который автоматически вызывает следующую последовательность в цикле for, упрощая кодирование.
let stream = stream.unwrap();
Поток переменных цикла возвращает Result
, который необходимо развернуть. (Это не элегантный способ справиться с этим, так как один сбой соединения тут, разрушит все приложение, что не должно произойти продакшине).
Кроме того, обратите внимание, что имя переменного потока совпадает с именем потока переменной цикла, который называется затенением в rust. Это также присутствует в других языках, как javascript.
Протестируем код, выполнив запрос:
curl localhost:8477
Вы, вероятно, должны получить что-то вроде
Connection reset by peer
Это потому, что мы ничего не делаем с соединением.
Создание обработчика соединений
Создание функции с именем handle_connection
для данных потока:
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|res| res.unwrap())
.take_while(|line| !line.is_empty())
.collect();
println!("Request: {:#?}", http_request);
}
О этой функции много можно рассказать, во-первых, ключевое слово mut
сообщает rust, что handle_connection
внесет изменения в поток (записав в него, чего мы сейчас не делаем).
Во второй строке мы создаем новый буфер с изменяемой ссылкой на тот же самый поток, то есть это ссылка кучи на тот же самый объект потока. (Больше пустяков дальше)
У нас есть некоторые простые буферные операции перед печатью самого вектора с помощью {:#?}
это основной способ распечатать вектор в rust, вы также можете использовать {:?}
, который распечатает все значения в одной строке.
Использование обработчика для потоков соединений
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream); // <- использовать здесь
println!("Подключение произошло");
}
Одно важное примечание - Объект потока теперь принадлежит функции handle_connection
, что означает, что на него больше нельзя ссылаться в цикле for
. Если просто попытаться добавить handle_connection(stream);
второй раз это даст вам ошибку во времени компиляции, указывающую, что поток переменных перемещен. Это очень мощный функционал rust, поскольку он упрощает очистку памяти.
Создание HTTP-соединения
Как мы узнали, HTTP - это просто способ определения структуры данных, передаваемых по TCP-соединению. Для получения дополнительной информации см. полное описание в разделе «Request For Comments».
Создание статуса ответа
Если бы вы сделали запрос с URL на локальном хосте (или открыли в браузере), вы бы не получили ответ, потому что мы фактически не отвечаем клиенту, вызывающему наш сервер.
Добавим это после handle_connection
функции:
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write_all(response.as_bytes()).unwrap();
Это стандартные метаданные успеха, здесь сервер сообщает клиенту, что он получил информацию, отправленную клиентом, и все в порядке.
Обратите внимание, что\r\n присутствует в переменной ответа два раза, это стандартный разделитель строк, определенный в протоколе, часто называемый CRLF.
- CR - возврат каретки (
\r
) - LF - переход на новую строку (
\n
)
Давайте отправим html
Создайте файл с именем response.html
. Создайте его в корне приложения, а не внутри src/
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Привет!</title>
</head>
<body>
<h1>Привет!</h1>
<p>Мое имя ведант</p>
</body>
</html>
Замените последние 2 строки в функции handle_connection
на эти:
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("response.html").unwrap();
let length = contents.len();
let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
fs
- модуль, используемый для работы с файловой системой в rust.
Мы используем format!
добавляя содержимое файла в качестве тела ответа об успешном выполнении вместе с длиной содержимого, требуемой протоколом.
Если открыть localhost:8477
в браузере, должно визуализировать HTML-файл.
Обратите внимание, что если вы также откроете какой-либо маршрут в браузере localhost:8477/vedant
например, это вернет вам тот же результат. Это не желаемый результат, так как мы хотим управлять ответом на основании того, по какому маршруту сделан запрос GET
.
Добавление маршрутов
let req_line = buf_reader.lines().next().unwrap().unwrap();
if req_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("response.html").expect("Не удалось открыть файл");
let length = contents.len();
let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Здесь мы извлекаем строку состояния запроса и проверяем его, чтобы узнать, какой маршрут вызывается клиентом.
Если мы сделаем это, соединение останется открытым для любого маршрута, который не является корнем, и будет не иметь никакого ответа, ни успеха, ни неудачи. Давайте добавим 404 для других маршрутов.
Создание файла 404.html:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Привет!</title>
</head>
<body>
<h1>Ну нет!</h1>
<p>Думаю, вы заблудилисьr.</p>
</body>
</html>
Добавить блок else
в функцию handle_request
:
else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
Теперь любой маршрут, не являющийся корневым, приведет к ошибкам с содержимым 404.
Рефакторинг кода
Мы можем переделать вышеуказанную функцию, чтобы бы она была более краткой.
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let req_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = if req_line == "GET / HTTP/1.1" {
("HTTP/1.1 200 OK", "response.html")
} else {
("HTTP/1.1 200 OK", "404.html")
};
let contents = fs::read_to_string(filename).expect("Не удалось открыть файл");
let length = contents.len();
let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
Здесь значения status_line
и имя файла получаются в условии, вроде троичного оператора (но это не точно).