1073 lines
62 KiB
Markdown
1073 lines
62 KiB
Markdown
# Содержание
|
||
|
||
<!-- toc -->
|
||
|
||
- [Введение](#%D0%B2%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5)
|
||
- [TCP и UDP](#tcp-%D0%B8-udp)
|
||
* [Transmission Control Protocol — TCP](#transmission-control-protocol--tcp)
|
||
* [User Datagram Protocol — UDP](#user-datagram-protocol--udp)
|
||
- [Самый простой сервер](#%D1%81%D0%B0%D0%BC%D1%8B%D0%B9-%D0%BF%D1%80%D0%BE%D1%81%D1%82%D0%BE%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80)
|
||
- [Прощаемся с синхронностью](#%D0%BF%D1%80%D0%BE%D1%89%D0%B0%D0%B5%D0%BC%D1%81%D1%8F-%D1%81-%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D1%8C%D1%8E)
|
||
- [Асинхронный TCP-сервер](#%D0%B0%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B9-tcp-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80)
|
||
- [Обработка ошибок](#%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D0%BE%D1%88%D0%B8%D0%B1%D0%BE%D0%BA)
|
||
* [Синхронные функции](#%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8)
|
||
+ [Пример. Исключения](#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D1%8F)
|
||
+ [Пример. Возвращаемое значение](#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B2%D0%BE%D0%B7%D0%B2%D1%80%D0%B0%D1%89%D0%B0%D0%B5%D0%BC%D0%BE%D0%B5-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5)
|
||
* [Асинхронные функции](#%D0%B0%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8)
|
||
* [error_code](#error_code)
|
||
- [Дальнейшее изучение](#%D0%B4%D0%B0%D0%BB%D1%8C%D0%BD%D0%B5%D0%B9%D1%88%D0%B5%D0%B5-%D0%B8%D0%B7%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5)
|
||
- [TCP чат-сервер](#tcp-%D1%87%D0%B0%D1%82-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80)
|
||
- [Упрощаем код](#%D1%83%D0%BF%D1%80%D0%BE%D1%89%D0%B0%D0%B5%D0%BC-%D0%BA%D0%BE%D0%B4)
|
||
|
||
<!-- tocstop -->
|
||
|
||
# Введение
|
||
|
||
Данное руководство посвящено работе асинхронного ввода-вывода, который в
|
||
основном используется для сетевого взаимодействия. Для лучшего понимания
|
||
происходящего, вы должны быть знакомы с современным C++, STL и Boost, а также
|
||
с базовыми принципами сетевого взаимодействия и многопоточности.
|
||
|
||
Мы будем использовать `Boost.Asio`, `Boost.Beast`, а также `C++20 Networking
|
||
library`. Чтобы добиться асинхронности, мы будем использовать _обработчики
|
||
завершения_, _сопрограммы_ (или корутины) и _фиберы_.
|
||
|
||
Чтобы скомпилировать исходный код из примеров, вам понадобиться установить
|
||
компилятор, поддерживающий стандарт `C++17`, а также библиотеку `Boost`. При
|
||
компиляции вам потребуется добавить `Boost` в `include directories` и
|
||
слинковать исходный код вашего приложения с ним.
|
||
|
||
На самом деле, для большинства примеров достаточно скомпилировать
|
||
`boost/libs/system/src/error_code.cpp`, поскольку остальная часть исходного
|
||
кода библиотеки `Boost` — это header-only библиотеки.
|
||
|
||
Обычно сетевое взаимодействие считается очень сложным предметом для изучения.
|
||
Неужели это действительно так сложно? Что ж, ответ — и да, и нет. Потребуется
|
||
время, чтобы стать экспертом в этой области, однако мы попробуем сделать так,
|
||
чтобы вам было понятно то, что происходит в этом руководстве.
|
||
|
||
Когда вы разрабатываете какое-либо приложение, вам следует использовать
|
||
пространства имен (namespaces) и псевдонимы типов (type aliases), чтобы код
|
||
было удобно читать. Мы начнем это делать позднее, после того, как у вас
|
||
появится четкое понимание откуда берутся те или иные вещи. Поэтому первое
|
||
время вы будете видеть что-то по типу `boost::asio::ip::tcp::socket`. Конечно
|
||
же, в реальном коде это должно быть заменено на что-то вроде `tcp::socket`.
|
||
|
||
Двумя важными элементами сетевого взаимодействия являются _клиенты_ и
|
||
_серверы_. Обычно подобные руководства начинаются с изучения работы клиента,
|
||
поскольку это более простая тема для рассмотрения. Однако в этом руководстве
|
||
мы начнем с серверов. Почему? Во-первых, сервера — это то место, где C++
|
||
проявляет себя с наилучшей стороны, а во-вторых, сервера не так страшны, как
|
||
кажутся на первый взгляд.
|
||
|
||
На этом вступление окончено. Теперь вы готовы приступить к погружению в
|
||
сетевое программирование на C++.
|
||
|
||
|
||
# TCP и UDP
|
||
|
||
|
||
Существует два основных протокола транспортного уровня, которые мы будем
|
||
использовать — TCP и UDP. Протокол — это набор соглашений о том, как должны
|
||
передаваться данные по сети.
|
||
|
||
|
||
## Transmission Control Protocol — TCP
|
||
|
||
|
||
TCP-соединение очень похоже на файл: мы открываем его, считываем из него
|
||
какие-то данные, записываем какие-то данные и закрываем его. Однако
|
||
существуют некоторые ограничения:
|
||
- При работе с файлом мы можем узнать его размер. В случае TCP-соединения
|
||
это невозможно.
|
||
- Вы можете изменять положение указателя, когда работаете с файлом. Этот
|
||
трюк также нельзя провернуть с TCP-соединением.
|
||
|
||
Другими словами, файл предоставляет вам произвольный доступ, в то время как
|
||
TCP-соединение представляет собой двунаправленный последовательный поток.
|
||
|
||
|
||
## User Datagram Protocol — UDP
|
||
|
||
|
||
Информация, передаваемая по протоколу UDP, представляет собой непрерывный
|
||
кусок данных. По сравнению с TCP, у UDP нет соединений. Невозможно получить
|
||
только часть данных, отправленных приложением. Вы либо получите все данные,
|
||
либо ничего. На данный момент вам нужно знать о UDP следующее:
|
||
- В UDP отсутствуют соединения, поскольку это не поток данных. Из этого
|
||
следует, что нет необходимости создавать или закрывать UDP-сокет. Все, что
|
||
вам требуется — это отправлять или получать данные.
|
||
- Буфер, используемый для получения UDP-пакета должен быть достаточно
|
||
большим, чтобы вместить весь пакет целиком. В противном случае, вы ничего
|
||
не получите. Из этого следует, что необходимо заранее знать верхнюю
|
||
границу размера пакетов, которые вы собрались получать.
|
||
- Порядок входящих пакетов, как правило, не соответствует порядку их
|
||
отправки. Это означает, что необходимо самостоятельно контролировать
|
||
порядок пакетов.
|
||
- Нет никаких гарантий, что все отправленные пакеты будут доставлены. Это
|
||
означает, что потеря UDP-пакетов — обычное дело. Следовательно, необходимо
|
||
самостоятельно контролировать, что все отправленные UDP-пакеты доставлены.
|
||
|
||
Как вы можете понять, UDP немного сложнее в использовании, чем TCP. Тем не
|
||
менее, у UDP есть свои преимущества, которые мы обсудим позднее.
|
||
|
||
Это все, что вам необходимо знать о протоколах на данный момент. Значит, мы
|
||
можем двигаться дальше.
|
||
|
||
|
||
|
||
# Самый простой сервер
|
||
|
||
|
||
Согласно [Википедии](https://ru.wikipedia.org/wiki/%d0%a1%d0%b5%d1%80%d0%b2%d0%b5%d1%80_(%d0%bf%d1%80%d0%be%d0%b3%d1%80%d0%b0%d0%bc%d0%bc%d0%bd%d0%be%d0%b5_%d0%be%d0%b1%d0%b5%d1%81%d0%bf%d0%b5%d1%87%d0%b5%d0%bd%d0%b8%d0%b5)),
|
||
> Сервер — программный компонент вычислительной системы, выполняющий
|
||
сервисные (обслуживающие) функции по запросу клиента, предоставляя ему доступ
|
||
к определённым ресурсам или услугам.
|
||
|
||
Это определение очень точно подмечает тот факт, что сервер — это всего лишь
|
||
приложение, которое получает какие-то данные от других приложений и
|
||
возвращает некоторые данные обратно.
|
||
|
||
Мы начнем с самого простого сервера, который приходит на ум — эхо UDP-сервер.
|
||
Он выполняет следующие действия:
|
||
- Получает любые данные, которые были отправлены на UDP-порт 15001.
|
||
- Отправляет полученные данные обратно отправителю «как есть».
|
||
|
||
На самом деле вы можете выбрать практически любой порт для вашего сервера.
|
||
Существует множество часто используемых портов для различных служб, которые
|
||
вы можете найти здесь:
|
||
[Список портов TCP и UDP](https://ru.wikipedia.org/wiki/%d0%a1%d0%bf%d0%b8%d1%81%d0%be%d0%ba_%d0%bf%d0%be%d1%80%d1%82%d0%be%d0%b2_TCP_%d0%b8_UDP).
|
||
Однако, как правило, только несколько из этих служб используется
|
||
одновременно в недавно установленной ОС.
|
||
|
||
Теперь давайте взглянем на следующий [исходный код](./code/simple_server.cpp):
|
||
|
||
```cpp
|
||
#include <boost/asio.hpp>
|
||
|
||
int main() {
|
||
std::uint16_t port = 15001;
|
||
|
||
boost::asio::io_context io_context;
|
||
boost::asio::ip::udp::endpoint receiver(boost::asio::ip::udp::v4(), port);
|
||
boost::asio::ip::udp::socket socket(io_context, receiver);
|
||
|
||
while (true) {
|
||
char buffer[65536];
|
||
boost::asio::ip::udp::endpoint sender;
|
||
std::size_t bytes_transferred =
|
||
socket.receive_from(boost::asio::buffer(buffer), sender);
|
||
socket.send_to(boost::asio::buffer(buffer, bytes_transferred), sender);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
Вам даже не обязательно отдельно скачивать `.cpp` файл сервера, поскольку
|
||
вышеприведенный код — это полноценный эхо UDP-сервер. Мы не реализовали
|
||
логирование и обработку ошибок, чтобы код выглядел максимально просто. Об
|
||
обработке ошибок мы поговорим позднее. Давайте разберемся, что происходит в
|
||
этом коде:
|
||
- `boost::asio::io_context` — основной поставщик услуг ввода-вывода. В данный
|
||
момент вы можете рассматривать его как исполнителя (executor)
|
||
запланированных задач. Вы поймете его назначение сразу после того, как мы
|
||
перейдем к асинхронному потоку управления, что произойдет очень скоро.
|
||
- `boost::asio::ip::udp::endpoint` — это пара IP-адреса и порта.
|
||
- `boost::asio::ip::udp::socket` — это сокет. Вы можете рассматривать его как
|
||
дескриптор файла, предназначенный для сетевого взаимодействия. Обычно,
|
||
когда вы открываете файл, вы получаете дескриптор файла. Когда вы
|
||
взаимодействуете по сети, вы используете сокет.
|
||
- Каждый сокет прикреплен к некоторому `io_context`, а потому каждый сокет
|
||
конструируется с помощью ссылки на `io_context`. Второй параметр
|
||
конструктора сокета — `endpoint` — IP-адрес и порт, который используется
|
||
для получения входящих дейтаграмм (в случае UDP) или соединений (в случае
|
||
TCP).
|
||
- `boost::asio::ip::udp::v4()` возвращает объект, который в данный момент вы
|
||
должны рассматривать как просто сетевой интерфейс UDP по умолчанию.
|
||
- `boost::asio::buffer()` — это представление буфера, которое содержит
|
||
указатель и размер, причем это представление не владеет памятью. В нашем
|
||
случае оно указывает на массив `char`.
|
||
- `socket::receive_from` ожидает входящий UDP-пакет, заполняет `buffer`
|
||
полученными данными, а также заполняет `sender` информацией об отправителе,
|
||
которая также включает в себя пару IP-адреса и порта.
|
||
- `socket::send_to` отправляет UDP-пакет, используя данные из представления
|
||
буфера. Получатель пакета передается вторым аргументом. В нашем случае
|
||
получателем является отправитель, поскольку речь идет об эхо-сервере.
|
||
|
||
Итак, мы сделали следующее:
|
||
- Создали UDP-сокет и настроили его на ожидание UDP-пакетов на порту 15001.
|
||
- Запустили бесконечный цикл, в котором ожидаем входящие UDP-пакеты, а после
|
||
получения отправляем их обратно отправителю.
|
||
|
||
Поздравляем! Вы только что создали ваш первый сервер с помощью C++ и
|
||
Boost.Asio!
|
||
|
||
|
||
# Прощаемся с синхронностью
|
||
|
||
|
||
В реальной жизни синхронный ввод-вывод практически бесполезен. Даже если вы
|
||
пишите простой клиент с единственным сетевым подключением, то скорее всего
|
||
ваше приложение будет выполнять такие функции как управление пользовательским
|
||
интерфейсом, чтение пользовательского ввода и т. п. Однако использование
|
||
синхронного ввода-вывода означает, что все его операции являются
|
||
блокирующими. Следовательно ваше приложение не сможет выполнять каких либо
|
||
операций до тех пор, пока не завершатся операции с вводом-выводом.
|
||
|
||
Вы можете обойти это ограничение с помощью создания дополнительных потоков.
|
||
Например, один поток может обрабатывать ввод-вывод, а другой управлять
|
||
пользовательским интерфейсом. Однако такой подход приведет к усложнению
|
||
вашего приложения, поскольку в какой-то момент вам придется синхронизировать
|
||
эти потоки. Более того, не существует безопасного способа отменить
|
||
блокирующую операцию ввода-вывода из другого потока. Хотя это и может
|
||
работать так, как вы ожидаете, но в целом это не безопасная операция. А
|
||
потому вы можете столкнуться с неопределенным поведением, если что-нибудь
|
||
измениться в вашем рабочем окружении (например, если вы скомпилируете код для
|
||
новой платформы, с которой вы раньше не работали).
|
||
|
||
Асинхронный подход лишен этих недостатков. Проще говоря, выполнение
|
||
асинхронного кода можно представить так: «Начни делать это в фоновом режиме,
|
||
а после того, как закончишь, вызови эту функцию. Тем временем я займусь
|
||
другими задачами, которые необходимо выполнить». Таким образом, выполнение
|
||
асинхронного кода — это неблокирующая операция, а значит вы можете совершать
|
||
другие действия, пока ваши задачи выполняются в фоновом режиме. Кроме того,
|
||
асинхронные задачи могут быть безопасно отменены в любое время.
|
||
|
||
Вспомним [код из предыдущего раздела](#самый-простой-сервер), в котором
|
||
используется синхронный подход:
|
||
```cpp
|
||
// Эта операция заблокирует поток управление до тех пор, пока не будет получено сообщение
|
||
std::size_t bytes_transferred = socket.receive_from(buffer, sender);
|
||
std::cout << "Message is received, message size is " << bytes_transferred;
|
||
```
|
||
|
||
Асинхронные версии функций ввода-вывода в Boost.Asio начинаются с приставки
|
||
`async_`. Теперь взгляните на тот же код, переписанный в асинхронном стиле:
|
||
```cpp
|
||
// Эта операция не блокирующая: выполнение кода продолжится сразу после вызова функции
|
||
socket.async_receive_from(
|
||
buffer,
|
||
sender,
|
||
[&](boost::system::error_code error, std::size_t bytes_transferred) {
|
||
// Эта лямбда-функция будет вызвана после получения сообщения
|
||
std::cout << "Message is received, message size is "
|
||
<< bytes_transferred;
|
||
});
|
||
```
|
||
|
||
В C++ нам нравится держать все под контролем. Первое, что вы должны спросить:
|
||
«Эй, где именно выполняется это фоновая задача? Должны ли мы создавать поток
|
||
для нее?». Вы получите ответ на этот вопрос в следующем разделе. А пока,
|
||
пришло время сказать «прощай» синхронному коду и двигаться дальше.
|
||
|
||
|
||
# Асинхронный TCP-сервер
|
||
|
||
|
||
Пришло время взглянуть на наш первый асинхронный TCP-сервер. Это последний
|
||
раз, когда мы не используем пространства имен (namespaces) и псевдонимы типов
|
||
(type aliases). В дальнейшем вы уже должны понимать откуда берутся те или
|
||
иные вещи.
|
||
|
||
Теперь наш сервер будет делать следующее:
|
||
- Слушать порт 15001 и ожидать входящее TCP-соединение.
|
||
- Принимать входящее соединение.
|
||
- Читать данные из соединения до тех пор, пока не встретится символ конца
|
||
строки (т. е. символ `\n`).
|
||
- Выводить полученные данные в стандартный вывод.
|
||
- Закрывать соединение.
|
||
|
||
Теперь давайте взглянем на полноценный пример такого сервера. Ниже мы все
|
||
разложим по полочкам и посмотрим как все устроено. Как и прежде, мы
|
||
пренебрегаем обработкой ошибок, чтобы код выглядел более понятным. Об обработке
|
||
ошибок мы поговорим позже.
|
||
|
||
```cpp
|
||
#include <boost/asio.hpp>
|
||
#include <iostream>
|
||
#include <optional>
|
||
|
||
class session: public std::enable_shared_from_this<session> {
|
||
public:
|
||
session(boost::asio::ip::tcp::socket&& socket) :
|
||
socket(std::move(socket)) {}
|
||
|
||
void start() {
|
||
boost::asio::async_read_until(
|
||
socket,
|
||
streambuf,
|
||
'\n',
|
||
[self = shared_from_this()](
|
||
boost::system::error_code error,
|
||
std::size_t bytes_transferred) {
|
||
std::cout << std::istream(&self->streambuf).rdbuf();
|
||
});
|
||
}
|
||
|
||
private:
|
||
boost::asio::ip::tcp::socket socket;
|
||
boost::asio::streambuf streambuf;
|
||
};
|
||
|
||
class server {
|
||
public:
|
||
server(boost::asio::io_context& io_context, std::uint16_t port) :
|
||
io_context(io_context),
|
||
acceptor(
|
||
io_context,
|
||
boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) {}
|
||
|
||
void async_accept() {
|
||
socket.emplace(io_context);
|
||
|
||
acceptor.async_accept(*socket, [&](boost::system::error_code error) {
|
||
std::make_shared<session>(std::move(*socket))->start();
|
||
async_accept();
|
||
});
|
||
}
|
||
|
||
private:
|
||
boost::asio::io_context& io_context;
|
||
boost::asio::ip::tcp::acceptor acceptor;
|
||
std::optional<boost::asio::ip::tcp::socket> socket;
|
||
};
|
||
|
||
int main() {
|
||
boost::asio::io_context io_context;
|
||
server srv(io_context, 15001);
|
||
srv.async_accept();
|
||
io_context.run();
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
По сравнению с предыдущим сервером, этот код занимает значительно больше
|
||
места. Но не стоит паниковать, здесь всего 57 строк кода, которые
|
||
представляют из себя полноценный асинхронный TCP-сервер.
|
||
|
||
В прошлый раз мы упомянули, что все функции с приставкой `async_` выполняются
|
||
в фоновом режиме. Так где же находится этот «фоновый режим»? Что ж, фоновый
|
||
режим находится _где-то_ внутри операционной системы. На самом деле, вам не
|
||
нужно заботиться о том, как это происходит. Единственное, что должно вас
|
||
волновать — откуда вызываются обработчики завершения. И это происходит внутри
|
||
`io_context.run()`. Давайте взглянем на функцию `main`:
|
||
```cpp
|
||
int main() {
|
||
boost::asio::io_context io_context;
|
||
server srv(io_context, 15001);
|
||
srv.async_accept();
|
||
io_context.run();
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
Функция `boost::asio::io_context::run` — это своего рода функция цикла
|
||
событий (event loop), которая управляет всеми операциями ввода-вывода. При
|
||
вызове функции `run` поток управления блокируется до тех пор, пока не
|
||
выполнятся все асинхронные операции, связанные с `io_context`. Все операции с
|
||
приставкой `async_` связаны с каким-либо `io_context`. В некоторых языках
|
||
программирования (например, JavaScript) функция цикла событий спрятана от
|
||
разработчика. Но в C++ нам нравится все держать под контролем, поэтому мы
|
||
решаем, где именно функция цикла событий будет запущена.
|
||
|
||
Теперь давайте рассмотрим класс `server`. Здесь встречается сразу несколько
|
||
новых вещей, которые находятся в `private` секции класса:
|
||
- `boost::asio::ip::tcp::socket` — этот тот же самый сокет, что и до этого,
|
||
только теперь он работает в рамках протокола TCP (вместо UDP, как это было
|
||
ранее).
|
||
- `boost::asio::ip::tcp::acceptor` — это объект, который принимает входящие
|
||
соединения.
|
||
|
||
Если вы посмотрите на конструктор класса `acceptor`, вы увидите то, что он
|
||
очень похож на метод `receive_from` у UDP-сокета:
|
||
```cpp
|
||
acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
|
||
```
|
||
|
||
Передав такие аргументы конструктору, мы получим, что `acceptor` будет
|
||
слушать входящие TCP-соединения на любом сетевом интерфейсе на указанном
|
||
порту.
|
||
|
||
Теперь давайте рассмотрим вызов функцию `async_accept` у `acceptor`:
|
||
```cpp
|
||
acceptor.async_accept(*socket, [&](boost::system::error_code error) {
|
||
std::make_shared<session>(std::move(*socket))->start();
|
||
async_accept();
|
||
});
|
||
```
|
||
|
||
Словами это можно описать так: «Ожидай входящее соединение, а после того как
|
||
установишь его, свяжи это соединение с сокетом и вызови обработчик
|
||
завершения». Как вы помните, функции с приставкой `async_` не блокируют
|
||
вызывающий поток.
|
||
|
||
Итак, сервер ожидает входящее соединение После установления соединения сервер
|
||
создает объект сессии. При создании мы перемещаем сокет, связанный с
|
||
установленным соединением, внутрь объекта сессии. После этого сервер начинает
|
||
ожидать следующее входящее соединение.
|
||
|
||
Обратите внимание, что серверу все равно, что происходит с установленным
|
||
соединением. Сервер сразу же начинает ожидать следующее входящее соединение
|
||
не беспокоясь о том, что происходит с предыдущим соединением. Установленные
|
||
соединения выполняются в фоновом режиме. Одновременно может существовать
|
||
почти любое количество соединений (количество открытых файловых дескрипторов
|
||
ограничено ОС), выполняемых в фоновом режиме. Это и есть принцип работы
|
||
асинхронного ввода-вывода.
|
||
|
||
Отлично, теперь мы знаем, как работает наш сервер. Давайте рассмотрим класс
|
||
`session`. Сессия — это класс, который поддерживает соединение. Сессия
|
||
содержит некоторые данные, связанные с соединением и предоставляет некоторый
|
||
набор функций, связанный с соединением. Давайте рассмотрим функцию `start`:
|
||
```cpp
|
||
void start() {
|
||
boost::asio::async_read_until(
|
||
socket,
|
||
streambuf,
|
||
'\n',
|
||
[self = shared_from_this()](
|
||
boost::system::error_code error,
|
||
std::size_t bytes_transferred) {
|
||
std::cout << std::istream(&self->streambuf).rdbuf();
|
||
});
|
||
}
|
||
```
|
||
|
||
Дословно, код выполняет следующее: «Читай данные из сокета в `streambuf`, а
|
||
когда встретишь символ `"\n"`, остановись и вызови обработчик завершения».
|
||
|
||
`boost::asio::streambuf` — это класс, унаследованный от `std::streambuf`.
|
||
Можете рассматривать его как реализацию `streambuf` в библиотеке
|
||
`Boost.Asio`.
|
||
|
||
Итого, сессия считывает данные из сокета до тех пор, пока не встретит символ
|
||
`"\n"`, а после записывает полученные данные в стандартный вывод.
|
||
|
||
Обратите внимание, что класс `session` унаследован от класса
|
||
`std::enable_shared_from_this`. Также заметим, что сессия захватывает в
|
||
лямбду обработчика завершения указатель на разделяемую копию себя посредством
|
||
`shared_from_this`. Мы делаем это, чтобы продлить время жизни сессии до тех
|
||
пор, пока не будет вызван обработчик завершения. После этого нам не нужно
|
||
ничего делать — указатель на разделяемый объект выйдет из области видимости и
|
||
сразу же уничтожится после завершения работы обработчика. В большинстве
|
||
случаев (но не во всех), это обычный способ для работы с сессиями.
|
||
|
||
Теперь вы знаете как написать простой асинхронный TCP-сервер и как он
|
||
работает. Последнее, что нам необходимо сделать — протестировать в реальной
|
||
жизни. Запустим сервер в терминале:
|
||
```
|
||
./server
|
||
```
|
||
|
||
Теперь в другом терминале запустим `telnet`, подключимся к 15001 порту,
|
||
введем `Hello asio!` и нажмем `Enter` (который введет ожидаемый символ
|
||
`"\n"`):
|
||
```
|
||
telnet localhost 15001
|
||
Hello asio!
|
||
```
|
||
|
||
В первом терминале вы должны увидеть это сообщение:
|
||
```
|
||
./server
|
||
Hello asio!
|
||
```
|
||
|
||
Круто! Вы только начали, а уже знаете, как написать почти любой
|
||
профессиональный асинхронный TCP-сервер с помощью современного C++ и
|
||
Boost.Asio. Поздравляем!
|
||
|
||
|
||
# Обработка ошибок
|
||
|
||
|
||
|
||
## Синхронные функции
|
||
|
||
|
||
В Boost.Asio все синхронные функции ввода-вывода имеют две перегрузки для
|
||
обработки ошибок: первая выбрасывает исключение, а вторая возвращает ошибку
|
||
по ссылке (подход с возвращением значения).
|
||
|
||
Исключения, которые выбрасывается из функций Boost.Asio, являются
|
||
экземплярами класса `boost::system::system_error`, который, в свою очередь,
|
||
унаследован от класса `std::runtime_error`.
|
||
|
||
При возврате ошибки по ссылке, используется экземпляр класса
|
||
`boost::system::error_code`.
|
||
|
||
|
||
### Пример. Исключения
|
||
|
||
|
||
```cpp
|
||
try {
|
||
socket.connect(endpoint);
|
||
} catch (const boost::system::system_error& e) {
|
||
std::cerr << e.what() << "\n";
|
||
}
|
||
```
|
||
|
||
Вы можете получить `error_code` из `system_error`, вызвав метод `code()`:
|
||
```cpp
|
||
catch (const boost::system::system_error& e) {
|
||
boost::system::error_code error = e.code();
|
||
std::cerr << error.message() << "\n";
|
||
}
|
||
```
|
||
|
||
|
||
### Пример. Возвращаемое значение
|
||
|
||
|
||
Если вы не хотите возиться с исключениями, вы можете использовать
|
||
перегрузку, чтобы получить ошибку по ссылке:
|
||
```cpp
|
||
boost::system::error_code error;
|
||
socket.connect(endpoint, error);
|
||
|
||
if (error) {
|
||
std::cerr << error.message() << "\n";
|
||
}
|
||
```
|
||
|
||
|
||
|
||
## Асинхронные функции
|
||
|
||
|
||
Асинхронные функции ввода-вывода не выбрасывают исключений. Вместо этого,
|
||
они передают `boost::system::error_code` в обработчик завершения. Поэтому
|
||
для того, чтобы проверить, была ли операция завершена успешно, вы должны
|
||
написать что-то наподобие этого:
|
||
```cpp
|
||
socket.async_connect(endpoint, [&](boost::system::error_code error) {
|
||
if (!error) {
|
||
// Асинхронная операция успешно завершена
|
||
} else {
|
||
// Что-то пошло не так
|
||
std::cerr << error.message() << "\n";
|
||
}
|
||
});
|
||
```
|
||
|
||
|
||
|
||
## error_code
|
||
|
||
|
||
Рассмотрим некоторый функционал `error_code`, который может вам пригодиться
|
||
при работе с ним.
|
||
- Если вы хотите получить удобочитаемое описание ошибки из
|
||
`boost::system::error_code`, нужно вызвать метод `message()`, который
|
||
вернет `std::string` с описанием ошибки.
|
||
- Если вы не хотите выделять дополнительную память для `std::string`, вы
|
||
можете использовать перегрузку `message(char const* buffer, std::size_t
|
||
size)`.
|
||
- Если вы хотите получить системный код (который имеет тип `int`) ошибки из
|
||
`error_code`, вызовите метод `value()`.
|
||
|
||
Если удаленное соединение было закрыто, то будет выброшена `end-of-file`
|
||
ошибка. В некоторых случаях вы не хотите рассматривать `end-of-file` ошибку
|
||
как ошибку приложения. Например, вы хотите получить некоторое сообщение от
|
||
удаленного хоста. После передачи сообщения хост разрывает соединение, что
|
||
является нормальным поведением. Для обработки такой ситуации вы могли бы
|
||
написать:
|
||
```cpp
|
||
socket.async_receive(
|
||
buffer,
|
||
[&](boost::system::error_code error, std::size_t bytes_transferred) {
|
||
if (!error) {
|
||
// Асинхронная операция выполнена успешно.
|
||
// Соединение все еще установлено
|
||
} else if (error == boost::asio::error::eof) {
|
||
// Соединение было разорвано.
|
||
// В буфере по-прежнему хранятся полученные данные,
|
||
// численно равные `bytes_transferred` (в байтах)
|
||
} else {
|
||
// Что-то пошло не так
|
||
std::cerr << error.message() << "\n";
|
||
}
|
||
});
|
||
```
|
||
|
||
У вас может возникнуть вопрос: как передавать `boost::system::error_code` в
|
||
функции? По ссылке или по значению? С одной стороны, если вы откроете
|
||
документацию Boost.Asio, то увидите, что автор библиотеки передает
|
||
`error_code` по ссылке. С другой стороны, `error_code` содержит в себе один
|
||
`int`, один `bool` и один сырой указатель.
|
||
|
||
Вспомним класс `std::string_view`: он содержит в себе один `std::size_t` и
|
||
один сырой указатель. Поскольку его размер невелик, его стоит передавать по
|
||
значению. В нашем случае `error_code` занимает столько же места, сколько и
|
||
`std::string_view` (в большинстве случаев это так, зависит от платформы).
|
||
Поэтому в нашем коде мы передаем `error_code` по значению.
|
||
|
||
|
||
# Дальнейшее изучение
|
||
|
||
|
||
В следующем разделе мы рассмотрим пример более масштабного сервера — TCP
|
||
чат-сервера. Но перед этим вы должны кое-что узнать.
|
||
|
||
У сокета есть функционал, о котором мы еще не говорили. Мы можем узнать у
|
||
сокета о его конечных устройствах с обеих сторон соединения:
|
||
```cpp
|
||
boost::asio::ip::tcp::endpoint endpoint;
|
||
endpoint = socket.local_endpoint(); // IP:порт локальной стороны соединения
|
||
endpoint = socket.remote_endpoint(); // IP:порт удаленной стороны соединения
|
||
```
|
||
|
||
Обратите внимание, что эти функции могут выбросить исключение. Если вы не
|
||
хотите возиться с исключениями, вы можете использовать перегрузку, чтобы
|
||
получить ошибку по ссылке:
|
||
```cpp
|
||
boost::system:error_code error;
|
||
auto endpoint = socket.remote_endpoint(error);
|
||
```
|
||
|
||
`endpoint` можно использовать с `iostreams`:
|
||
```cpp
|
||
boost::system:error_code error;
|
||
auto endpoint = socket.remote_endpoint(error);
|
||
std::cout << "Remote endpoint: " << endpoint << "\n";
|
||
```
|
||
|
||
Если запустить этот код, вы скорее всего увидите что-то на подобии этого:
|
||
```sh
|
||
Remote endpoint: 127.0.0.1:38529
|
||
```
|
||
|
||
Иногда вам может понадобиться отменить асинхронную операцию, которая была
|
||
запланирована ранее. Единственный надежный и переносимый способ сделать это —
|
||
закрыть связанный с операцией сокет.
|
||
```cpp
|
||
boost::asio::async_read(socket, buffer, completion_handler);
|
||
// ...
|
||
socket.close();
|
||
```
|
||
|
||
Обратите внимание, что `socket::close` может выбросить исключение. Как
|
||
всегда, существует перегрузка для получения ошибки по ссылке:
|
||
```cpp
|
||
boost::system::error_code error;
|
||
socket.close(error);
|
||
```
|
||
|
||
Также существует метод `socket::cancel`, который позволяет отменить
|
||
выполняемую в данный момент асинхронную операцию без закрытия сокета. Однако
|
||
это поведение специфично для конкретной платформы. Метод может работать так,
|
||
как вы этого ожидание, а может быть и проигнорирован ОС. Такая особенность
|
||
почти наверняка говорит о том, что система имеет плохой дизайн. Старайтесь
|
||
избегать этой операции.
|
||
|
||
Когда вы отправляете данные по сети, вы всегда знаете точно, сколько байт
|
||
должно быть передано. Когда вы получите данные,вы также можете ожидать
|
||
получения некоторого фиксированного количества байтов. Однако в некоторых
|
||
случаях вам нужно считывать данные до тех пор, пока не выполнится какое-то
|
||
условие. Например, до тех пор, пока не встретится определенная
|
||
последовательность (например, символ `"\n"`). В этом случае использование
|
||
`boost::asio::streambuf` может быть более удобным, чем использование буфера с
|
||
фиксированным размером. При использовании этого контейнера вы должны
|
||
установить верхнюю границу размера `streambuf`, передав максимальный размер в
|
||
конструктор:
|
||
```cpp
|
||
boost::asio::streambuf streambuf(65536);
|
||
```
|
||
|
||
В противном случае, размер буфера может расти до тех пор, пока не закончится
|
||
память. После того, как вы обработали часть полученных данных, необходимо
|
||
стереть эти данные из `streambuf`, чтобы размер буфера увеличивался. Это
|
||
можно сделать с помощью метода `consume()`:
|
||
```cpp
|
||
boost::asio::async_read_until(
|
||
socket,
|
||
streambuf,
|
||
"\n",
|
||
[&](boost::system::error_code error, std::size_t bytes_transferred) {
|
||
// Обработка полученных данных
|
||
// ...
|
||
streambuf.consume(bytes_transferred);
|
||
});
|
||
```
|
||
|
||
Библиотека самостоятельно не ставит в очередь асинхронные операции. Чтобы
|
||
запланировать новую задачу, вам необходимо дождаться завершения текущей
|
||
задачи. Это значит, что вы должны управлять очередью задач самостоятельно.
|
||
Конечно, это проблема, если мы говорим об одном и том же типе операций:
|
||
несколько чтений или несколько записей. Однако операции `async_read` и
|
||
`async_write` могут быть запланированы параллельно без каких-либо проблем.
|
||
|
||
Временем жизни объекта сессии можно управлять разными способами. Это зависит
|
||
от логики сервера. Иногда достаточно захватить указатель на разделяемый
|
||
объект в обработчик завершения. Таким образом мы можем продлить время жизни
|
||
сессии до тех пор, пока не завершится текущая асинхронная операция.
|
||
|
||
Однако иногда серверу нужно знать обо всех активных клиентах. Например, чтобы
|
||
иметь возможность получить доступ к каждому клиенту. Этого можно добиться,
|
||
поместив указатели на клиентов в некоторый специальный контейнер.
|
||
|
||
Иногда объект сессии должен оставаться в памяти тогда, когда отсутствуют
|
||
запланированные задачи, т. е. нет обработчиков завершения, которые могли бы
|
||
хранить указатель на разделяемый объект. Этого можно добиться, если хранить
|
||
указатель на разделяемый объект сессии где-нибудь в другом месте (например, в
|
||
контейнере сервера). Также этого можно достичь использованием сырых
|
||
указателей. Работа с сырыми указателями может показаться странной — в конце
|
||
концов, мы говорим о C++. Однако в некоторых особых случаях этот способ очень
|
||
хорош для управления асинхронным взаимодействием. Мы обсудим метод с сырыми
|
||
указателями позднее.
|
||
|
||
Обычно сервер знает о классе сессии, с которым он работает. Однако классу
|
||
сессии также может понадобиться передать некоторую информацию серверу. Это
|
||
приводит нас к необходимости цикличной видимости. Для достижения этого мы
|
||
могли бы использовать предварительное объявление (forward declaration) класса
|
||
сервера. После чего мы бы передавали ссылку на сервер в конструктор сессии.
|
||
Однако это не очень хороший дизайн. Более хороший способ решить эту проблему
|
||
— использовать функции обработчиков событий:
|
||
|
||
```cpp
|
||
using message_handler = std::function<void(std::string)>;
|
||
|
||
// На стороне сервера
|
||
void server::create_session() {
|
||
auto client = std::make_shared<session>([&](const std::string& message) {
|
||
std::cout << "We got a message: " << message;
|
||
});
|
||
}
|
||
|
||
// На стороне клиента
|
||
void session::session(message_handler&& handler) :
|
||
on_message(std::move(handler)) {}
|
||
|
||
void session::async_receive() {
|
||
boost::asio::async_receive(socket, [...](...) { on_message(some_buffer); });
|
||
}
|
||
```
|
||
|
||
Более хороший не означает, что лучший. Есть несколько способов передавать
|
||
данные между сессиями и сервером. Какой из них является лучшим — зависит от
|
||
деталей реализации вашего приложения.
|
||
|
||
Отлично, теперь вы знаете все, что нужно знать, чтобы рассмотреть следующий
|
||
пример — простой TCP чат-сервер.
|
||
|
||
|
||
# TCP чат-сервер
|
||
|
||
|
||
Вы уже знаете какие вещи откуда берутся, поэтому отныне мы будем использовать
|
||
псевдонимы типов, чтобы сделать названия типов короче.
|
||
|
||
В этом разделе мы рассмотрим очень простой чат-сервер. Этот сервер не будет
|
||
поддерживать пользовательские ники, цвета и другие аспекты, специфичные для
|
||
конкретного пользователя. Мы отказываемся от этого, чтобы сервер был более
|
||
простым.
|
||
|
||
В предыдущем разделе мы подробно обсудили новые вещи, которые будут
|
||
использоваться в этом сервере. Поэтому в этом разделе мы рассмотрим сервер
|
||
лишь вкратце.
|
||
|
||
Полный исходный код, который мы будем разбирать в этом разделе, вы можете
|
||
найти [здесь](./code/tcp_chat_server.cpp). После чтения этого раздела качайте
|
||
его, скомпилируйте, посмотрите как он работает. Попытайтесь самостоятельно
|
||
понять, как все работает, основываясь на том, что вы узнали за все это время.
|
||
В конце концов, вам нужно научиться понимать код.
|
||
|
||
Ну что ж, начнем. Первое, что вы увидите в исходном коде — это включение
|
||
заголовков и использование псевдонимов типов:
|
||
```cpp
|
||
#include <boost/asio.hpp>
|
||
|
||
#include <optional>
|
||
#include <queue>
|
||
#include <unordered_set>
|
||
|
||
namespace io = boost::asio;
|
||
using tcp = io::ip::tcp;
|
||
using error_code = boost::system::error_code;
|
||
using namespace std::placeholders;
|
||
|
||
using message_handler = std::function<void(std::string)>;
|
||
using error_handler = std::function<void()>;
|
||
```
|
||
|
||
Пока что все должно быть очевидно. Функция `main` выглядит точно также, как и
|
||
в предыдущем примере (за исключением использования псевдонимов типов):
|
||
```cpp
|
||
int main() {
|
||
io::io_context io_context;
|
||
server srv(io_context, 15001);
|
||
srv.async_accept();
|
||
io_context.run();
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
В этот раз класс сервера и класс сессии немного больше, но поскольку мы уже
|
||
разобрали часть кода, рассмотрим только ключевые моменты.
|
||
|
||
Давайте начнем с ключевых моментов сессии. Функция `start` теперь принимает
|
||
обработчики событий:
|
||
```cpp
|
||
void start(message_handler&& on_message, error_handler&& on_error) {
|
||
this->on_message = std::move(on_message);
|
||
this->on_error = std::move(on_error);
|
||
async_read();
|
||
}
|
||
```
|
||
|
||
Функция `post` добавляет в очередь сообщение, адресованное клиенту. Отправка
|
||
сообщения начинается, если в данный момент не отправляется предыдущее
|
||
сообщение:
|
||
```cpp
|
||
void post(const std::string& message) {
|
||
bool idle = outgoing.empty();
|
||
outgoing.push(message);
|
||
|
||
if (idle) {
|
||
async_write();
|
||
}
|
||
}
|
||
```
|
||
|
||
Асинхронные функции чтения и записи выделены в отдельные методы класса
|
||
сессии. Функция `async_read` считывает данные с удаленного клиента в
|
||
`streambuf`, а функция `async_write` отправляет первое в очереди сообщение
|
||
удаленному клиенту:
|
||
```cpp
|
||
void async_read() {
|
||
io::async_read_until(
|
||
socket,
|
||
streambuf,
|
||
"\n",
|
||
std::bind(&session::on_read, shared_from_this(), _1, _2));
|
||
}
|
||
|
||
void async_write() {
|
||
io::async_write(
|
||
socket,
|
||
io::buffer(outgoing.front()),
|
||
std::bind(&session::on_write, shared_from_this(), _1, _2));
|
||
}
|
||
```
|
||
|
||
Обработчик чтения выполняет следующие действия:
|
||
1. форматирует сообщение, полученное от клиента;
|
||
2. передает отформатированное сообщение в обработчик сообщений;
|
||
3. начинает ожидать следующего сообщения.
|
||
|
||
Кроме того, он также выполняет обработку ошибок:
|
||
```cpp
|
||
void on_read(error_code error, std::size_t bytes_transferred) {
|
||
if (!error) {
|
||
std::stringstream message;
|
||
message << socket.remote_endpoint(error) << ": "
|
||
<< std::istream(&streambuf).rdbuf();
|
||
streambuf.consume(bytes_transferred);
|
||
on_message(message.str());
|
||
async_read();
|
||
} else {
|
||
socket.close(error);
|
||
on_error();
|
||
}
|
||
}
|
||
```
|
||
|
||
Обработчик записи работает так:
|
||
1. удаляет сообщение из очереди;
|
||
2. если в очереди еще остались сообщения, начинает отправку следующего
|
||
сообщения.
|
||
|
||
Он также выполняет обработку ошибок:
|
||
```cpp
|
||
void on_write(error_code error, std::size_t bytes_transferred) {
|
||
if (!error) {
|
||
outgoing.pop();
|
||
|
||
if (!outgoing.empty()) {
|
||
async_write();
|
||
}
|
||
} else {
|
||
socket.close(error);
|
||
on_error();
|
||
}
|
||
}
|
||
```
|
||
|
||
У класса сессии следующие атрибуты:
|
||
```cpp
|
||
tcp::socket socket; // Сокет клиента
|
||
io::streambuf streambuf; // Буфер для входящих данных
|
||
std::queue<std::string> outgoing; // Очередь исходящих сообщений
|
||
message_handler on_message; // Обработчик сообщений
|
||
error_handler on_error; // Обработчик ошибок
|
||
```
|
||
|
||
Теперь давайте рассмотрим класс сервера. Начнем с атрибутов:
|
||
```cpp
|
||
io::io_context& io_context;
|
||
tcp::acceptor acceptor;
|
||
std::optional<tcp::socket> socket;
|
||
std::unordered_set<session::pointer> clients; // Список подключенных клиентов
|
||
```
|
||
|
||
Функция `post` рассылает сообщение всем подключенным клиентам. Эта функция
|
||
также используется в качестве обработчика сообщений (см. далее):
|
||
```cpp
|
||
void post(const std::string& message) {
|
||
for (auto& client : clients) {
|
||
client->post(message);
|
||
}
|
||
}
|
||
```
|
||
|
||
Функция `async_accept` приветствует только что подключившегося клиента и
|
||
сообщает всем остальным клиентам о новоприбывшем. Здесь также реализована
|
||
обработка ошибок, которая в случае чего удаляет сессию из списка клиентов,
|
||
после чего уведомляет об этом остальных клиентов:
|
||
```cpp
|
||
void async_accept() {
|
||
socket.emplace(io_context);
|
||
|
||
acceptor.async_accept(*socket, [&](error_code error) {
|
||
auto client = std::make_shared<session>(std::move(*socket));
|
||
client->post("Welcome to chat\n\r");
|
||
post("We have a newcomer\n\r");
|
||
|
||
clients.insert(client);
|
||
|
||
client->start(
|
||
std::bind(&server::post, this, _1),
|
||
[&, weak = std::weak_ptr(client)] {
|
||
if (auto shared = weak.lock();
|
||
shared && clients.erase(shared)) {
|
||
post("We are one less\n\r");
|
||
}
|
||
});
|
||
|
||
async_accept();
|
||
});
|
||
}
|
||
```
|
||
|
||
Теперь давайте запустим наш сервер:
|
||
```
|
||
./server
|
||
```
|
||
|
||
Также запустим клиент `telnet`:
|
||
```
|
||
telnet localhost 15001
|
||
|
||
Welcome to chat
|
||
```
|
||
|
||
Запустив второй клиент `telnet`, на первом клиенте вы увидите:
|
||
```
|
||
telnet localhost 15001
|
||
|
||
Welcome to chat
|
||
We have a newcomer
|
||
```
|
||
|
||
Запустите еще один клиент, напишите что-нибудь в чат и нажмите `Enter`:
|
||
```
|
||
telnet localhost 15001
|
||
|
||
Welcome to chat
|
||
Hello guys
|
||
```
|
||
|
||
Остальные клиенты должны увидеть что-то вроде этого:
|
||
```
|
||
telnet localhost 15001
|
||
|
||
Welcome to chat
|
||
We have a newcomer
|
||
We have a newcomer
|
||
127.0.0.1:47235: Hello guys
|
||
```
|
||
|
||
|
||
# Упрощаем код
|
||
|
||
В предыдущем разделе мы рассмотрели очень простой чат-сервер, занимающий
|
||
всего лишь 131 строку кода. Однако, если бы мы писали такой же сервер на
|
||
языке программирования более высокого уровня (например, Python или Erlang), у
|
||
нас бы получилось гораздо меньше кода.
|
||
|
||
Вы могли бы заметить: «Но C++ — это не Python и не Erlang. Разве C++ не
|
||
является низкоуровневым языком программирования?». Ответ: и да, и нет. C++
|
||
очень гибкий язык, который позволяет работать и на низком, и на высоком
|
||
уровне. Однако такая свобода возлагает на программиста большую
|
||
ответственность: нужно быть аккуратным, чтобы код не превратился в непонятную
|
||
кашу.
|
||
|
||
Вы конечно можете работать с сырой памятью и сырыми указателями. Вы можете
|
||
париться по поводу порядка определенных байтов. Ваш код может генерировать
|
||
неустранимые ошибки, которые приведут к падению вашего приложения. И еще
|
||
тысяча особенностей, с которыми вы не столкнетесь, если бы вы будете
|
||
использовать Python. Однако в C++ вы можете спроектировать свой код таким
|
||
образом, чтобы каждый слой абстракции имел узкий набор обязанностей. Тем
|
||
самым, вы можете сделать свой код таким же высокоуровневым, как Python или
|
||
Erlang.
|
||
|
||
Обобщая вышесказанное, C++ — это язык программирования, используя который вы
|
||
должны делить код на определенные слои абстракции, причем делать это нужно
|
||
очень осторожно.
|
||
|
||
Boost.Asio — это библиотека, которая предоставляет вам низкоуровневую
|
||
функциональность. В настоящем приложении вам не следует напрямую использовать
|
||
Boost.Asio, равно как и использовать мьютексы или функцию `fopen`.
|
||
Boost.Beast — это библиотека, основанная на Boost.Asio, которая предоставит
|
||
вам всю необходимую функциональность, связанную с HTTP и Web-сокетами.
|
||
Однако даже Boost.Beast вам не следует использовать напрямую в вашем
|
||
приложении. По словами Vinnie Falco (автор Boost.Beast), библиотека
|
||
Boost.Beast — это не готовый для использования сервер или клиент. Это набор
|
||
инструментов, который вы должны использовать, чтобы создавать свои
|
||
собственные библиотеки. Причем ваше приложение должно основываться на этих
|
||
библиотеках, основанных на Boost.Beast, который, в свою очередь, основан на
|
||
Boost.Asio.
|
||
|
||
Со временем ваше приложение начнет расти, поэтому вам необходимо
|
||
структурировать ваш код таким образом, чтобы каждый слой абстракции решал
|
||
только тот круг задач, на который он рассчитан. Если переложить вышесказанное
|
||
на наш сервер, то его реализация могла бы выглядеть следующим образом:
|
||
```cpp
|
||
#include <chat/server.hpp>
|
||
|
||
using message_type = std::string;
|
||
using session_type = chat::session<message_type>;
|
||
using server_type = chat::server<session_type>;
|
||
|
||
class server {
|
||
public:
|
||
server(io::io_context& io_context, std::uint16_t port) :
|
||
srv(io_context, port) {
|
||
srv.on_join([&](session_type& client) {
|
||
client.post("Welcome to chat");
|
||
srv.broadcast("We have a newcomer");
|
||
});
|
||
|
||
srv.on_leave([&] { srv.broadcast("We are one less"); });
|
||
|
||
srv.on_message(
|
||
[&](message_type const& message) { srv.broadcast(message); });
|
||
}
|
||
|
||
void start() {
|
||
srv.start();
|
||
}
|
||
|
||
private:
|
||
server_type srv;
|
||
};
|
||
|
||
int main() {
|
||
io::io_context io_context;
|
||
server srv(io_context, 15001);
|
||
srv.start();
|
||
io_context.run();
|
||
return 0;
|
||
}
|
||
``` |