ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1139
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Попробуем запустить этот код! Вызовите cargo run в терминале, а затем загрузите
127.0.0.1:7878 в веб-браузере. В браузере должно отображаться сообщение об ошибке,
например «Connection reset», поскольку сервер в настоящее время не отправляет обратно никаких данных. Но когда вы посмотрите на свой терминал, вы должны увидеть несколько сообщений, которые были напечатаны, когда браузер подключался к серверу!
Иногда вы видите несколько сообщений, напечатанных для одного запроса браузера;
Причина может заключаться в том, что браузер выполняет запрос страницы, а также других ресурсов, таких как значок favicon.ico, который отображается на вкладке браузера.
Также может быть, что браузер пытается подключиться к серверу несколько раз, потому что сервер не отвечает. Когда stream выходит из области видимости и отбрасывается в конце цикла, соединение закрывается как часть реализации drop
. Браузеры иногда обрабатывают закрытые соединения, повторяя попытки, потому что проблема может быть временной. Важным фактором является то, что мы успешно получили дескриптор
TCP-соединения!
Не забудьте остановить программу, нажав ctrl-c, когда вы закончите запускать определённую версию кода. Затем перезапустите cargo run после того, как вы внесли следующий набор изменений, чтобы убедиться, что вы используете самый новый код.
Чтение запросов
Реализуем функционал чтения запроса из браузера! Чтобы разделить части, связанные с получением соединения и последующим действием с ним, мы запустим новую функцию для обработки соединения. В этой новой функции handle_connection мы будем читать данные из потока TCP и распечатывать их, чтобы мы могли видеть данные,
отправленные из браузера. Измените код, чтобы он выглядел как в листинге 20-2.
Файл: src/main.rs
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
Листинг 20-2: Чтение из потока
TcpStream
и печать данных
Мы добавляем std::io::prelude в область видимости, чтобы получить доступ к определённым свойствам, которые позволяют нам читать и писать в поток. В цикле for функции main вместо вывода сообщения о том, что мы установили соединение, мы теперь вызываем новую функцию handle_connection и передаём ей stream
В функции handle_connection мы сделали параметр stream изменяемым. Причина в том, что экземпляр
TcpStream отслеживает, какие данные он нам возвращает. Он может прочитать больше данных, чем мы запрашивали, и сохранить их для следующего раза,
когда мы запросим данные. Следовательно, он должен быть mut поскольку его внутреннее состояние может измениться; Обычно мы думаем, что «чтение» не требует мутации, но в этом случае нам нужно ключевое слово mut
Далее нам нужно фактически прочитать данные из потока. Мы делаем это в два этапа:
во-первых, мы объявляем buffer в стеке для хранения считываемых данных. Мы сделали буфер размером 1024 байта, что достаточно для хранения данных базового запроса и достаточно для наших целей в этой главе. Если бы мы хотели обрабатывать запросы произвольного размера, управление буфером должно было бы быть более сложным; пока делаем проще. Мы передаём буфер в stream.read
, который считывает байты из
TcpStream и помещает их в буфер.
Во-вторых, мы конвертируем байты из буфера в строку и печатаем эту строку. Функция
String::from_utf8_lossy принимает
&[u8]
и создаёт из неё
String
. Названия «lossy» (с потерями) в её имени указывает на поведение этой функции. Когда она видит 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(); handle_connection(stream);
}
} 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(); println!
(
"Request: {:#?}"
, http_request);
}
недопустимую последовательность UTF-8: она заменяет недопустимую последовательность на символ
�
, символ замены
U+FFFD REPLACEMENT CHARACTER
. Вы могли видеть заменяющие символы в буфере, который не заполнен данными из запроса.
Попробуем этот код! Запустите программу и снова сделайте запрос в веб-браузере.
Обратите внимание, что мы по-прежнему будем получать в браузере страницу с ошибкой, но вывод нашей программы в терминале теперь будет выглядеть примерно так:
В зависимости от вашего браузера результат может немного отличаться. Теперь, когда мы печатаем данные запроса, мы можем понять, почему мы получаем несколько подключений из одного запроса браузера, посмотрев на путь после
Request: GET
. Если все повторяющиеся соединения запрашивают / , мы знаем, что браузер пытается получить / повторно, потому что он не получает ответа от нашей программы.
Давайте разберём эти данные запроса, чтобы понять, что браузер запрашивает у нашей программы.
�
, символ замены
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
Последняя часть - это версия 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}}
который печатал данные запроса и заменял их кодом из Листинга 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
}
}
Проверка запроса и выборочное возвращение ответа
Прямо сейчас наш веб-сервер вернёт 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}}