ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1142
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
переменным; затем мы можем безоговорочно использовать эти переменные в коде для чтения файла и записи ответа. В листинге 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();
}
Файл: 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--
}
Этот код немного неряшливый, но он достаточно хорошо подходит для целей имитации.
Мы создали второй запрос sleep
, данные которого распознает сервер. Мы добавили else if после блока if
, чтобы проверить запрос к /sleep. Когда этот запрос будет получен, сервер заснёт на 5 секунд, прежде чем отобразить HTML страницу успешного выполнения.
Можно увидеть, насколько примитивен наш сервер: реальные библиотеки будут обрабатывать распознавание нескольких запросов гораздо менее многословно!
Запустите сервер командой cargo run
. Затем откройте два окна браузера: одно с адресом http://127.0.0.1:7878/, другое с http://127.0.0.1:7878/sleep. Если вы несколько раз обратитесь к URI /, то как и раньше увидите, что сервер быстро ответит. Но если вы введёте URI /sleep, затем загрузите URI /, то увидите что / ждёт, пока
/sleep не отработает полные 5 секунд перед загрузкой страницы.
Есть несколько способов изменить работу нашего веб-сервера, чтобы избежать медленной обработки большого количества запросов из-за одного медленного; способ который мы реализуем является пулом потоков.
Улучшение пропускной способности с помощью пула потоков
Пул потоков является группой заранее порождённых потоков, ожидающих в пуле и готовых выполнить задачу. Когда программа получает новую задачу, она назначает задачу одному из потоков в пуле и этот поток будет обрабатывать задачу. Остальные потоки в пуле доступны для обработки любых других задач, возникающих во время обработки первого потока. Когда первый поток завершает обработку своей задачи, он возвращается в пул свободных потоков, готовых обработать новую задачу. Пул потоков позволяет обрабатывать соединения одновременно, увеличивая пропускную способность вашего сервера.
Мы ограничим число потоков в пуле небольшим числом, чтобы защитить нас от атак типа «отказ в обслуживании» (DoS - Denial of Service); если бы наша программа создавала новый поток в момент поступления каждого запроса, то кто-то сделавший 10 миллионов запросов к серверу, мог бы создать хаос, использовать все ресурсы нашего сервера и остановить обработку запросов.
Вместо порождения неограниченного количества потоков, у нас будет фиксированное количество потоков, ожидающих в пуле. По мере поступления запросов они будут отправляться в пул для обработки. Пул будет поддерживать очередь входящих запросов.
Каждый из потоков в пуле будет извлекать запрос из этой очереди, обрабатывать запрос и затем запрашивать в очереди следующий запрос. При таком дизайне мы можем обрабатывать
N
запросов одновременно, где
N
- количество потоков. Если каждый поток отвечает на длительный запрос, последующие запросы могут по-прежнему задержаться в очереди, но мы увеличили число долго играющих запросов, которые можно обработать до достижения этой точки.
Этот подход является лишь одним из многих способов улучшить пропускную способность веб-сервера. Другими вариантами, которые вы могли бы изучить являются модель fork/join и однопоточная модель асинхронного ввода-вывода. Если вам интересна эта тема, вы можете прочитать о других решениях больше и попробовать внедрить их в помощью Rust. С языком низкого уровня как Rust, возможны все эти варианты.
Прежде чем приступить к реализации пула потоков, давайте поговорим о том, как должно выглядеть использование пула. Когда вы пытаетесь проектировать код, сначала необходимо написать клиентский интерфейс. Напишите API кода, чтобы он был структурирован так, как вы хотите его вызывать, затем реализуйте функциональность данной структуры, вместо подхода реализовывать функционал, а затем разрабатывать общедоступный API.
Подобно тому, как мы использовали разработку через тестирование (test-driven) в проекте главы 12, мы будем использовать здесь разработку, управляемую компилятором
(compiler-driven). Мы напишем код, который вызывает нужные нам функции, а затем посмотрим на ошибки компилятора, чтобы определить, что мы должны изменить дальше, чтобы заставить код работать.
1 ... 54 55 56 57 58 59 60 61 62
Структура кода, если мы могли бы создавать поток для каждого запроса
Сначала давайте рассмотрим, как мог бы выглядеть код, если он создавал бы новый поток для каждого соединения. Как упоминалось ранее, это не окончательный план, а это отправная точка из-за проблем с возможным порождением неограниченного количества потоков. В листинге 20-11 показаны изменения, которые нужно внести в main
, чтобы запускать новый поток для обработки каждого входящего потока соединения в цикле for
Файл: src/main.rs
Листинг 20-11: Порождение нового потока для каждого потока соединения
Как вы изучили в главе 16, thread::spawn создаст новый поток и затем запустит код замыкания в этом новом потоке. Если вы запустите этот код и загрузите /sleep в своём браузере, в затем загрузите / в двух других вкладках браузера, вы действительно увидите,
fn main
() { let listener = TcpListener::bind(
"127.0.0.1:7878"
).unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(|| { handle_connection(stream);
});
}
}
что запросы к / не должны ждать завершения /sleep. Но, как мы уже упоминали, это в конечном счёте перегрузит систему, потому что вы будете создавать новые потоки без каких-либо ограничений.
Создание аналогичного интерфейса для конечного числа потоков
Мы хотим, чтобы наш пул потоков работал аналогичным, знакомым образом, чтобы переключение с потоков на пул потоков не требовало больших изменений в коде использующем наш API. В листинге 20-12 показан гипотетический интерфейс для структуры
ThreadPool
, который мы хотим использовать вместо thread::spawn
Файл: src/main.rs
Листинг 20-12: Наш идеальный интерфейс
ThreadPool
Мы используем
ThreadPool::new
, чтобы создать новый пул потоков с конфигурируемым количеством потоков, в данном случае четыре. Затем в цикле for выполняем pool.execute имеющий интерфейс, аналогичный интерфейсу thread::spawn
, в котором выполняется замыкание, которое пул должен выполнить для каждого потока соединения. Нам нужно реализовать pool.execute
, чтобы он принимал замыкание и передавал его потоку из пула для выполнения. Этот код не компилируется, но мы постараемся, чтобы компилятор в его исправлении.
Создание структуры ThreadPool использованием разработки, управляемой
компилятором
Внесите изменения листинга 20-12 в файл src/main.rs, а затем давайте воспользуемся ошибками компилятора из команды cargo check для управления нашей разработкой.
Вот первая ошибка, которую мы получаем:
fn main
() { let listener = TcpListener::bind(
"127.0.0.1:7878"
).unwrap(); let pool = ThreadPool::new(
4
); for stream in listener.incoming() { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream);
});
}
}
Создание аналогичного интерфейса для конечного числа потоков
Мы хотим, чтобы наш пул потоков работал аналогичным, знакомым образом, чтобы переключение с потоков на пул потоков не требовало больших изменений в коде использующем наш API. В листинге 20-12 показан гипотетический интерфейс для структуры
ThreadPool
, который мы хотим использовать вместо thread::spawn
Файл: src/main.rs
Листинг 20-12: Наш идеальный интерфейс
ThreadPool
Мы используем
ThreadPool::new
, чтобы создать новый пул потоков с конфигурируемым количеством потоков, в данном случае четыре. Затем в цикле for выполняем pool.execute имеющий интерфейс, аналогичный интерфейсу thread::spawn
, в котором выполняется замыкание, которое пул должен выполнить для каждого потока соединения. Нам нужно реализовать pool.execute
, чтобы он принимал замыкание и передавал его потоку из пула для выполнения. Этот код не компилируется, но мы постараемся, чтобы компилятор в его исправлении.
Создание структуры ThreadPool использованием разработки, управляемой
компилятором
Внесите изменения листинга 20-12 в файл src/main.rs, а затем давайте воспользуемся ошибками компилятора из команды cargo check для управления нашей разработкой.
Вот первая ошибка, которую мы получаем:
fn main
() { let listener = TcpListener::bind(
"127.0.0.1:7878"
).unwrap(); let pool = ThreadPool::new(
4
); for stream in listener.incoming() { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream);
});
}
}
Замечательно! Ошибка говорит о том, что нам нужен тип или модуль
ThreadPool
,
поэтому мы создадим его сейчас. Наша реализация
ThreadPool будет зависеть от того,
какую работу выполняет наш веб-сервер. Итак, давайте переделаем крейт hello из бинарного в библиотечный для хранения реализации
ThreadPool
. После того, как поменяем в библиотечный крейт, мы также сможем использовать отдельную библиотеку пула потоков для любой работы, которую мы хотим выполнить с его использованием, а не только для обслуживания веб-запросов.
Создайте файл src/lib.rs, который содержит следующее, что является простейшим определением структуры
ThreadPool
, которую мы можем иметь в данный момент:
Файл: src/lib.rs
Затем создайте новый каталог src/bin и переместите двоичный крейт с корнем в
src/main.rs в src/bin/main.rs. Это сделает библиотечный крейт основным крейтом в каталоге hello; мы все ещё можем запустить двоичный файл из src/bin/main.rs, используя cargo run
. Переместив файл main.rs, отредактируйте его, чтобы подключить крейт библиотеки и добавить тип
ThreadPool в область видимости, добавив следующий код в начало src/bin/main.rs:
Файл: src/bin/main.rs
Этот код по-прежнему не будет работать, но давайте проверим его ещё раз, чтобы получить следующую ошибку, которую нам нужно устранить:
$
cargo check
Checking hello v0.1.0 (file:///projects/hello) error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
-->
src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`. error: could not compile `hello` due to previous error pub struct
ThreadPool
;
{{#rustdoc_include ../listings/ch20-web-server/no-listing-
01
-define-threadpool- struct
/
src
/bin/main.rs:here}}
Эта ошибка указывает, что далее нам нужно создать ассоциированную функцию с именем new для
ThreadPool
. Мы также знаем, что new должен иметь один параметр,
который может принимать
4
в качестве аргумента и должен возвращать экземпляр
ThreadPool
. Давайте реализуем простейшую функцию new
, которая будет иметь эти характеристики:
Файл: src/lib.rs
Мы выбираем usize в качестве типа параметра size
, потому что мы знаем, что отрицательное число потоков не имеет никакого смысла. Мы также знаем, что мы будем использовать число 4 в качестве количества элементов в коллекции потоков, для чего предназначен тип usize
, как обсуждалось в разделе "Целочисленные типы"
главы 3.
Давайте проверим код ещё раз:
Теперь мы получаем предупреждение и ошибку. Игнорируем предупреждение не надолго, ошибка происходит потому что у нас нет метода execute в структуре
ThreadPool
. Вспомните раздел "Создание подобного интерфейса для конечного числа
$
cargo check
Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no function or associated item named `new` found for struct
`ThreadPool` in the current scope
-->
src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in
`ThreadPool`
For more information about this error, try `rustc --explain E0599`. error: could not compile `hello` due to previous error pub struct
ThreadPool
; impl
ThreadPool { pub fn new
(size: usize
) -> ThreadPool {
ThreadPool
}
}
$
cargo check
Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
-->
src/main.rs:17:14
|
17 | pool.execute(|| {
| ^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`. error: could not compile `hello` due to previous error