недопустимую последовательность UTF-8: она заменяет недопустимую последовательность на символ
�
, символ замены
U+FFFD REPLACEMENT CHARACTER
. Вы могли видеть заменяющие символы в буфере, который не заполнен данными из запроса.
Попробуем этот код! Запустите программу и снова сделайте запрос в веб-браузере.
Обратите внимание, что мы по-прежнему будем получать в браузере страницу с ошибкой, но вывод нашей программы в терминале теперь будет выглядеть примерно так:
В зависимости от вашего браузера результат может немного отличаться. Теперь, когда мы печатаем данные запроса, мы можем понять, почему мы получаем несколько подключений из одного запроса браузера, посмотрев на путь после
Request: GET
. Если все повторяющиеся соединения запрашивают
/ , мы знаем, что браузер пытается получить
/ повторно, потому что он не получает ответа от нашей программы.
Давайте разберём эти данные запроса, чтобы понять, что браузер запрашивает у нашей программы.
1 ... 52 53 54 55 56 57 58 59 ... 62
Пристальный взгляд на HTTP запросHTTP - это текстовый протокол и запрос имеет следующий формат:Первая строка - это
строка запроса, содержащая информацию о том, что запрашивает клиент. Первая часть строки запроса указывает используемый
метод, например GET илиPOST, который описывает, как клиент выполняет этот запрос. Наш клиент использовал запрос GETСледующая часть строки запроса - это
/, которая указывает
унифицированныйидентификаторресурса (URI), который запрашивает клиент: URI почти, но не совсем то$ 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 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 ������������������������������������Method Request-URI HTTP-Version CRLF headers CRLF message-body
же самое, что и унифицированный указатель ресурса (URL). Разница между URI и URL- адресами не важна для наших целей в этой главе, но спецификация HTTP использует термин URI, поэтому мы можем просто мысленно заменить URL-адрес здесь.
Последняя часть - это версия HTTP, которую использует клиент, а затем строка запроса заканчивается последовательностью CRLF . (CRLF обозначает возврат каретки и перевод
строки , что является термином из дней пишущих машинок!) Последовательность CRLF
также может быть записана как
\r\n
, где
\r
- возврат каретки, а
\n
- перевод строки.
Последовательность CRLF отделяет строку запроса от остальных данных запроса.
Обратите внимание, что при печати CRLF мы видим начало новой строки, а не
\r\n
Глядя на данные строки запроса, которые мы получили от запуска нашей программы, мы видим, что
GET
- это метод, / - это URI запроса, а
HTTP/1.1
- это версия.
После строки запроса оставшиеся строки, начиная с
Host:
далее, являются заголовками.
GET
запросы не имеют тела.
Попробуйте сделать запрос из другого браузера или запросить другой адрес, например
127.0.0.1:7878/test , чтобы увидеть, как изменяются данные запроса.
Теперь, когда мы знаем, что запрашивает браузер, давайте отправим обратно в ответ некоторые данные!
Написание ответа
Теперь мы реализуем отправку данных в ответ на запрос клиента. Ответы имеют следующий формат:
Первая строка - это строка состояния, которая содержит версию HTTP, используемую в ответе, числовой код состояния, который суммирует результат запроса, и фразу причины, которая предоставляет текстовое описание кода состояния. После последовательности CRLF идут любые заголовки, другая последовательность CRLF и тело ответа.
Вот пример ответа, который использует HTTP версии 1.1, имеет код состояния 200, фразу причины OK, без заголовков и без тела:
Код состояния 200 - это стандартный успешный ответ. Текст представляет собой крошечный успешный HTTP-ответ. Давайте запишем это в поток как наш ответ на
HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body
HTTP/1.1 200 OK\r\n\r\n
успешный запрос! Из функции handle_connection удалите println!
который печатал данные запроса и заменял их кодом из Листинга 20-3.
Файл: src/main.rs
Листинг 20-3: Запись короткого успешного HTTP ответа в поток
Первая новая строка определяет переменную response которая содержит данные сообщения об успешном выполнении. Затем мы вызываем as_bytes в нашем response чтобы преобразовать строковые данные в байты. Метод write в stream принимает
&
[u8]
и отправляет эти байты напрямую по соединению.
Поскольку операция write может завершиться неудачно, мы, как и раньше, используем unwrap для любого результата ошибки. Опять же, в реальном приложении вы бы добавили сюда обработку ошибок. Наконец, flush подождёт и предотвратит продолжение программы, пока все байты не будут записаны в соединение;
TcpStream содержит внутренний буфер для минимизации обращений к базовой операционной системе.
Сделав этим изменения давайте запустим код и сделаем запрос. Мы больше не выводим в терминал любые данные, поэтому мы не увидим ничего, кроме вывода из Cargo. Когда вы загружаете адрес 127.0.0.1:7878 в веб-браузер, вы должны получить пустую страницу вместо ошибки. Вы только что вручную закодировали запрос и ответ HTTP!
Возвращение реального HTML
Реализуем функционал для возврата более пустой страницы. Создайте новый файл
hello.html в корне каталога вашего проекта, а не в каталоге src . Вы можете ввести любой
HTML-код; В листинге 20-4 показана одна возможность.
Файл: hello.html
Листинг 20-4. Образец HTML-файла для возврата в ответ
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 response =
"HTTP/1.1 200 OK\r\n\r\n"
; stream.write_all(response.as_bytes()).unwrap();
}
{{#include ../listings/ch20-web-server/listing-20-04/hello.html}}
Это минимальный документ HTML5 с заголовком и некоторым текстом. Чтобы вернуть это с сервера при получении запроса, мы handle_connection как показано в листинге 20-
5, чтобы прочитать файл HTML, добавить его в ответ в виде тела и отправить.
Файл: src/main.rs
Листинг 20-5. Отправка содержимого hello.html в качестве тела ответа
Мы добавили строку вверху, чтобы включить в область видимости модуль файловой системы стандартной библиотеки. Код для чтения содержимого файла в строку должен выглядеть знакомо; мы использовали его в главе 12, когда читали содержимое файла для нашего проекта ввода-вывода в листинге 12-4.
Далее мы используем format!
чтобы добавить содержимое файла в качестве тела ответа об успешном завершении. Чтобы гарантировать действительный HTTP-ответ, мы добавляем заголовок
Content-Length который имеет размер тела нашего ответа, в данном случае размер hello.html
Запустите этот код командой cargo run и загрузите 127.0.0.1:7878 в браузере; вы должны увидеть выведенный HTML в браузере!
В настоящее время мы игнорируем данные запроса в buffer и просто безоговорочно отправляем обратно содержимое HTML-файла. Это означает, что если вы попытаетесь запросить 127.0.0.1:7878/something-else в своём браузере, вы все равно получите тот же ответ HTML. Наш сервер очень ограничен, и это не то, что делает большинство веб- use std::{ 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(); let response = format!
(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
); stream.write_all(response.as_bytes()).unwrap();
}
серверов. Мы хотим настроить наши ответы в зависимости от запроса и отправлять обратно HTML-файл только для правильно сформированного запроса в / .
Проверка запроса и выборочное возвращение ответа
Прямо сейчас наш веб-сервер вернёт HTML-код в файле независимо от того, что запросил клиент. Давайте добавим функциональность, чтобы проверять, запрашивает ли браузер / перед возвратом HTML-файла, и возвращать ошибку, если браузер запрашивает что-либо ещё. Для этого нам нужно изменить handle_connection
, как показано в листинге 20-6. Этот новый код проверяет содержимое полученного запроса на соответствие тому, как мы знаем, что запрос на / выглядит как, и добавляет блоки if и else чтобы обрабатывать запросы по-разному.
Файл: src/main.rs
Листинг 20-6: Сопоставление запроса и обработка запросов для корневого ресурса /, отличающимся от
запросов других ресурсов
Сначала мы жёстко кодируем данные, соответствующие запросу /, в переменную get
Поскольку мы читаем необработанные байты в буфер, мы преобразуем get в байтовую строку, добавляя синтаксис байтовой строки b""
в начало данных содержимого. Затем мы проверяем, начинается ли buffer с байтов в get
. Если это так, это означает, что мы получили правильно сформированный запрос к / , и это успешный случай, который мы обработаем в блоке if который возвращает содержимое нашего HTML-файла.
Если buffer
не начинается с байтов в get
, это означает, что мы получили другой запрос. Мы добавим код в блок else через мгновение, чтобы ответить на все остальные запросы.
// --snip-- fn handle_connection
(
mut stream: TcpStream) { let buf_reader = BufReader::new(&
mut stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); 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\nContent-Length: {length}\r\n\r\n{contents}"
); stream.write_all(response.as_bytes()).unwrap();
} else
{
// some other request
}
}
Запустите этот код сейчас и запросите 127.0.0.1:7878 ; вы должны получить HTML в
hello.html . Если вы сделаете любой другой запрос, например 127.0.0.1:7878/something-else ,
вы получите ошибку соединения, подобную той, которую вы видели при запуске кода из
Листинга 20-1 и Листинга 20-2.
Теперь давайте добавим код из листинга 20-7 в блок else чтобы вернуть ответ с кодом состояния 404, который сигнализирует о том, что контент для запроса не найден. Мы также вернём HTML-код для страницы, отображаемой в браузере, с указанием ответа конечному пользователю.
Файл: src/main.rs
Листинг 20-7. Ответ с кодом состояния 404 и страницей с ошибкой, если было запрошено что-либо, кроме /
Здесь ответ имеет строку состояния с кодом 404 и фразу причины
NOT FOUND
. Тело ответа будет HTML из файла 404.html. Вам нужно создать файл 404.html рядом с hello.html для этой страницы ошибки; снова не стесняйтесь использовать любой HTML код или пример
HTML кода в листинге 20-8.
Файл: 404.html
Листинг 20-8. Пример содержимого страницы для отправки с любым ответом 404
С этими изменениями снова запустите сервер. Запрос на 127.0.0.1:7878 должен возвращать содержимое hello.html, и любой другой запрос, как 127.0.0.1:7878/foo, должен возвращать сообщение об ошибке HTML от 404.html.
Рефакторинг
В настоящий момент блоки if и else часто повторяются: они читают файлы и записывают содержимое файлов в поток. Единственные различия - это строка состояния и имя файла. Давайте сделаем код более кратким, выделив эти различия в отдельные строки if и else которые будут назначать значения строки состояния и имени файла
// --snip--
} 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();
}
{{#include ../listings/ch20-web-server/listing-20-08/404.html}}
переменным; затем мы можем безоговорочно использовать эти переменные в коде для чтения файла и записи ответа. В листинге 20-9 показан код, полученный после замены больших блоков if и else
Файл: src/main.rs
Листинг 20-9. Реорганизация блоков
if
и
else
чтобы они содержали только код, который отличается в
двух случаях.
Теперь блоки if и else возвращают только соответствующие значения для строки состояния и имени файла в кортеже; Затем мы используем деструктурирование, чтобы присвоить эти два значения status_line и filename используя шаблон в операторе let
, как обсуждалось в главе 18.
Ранее дублированный код теперь находится вне блоков if и else и использует переменные status_line и filename
. Это позволяет легче увидеть разницу между этими двумя случаями и означает, что у нас есть только одно место для обновления кода, если захотим изменить работу чтения файлов и записи ответов. Поведение кода в листинге
20-9 будет таким же, как и в 20-8.
Потрясающие! Теперь у нас есть простой веб-сервер примерно на 40 строках кода Rust,
который отвечает на один запрос страницей с контентом и отвечает на все остальные запросы ответом 404.
В настоящее время наш сервер работает в одном потоке, что означает, что он может обслуживать только один запрос за раз. Давайте посмотрим, как это может быть проблемой, смоделировав несколько медленных запросов. Затем мы исправим это,
чтобы наш сервер мог обрабатывать несколько запросов одновременно.
// --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\nContent-Length: {length}\r\n\r\n{contents}"
); stream.write_all(response.as_bytes()).unwrap();
}
Превращение однопоточного сервера в
многопоточный сервер
Прямо сейчас сервер будет обрабатывать каждый запрос в очереди, что означает, что он не будет обрабатывать второе соединение, пока первое не завершит обработку. Если бы сервер получал все больше и больше запросов, это последовательное выполнение было бы все менее и менее оптимальным. Если сервер получает какой-то запрос, обработка которого занимает слишком много времени, то последующие запросы должны будут ждать завершения обработки длительного запроса, даже если эти новые запросы могут быть обработаны гораздо быстрее. Нам нужно это исправить, но сначала мы рассмотрим проблему в действии.
Имитация медленного запроса в текущей реализации сервера
Мы посмотрим, как запрос с медленной обработкой может повлиять на другие запросы,
сделанные к серверу в текущей реализации. В листинге 20-10 реализована обработка запроса к ресурсу /sleep с эмуляцией медленного ответа, который заставит сервер не работать в течение 5 секунд перед ответом.
Файл: src/main.rs
Листинг 20-10: Имитация медленного запроса путём распознавания обращения к /sleep и засыпанию на 5
секунд
use std::{ fs, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, thread, time::Duration,
};
// --snip-- fn handle_connection
(
mut stream: TcpStream) {
// --snip-- let
(status_line, filename) = match
&request_line[..] {
"GET / HTTP/1.1"
=> (
"HTTP/1.1 200 OK"
,
"hello.html"
),
"GET /sleep HTTP/1.1"
=> { thread::sleep(Duration::from_secs(
5
));
(
"HTTP/1.1 200 OK"
,
"hello.html"
)
}
_ => (
"HTTP/1.1 404 NOT FOUND"
,
"404.html"
),
};
// --snip--
}