Збірка Однопотокового Вебсервера
Ми розпочнемо з запуску однопотокового вебсервера. Перш ніж почати, розгляньмо короткий огляд протоколів, залучених до створення вебсерверів. Деталі цих протоколів лежать поза межами цієї книги, але короткий огляд надасть вам потрібну інформацію.
Два основні протоколи, залучені у вебсерверах, це Протокол передачі гіпертексту (HTTP) і Протокол керування передаванням (TCP). Обидва протоколи є протоколами відповіді на запит, тобто клієнт ініціює запити, а сервер слухає запити та надає відповідь клієнту. Вміст цих запитів та відповідей визначається протоколами.
TCP - це протокол нижчого рівня, який описує деталі того, як інформація дістається від одного сервера до іншого, але не вказує, що це за інформація. HTTP є надбудовою над TCP і визначає зміст запитів та відповідей. Технічно можливо використовувати HTTP з іншими протоколами, але переважній більшості випадків HTTP відправляє його дані через TCP. Ми працюватимемо з необробленими байтами TCP та запитами і відповідями HTTP.
Прослуховування З'єднання TCP
Наш вебсервер має прослуховувати TCP-з'єднання, тож це буде першою частиною над якою ми працюватимемо. Стандартна бібліотека надає модуль std::net
, який дозволить нам це зробити. Створімо новий проєкт у звичний спосіб:
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
Тепер введіть код з Блока коду 20-1 у src/main.rs для початку. Цей код прослуховує локальну адресу 127.0.0.1:7878
на вхідні потоки TCP. Коли він отримує вхідний потік, то виведе Connection established!
.
Файл: src/main.rs
use std::net::TcpListener; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println!("Connection established!"); } }
За допомогою TcpListener
ми можемо прослуховувати TCP-з'єднання за адресою 127.0.0.1:7878
. У адресі розділ перед двокрапкою є IP-адресою, що представляє ваш комп'ютер (вона однакова для всіх комп'ютерів і не представляє конкретно комп'ютер автора), а 7878
- порт. Ми обрали цей порт з двох причин: HTTP зазвичай не приймається на цьому порті, тож наш сервер навряд чи конфліктуватиме з іншим вебсервером, що може працювати на вашій машині, і 7878 - це rust, набране на кнопках телефона.
Функція bind
у цьому сценарії працює як функція new
функція в тому, що повертає новий екземпляр TcpListener
. Функція називається bind
тому, що в організації мережі підключення до порту для прослуховування відоме як "прив'язування до порту."
Функція bind
повертає Result<T, E>
, що позначає, що зв'язування може бути невдалим. Наприклад, для підключення до порту 80 потрібні права адміністратора (не-адміністратори можуть слухати лише порти вище ніж 1023), так що, якщо ми намагались під'єднатися до порту 80 без прав адміністратора, зв'язування не спрацює. Зв'язування також не спрацює, наприклад, якщо ми запустимо два екземпляри нашої програми і відтак матимемо дві програми, що слухають один порт. Оскільки ми пишемо базовий сервер лише для навчальних цілей, ми не турбуватимемося про обробку таких помилок; натомість, ми використаємо unwrap
, щоб зупинити програму, якщо виникнуть помилки.
Метод incoming
для TcpListener
повертає ітератор, який дає нам послідовність потоків (точніше, потоків типу TcpStream
). Кожен stream представляє відкрите з'єднання між клієнтом і сервером. З'єднання connection є назвою для усього процесу запиту та відповіді, в якому клієнт підключається до сервера, сервер генерує відповідь, і сервер же закриває з'єднання. Таким чином ми читатимемо з TcpStream
, щоб побачити, що надіслав клієнт, а потім писатимемо нашу відповідь до потоку, щоб відправити дані назад до клієнта. Загалом, цей циклу for
буде обробляти кожне підключення по черзі і створить ряд потоків, які ми оброблятимемо.
Наразі наша обробка потоку складається з виклику unwrap
для припинення нашої програми, якщо потік має будь-які помилки; якщо помилок немає, програма виводить повідомлення. У наступному блоці коду ми додамо більше функціональності для варіанту вдалого з'єднання. Причина, з якої ми можемо отримувати помилки з методу incoming
, коли клієнт підключається до сервера це те, що ми насправді ітеруємо не по з'єднаннях. Натомість ми ітеруємо по спробах з'єднання. З'єднання може бути невдалим з ряду причин, багато з них специфічні для різних операційних систем. Наприклад, багато операційних систем мають обмеження на кількість одночасних відкритих підключень, які вони можуть підтримувати; нове спроба підключення після цієї кількості призводитиме до помилки, поки якісь з відкритих підключень не закриються.
Спробуймо запустити цей код! Викличте cargo run
у терміналі та завантажите 127.0.0.1:7878 у веббраузері. Браузер повинен показати повідомлення про помилку на кшталт "З'єднання скинуто", оскільки сервер поки що не надсилає жодних даних. Але поглянувши в термінал, ви маєте побачити кілька повідомлень, які ми виводимо, коли браузер з'єднується із сервером!
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
Іноді ви бачитимете кілька виведених повідомлень на один запит браузера; причина може бути в тому, що браузер запитує сторінку, а також деякі інші ресурси на кшталт піктограми favicon.ico, що показується у вкладці браузера.
Також можливо, що браузер намагається з'єднатися із сервером багато разів, бо сервер не надіслав у відповідь жодних даних. Коли stream
виходить з області видимості й очищується в кінці циклу, з'єднання закривається, бо це є частиною реалізації drop
. Браузери іноді намагаються повторно з'єднатися із закритими підключеннями, оскільки проблема може бути тимчасовою. Але важливим тут є те, що ми успішно отримали TCP-з'єднання!
Не забудьте зупинити програму, натиснувши ctrl-c, коли ви закінчили працювати з певною версією коду. Потім перезапустіть програму, запустивши команду cargo run
після того, як робите кожен набір змін у коді для того, щоб переконатися, що у вас працює найновіший код.
Читання Запиту
А тепер реалізуймо функціональність для читання запиту з браузера! Для поділу інтересів - спершу встановлення з'єднання, а потім вживання якихось дій зі з'єднанням, ми почнемо нову функцію для обробки з'єднань. У цій новій функції handle_connection
ми прочитаємо дані з потоку TCP і виведемо їх, щоб ми могли побачити дані. що пересилаються з браузера. Змініть код, щоб він виглядав як у Блоці коду 20-2.
Файл: src/lib.rs
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); }
Ми вносимо std::io::prelude
і std::io::BufReader
до області видимості, щоб отримати доступ до трейтів і типів, що дозволяють нам читати і писати до потоку. У циклі for
у функції main
замість того, щоб виводити повідомлення про те, що ми встановили з'єднання, тепер ми викликаємо нову функцію handle_connection
і передаємо їй stream
.
У функції handle_connection
ми створюємо новий екземпляр BufReader
, який огортає мутабельне посилання на stream
. BufReader
додає буферизацію, керуючи викликами до трейтових методів std::io::Read
замість нас.
Ми створюємо змінну з назвою http_request
для збору рядків запиту, що браузер відправляє на наш сервер. Ми позначаємо, що хочемо зібрати ці рядки у вектор, додавши анотацію типу Vec<_>
.
BufReader
реалізує трейт std::io::BufRead
, що надає метод lines
. Метод lines
повертає ітератор Result<String, std::io::Error>
, розділяючи потік даних кожного разу, коли він бачить байт нового рядка. Щоб отримати кожен String
, ми відображаємо і робимо unwrap
для кожного Result
. Result
може бути помилкою, якщо дані не є коректним UTF-8 або виникли проблеми із читанням з потоку. Знову ж таки, готова програма повинна обробляти ці помилки більш майстерно, але для простоти ми просто зупиняємо програму у випадку помилки.
Браузер сигналізує про кінець запиту на HTTP, надіславши поспіль два символи нового рядка, тож щоб отримати один запит з потоку, ми беремо рядки, доки не отримаємо рядок, що є порожньою стрічкою. Коли ми зберемо рядки у вектор, ми виводимо їх за допомогою гарного форматування для налагодження, щоб ми могли подивитися на інструкції, що веббраузер надсилає на наш сервер.
Спробуймо цей код! Запустіть програму і знову зробіть запит у веббраузері. Зверніть увагу, що ми все ще бачимо сторінку помилку в браузері, але виведення від нашої програми в термінал виглядатиме тепер схожим на це:
$ 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 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
Залежно від вашого браузера ви можете отримати трохи інше виведення. Тепер, коли ми виводимо дані запиту, ми бачимо, чому ми отримуємо декілька підключень з одного запиту до браузера, дивлячись на шлях за GET
в першому рядку запиту. Якщо повторні з'єднання всі запитують /, то ми знатимемо, що браузер повторно намагається отримати /, бо не отримує відповіді від нашої програми.
Розберімо дані цього запиту, щоб зрозуміти, що саме браузер запитує в нашої програми.
Докладніший Розгляд HTTP-Запиту
HTTP - це протокол на основі тексту і запит використовує такий формат:
Метод URI-запит HTTP-версія CRLF
заголовки CRLF
тіло повідомлення
Перший рядок це рядок рядок запиту, який містить інформацію про те, що саме клієнт запитує. Перша частина рядка запиту позначає на метод, наприклад GET
чи POST
, який описує як клієнт робить цей запит. Наш клієнт використав запит GET
, що означає, що він запитує інформацію.
Наступна частина рядка запиту - це /, що є уніфікованим ідентифікатором ресурсу (Uniform Resource Identifier, URI), який запитує клієнт: URI це майже те, хоча й не зовсім, що й уніфікований локатор ресурсу (Uniform Resource Locator, URL). Різниця між URI і URL не є важливою для наших цілей у цьому розділі, але специфікація HTTP використовує термін URI, тому ми тут можемо просто думати про URL замість URI.
Остання частина - це версія HTTP, яку використовує клієнт, а потім рядок запиту закінчується послідовністю CRLF. (CRLF означає повернення каретки і зміна рядка, тобто терміни з часів друкарських машинок!) Послідовність CRLF також записується як \r\n
, де\r
- повернення каретки, а \n
- зміна рядка. Послідовність CRLF відділяє рядок запиту від решти даних запиту. Зверніть увагу, що коли виводиться CRLF, ми бачимо початок нового рядка, а не \r\n
.
Дивлячись на дані рядка запиту, який ми отримали, запустивши нашу програму, ми бачимо, що GET
- це метод, / - URI запиту і HTTP/1.1
- це версія.
Рядки після рядка запиту, починаючи від Host:
і далі - це заголовки. Запити GET
не мають тіла.
Спробуйте запит з іншого браузера або запросіть іншу адресу, наприклад, 127.0.0.1:78/test, щоб побачити, як змінюються дані запиту.
Тепер, коли ми знаємо, що браузер запитує, спробуймо відправити трохи даних у відповідь!
Написання Відповіді
Ми збираємось реалізувати виправляння даних у відповідь на запит клієнта. Відповіді мають такий формат:
HTTP-версія статус-код фраза-прояснення CRLF
заголовки CRLF
тіло повідомлення
Перший рядок - це рядок стану, що містить версію HTTP, використану у відповіді, числовий код стану, що підсумовує результат запиту, і фразу-пояснення з текстовим описом коду статусу. Після послідовності CRLF ідуть заголовками, ще одна послідовність CRLF та тіло відповіді.
Ось приклад відповіді, що використовує HTTP версії 1.1, має код стану 200, фразу-пояснення OK, без заголовків і без тіла:
HTTP/1.1 200 OK\r\n\r\n
Код стану 200 це стандартна відповідь про успіх. Цей текст є крихітною успішною відповіддю HTTP. Запишімо її в потік, як нашу відповідь на успішний запит! З функції handle_connection
видалімо println!
, який друкував дані запиту, і замінімо їх кодом з Блоку коду 20-3.
Файл: src/main.rs
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(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); }
Перший новий рядок визначає змінну response
, яка містить дані повідомлення про успіх. Потім ми викликаємо as_bytes
для response
, щоб перетворити стрічку даних на байти. Метод write_all
для stream
приймає &[u8]
і відправляє ці байти безпосередньо у з'єднання. Оскільки операція write_all
можуть бути невдалою, ми застосовуємо unwrap
для будь-яких помилок, як і раніше. Знову ж таки в реальній програмі ви маєте додати тут обробку помилок.
Змінивши так код, запустімо його і зробимо запит. Ми більше не виводимо жодних даних до термінала, тому не побачимо нічого крім того, що виведе Cargo. При завантаженні 127.0.0.1:7878 у веббраузері ви маєте отримати порожню сторінку замість помилки. Ви щойно своїми руками закодували отримання запиту HTTP і відправлення відповіді!
Повертаємо Справжній HTML
Реалізуймо функціональність для повернення чогось більшого за порожню сторінку. Створіть новий файл hello.html у кореневій теці вашого проєкту, а не в теці src. Ви можете ввести будь-який HTML за вашим бажанням; Блок коду 20-4 показує одну з можливостей.
Файл: hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
Це мінімальний документ HTML5 із заголовком та текстом. Щоб сервер повернув це після отримання запиту, ми змінимо функцію handle_connection
, як показано у Блоці коду 20-5, щоб вона читала HTML файл, додавала його до відповіді як тіло і відправляла його.
Файл: src/main.rs
use std::{ fs, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; // --snip-- 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(); 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(); }
Ми додали fs
в інструкцію use
, щоб ввести в область видимості модуль файлової системи зі стандартної бібліотеки. Код для читання вмісту файлу до стрічки має бути вам знайомим; ми використовували його в Розділі 12, коли читали вміст файлу для нашого проєкту I/O в Блоці коду 12-4.
Далі, ми використовуємо format!
, щоб додати вміст файлу як тіло успішної відповіді. Для забезпечення коректної HTTP відповіді ми додаємо заголовок Content-Length
, встановлений у розмір тіла нашої відповіді, у цьому випадку розмір hello.html
.
Запустіть цей код за допомогою cargo run
і завантажте 127.0.0.1:78 у браузері; ви повинні побачити зображеним свій HTML!
Наразі ми ігноруємо дані запиту у http_request
і лише безумовно відправляємо у відповідь вміст HTML файлу. Це означає, що якщо ви спробуєте запитати 127.0.0.1:7878/something-else у своєму браузері, то все одно отримаєте ту ж саму HTML відповідь. На зараз наш сервер украй обмежений і не робить того, що робить більшість вебсерверів. Ми хочемо налаштувати наші відповіді залежно від запиту і відправляти назад HTML файл лише для правильного сформованого запиту /.
Перевірка Запиту і Вибіркова Відповідь
Зараз наш вебсервер поверне HTML з файлу, незалежно від того, що клієнт запитував. Додамо функціональність для перевірки, чи браузер запитує /, перед поверненням HTML файлу і повертатимемо помилку, якщо браузер запитав щось інше. Для цього нам потрібно змінити handle_connection
, як показано у Блоці коду 20-6. Цей новий код порівнює вміст отриманого запиту із тим, як, як ми знаємо, має виглядати запит до /, і додає блоки if
та else
, щоб нарізно обробляти запити.
Файл: src/main.rs
use std::{ fs, 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); } } // --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 } }
Ми збираємося проглядати лише перший рядок HTTP запиту, тож замість зчитувати весь запит у вектор, ви викликаємо next
, щоб отримати перший елемент з ітератора. Перший unwrap
обробляє Option
і зупиняє програму, якщо ітератор не має елементів. Другий unwrap
обробляє Result
і має такий самий ефект, що й unwrap
, який був у map
, доданому в Блоці коду 20-2.
Далі ми перевіряємо, чи request_line
дорівнює рядку запиту для запиту GET до шляху /. Якщо це так, блок if
поверне вміст нашого HTML файлу.
Якщо request_line
не дорівнює GET запиту до шляху /, це означає, що ми отримали якийсь інший запит. Ми додамо код блоку else
, щоб відповісти на всі інші запити, за хвилинку.
Запустіть цей код і запросіть 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
use std::{ fs, 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 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(); // --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(); } }
Тут наша відповідь має рядок стану з кодом стану 404 і фразу-пояснення NOT FOUND
. Тіло відповіді буде HTML з файлу 404.html. Вам треба створити файл 404.html поруч із hello.html для сторінки помилки; знову ж можете використати будь-який HTML, який бажаєте, чи зразок HTML з Блоку коду 20-8.
Файл: 404.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
Після цих змін запустіть ваш сервер знову. Запит 127.0.0.1:7878 повинен повернути вміст hello.html, а будь-який інший запит, наприклад 127.0.0.1:7878/foo, повинен повернути HTML помилки з 404.html.
Трохи Рефакторингу
На цей момент блоки if
та else
мають багато повторень: обидва читають файли і записують вміст файлів до потоку. Єдиною відмінністю є рядок стану й ім'я файлу. Зробімо код виразнішим, витягши ці відмінності в окремі рядки if
та else
, які присвоять значення рядка стану та імені файлу змінним; тоді ми можемо використати ці змінні безумовно в коді, щоб прочитати файл і записати відповідь. Блок коду 20-9 показує отриманий код після заміни великих блоків if
та else
.
Файл: src/main.rs
use std::{ fs, 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); } } // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- let buf_reader = BufReader::new(&mut stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); 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(); }
Тепер блоки if
та else
лише повертають відповідні значення для рядка стану й імені файлу в кортежі; далі ми використовуємо деструктуризацію, щоб присвоїти ці два значення змінним status_line
і filename
скориставшись шаблоном в інструкції let
, як пояснювалося в Розділі 18.
Цей раніше дубльований код знаходиться поза межами блоків if
та else
і використовує змінні status_line
і filename
. Це дає змогу легше бачити відмінності між двома випадками, і це означає, що у нас є тільки одне місце, щоб змінити код, якщо ми хочемо змінити, як працює читання файлів чи відправлення відповіді. Поведінка коду у Блоці коду 20-9 буде такою ж, як у Блоці коду 20-8.
Блискуче! Тепер ми маємо простий вебсервер з приблизно 40 рядків коду на Rust, що відповідає на один запит сторінкою з вмістом і на всі інші запити відповіддю 404.
Наразі наш сервер працює в одному потоці, тобто він може обслуговувати лише один запит за раз. Дослідимо, чому це може бути проблемою, симулюючи повільні запити. Тоді ми полагодимо цю проблему, щоб наш сервер міг обробляти багато запитів одночасно.