commit b670e176709e5497f1e50caa42523922770da1d5 Author: Артём Кокос Date: Mon Jan 26 21:22:56 2026 +0700 Initial commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ec76cef --- /dev/null +++ b/.clang-format @@ -0,0 +1,85 @@ +AccessModifierOffset: -2 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: false +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +BreakStringLiterals: false +ColumnLimit: 80 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +FixNamespaceComments: true +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + CaseSensitive: false + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 3 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: BeforeHash +IndentWidth: 4 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +PointerAlignment: Left +ReflowComments: false +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 4 +UseTab: Never diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..655f895 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +build-md: + nvim --headless -c "execute 'Neorg export to-file TUTORIAL.md' | sleep 100m | q" norg/tutorial.norg + markdown-toc -i TUTORIAL.md + nvim --headless -c "execute 'Neorg export to-file README.md' | sleep 100m | q" norg/readme.norg diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbf3f50 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# 📚 Boost.Asio: Асинхронность в C++ + +Руководство по созданию клиент-серверных систем с помощью C++, библиотек Boost.Asio и Boost.Beast. + +

+ +[ЧИТАТЬ ОНЛАЙН](./TUTORIAL.md) + +

+ +
+ +Руководство находится в стадии перевода + +
+ + + +# 💡 О руководстве + +Это руководство является вольным переводом [серии туториалов](https://dens.website/tutorials/cpp-asio). Поскольку перевод вольный, в нем могут быть некоторые отступления от изначального текста. + + +# 🎈 Помощь в переводе + +В переводе могут быть некоторые неточности, которые неправильно преподносят суть тех или иных вещей. Если вы нашли подобную неточность, откройте Issue или создайте Pull Request. Спасибо! \ No newline at end of file diff --git a/TUTORIAL.md b/TUTORIAL.md new file mode 100644 index 0000000..080ae89 --- /dev/null +++ b/TUTORIAL.md @@ -0,0 +1,1073 @@ +# Содержание + + + +- [Введение](#%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) + + + +# Введение + +Данное руководство посвящено работе асинхронного ввода-вывода, который в +основном используется для сетевого взаимодействия. Для лучшего понимания +происходящего, вы должны быть знакомы с современным 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 + +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 +#include +#include + +class session: public std::enable_shared_from_this { + 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(std::move(*socket))->start(); + async_accept(); + }); + } + + private: + boost::asio::io_context& io_context; + boost::asio::ip::tcp::acceptor acceptor; + std::optional 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(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 server::create_session() { + auto client = std::make_shared([&](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 + +#include +#include +#include + +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; +using error_handler = std::function; +``` + +Пока что все должно быть очевидно. Функция `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 outgoing; // Очередь исходящих сообщений +message_handler on_message; // Обработчик сообщений +error_handler on_error; // Обработчик ошибок +``` + +Теперь давайте рассмотрим класс сервера. Начнем с атрибутов: +```cpp +io::io_context& io_context; +tcp::acceptor acceptor; +std::optional socket; +std::unordered_set 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(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 + +using message_type = std::string; +using session_type = chat::session; +using server_type = chat::server; + +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; +} +``` \ No newline at end of file diff --git a/code/simple_server.cpp b/code/simple_server.cpp new file mode 100644 index 0000000..8b1a009 --- /dev/null +++ b/code/simple_server.cpp @@ -0,0 +1,19 @@ +#include + +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; +} \ No newline at end of file diff --git a/code/tcp_async_server.cpp b/code/tcp_async_server.cpp new file mode 100644 index 0000000..8e3d6c4 --- /dev/null +++ b/code/tcp_async_server.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +class session: public std::enable_shared_from_this { + 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(std::move(*socket))->start(); + async_accept(); + }); + } + + private: + boost::asio::io_context& io_context; + boost::asio::ip::tcp::acceptor acceptor; + std::optional socket; +}; + +int main() { + boost::asio::io_context io_context; + server srv(io_context, 15001); + srv.async_accept(); + io_context.run(); + return 0; +} \ No newline at end of file diff --git a/code/tcp_chat_server.cpp b/code/tcp_chat_server.cpp new file mode 100644 index 0000000..6bdb213 --- /dev/null +++ b/code/tcp_chat_server.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include + +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; +using error_handler = std::function; + +class session: public std::enable_shared_from_this { + public: + session(tcp::socket&& socket) : socket(std::move(socket)) {} + + 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(); + } + + void post(const std::string& message) { + bool idle = outgoing.empty(); + outgoing.push(message); + + if (idle) { + async_write(); + } + } + + private: + void async_read() { + io::async_read_until( + socket, + streambuf, + "\n", + std::bind(&session::on_read, shared_from_this(), _1, _2)); + } + + 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(); + } + } + + void async_write() { + io::async_write( + socket, + io::buffer(outgoing.front()), + std::bind(&session::on_write, shared_from_this(), _1, _2)); + } + + 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(); + } + } + + tcp::socket socket; + io::streambuf streambuf; + std::queue outgoing; + message_handler on_message; + error_handler on_error; +}; + +class server { + public: + server(io::io_context& io_context, std::uint16_t port) : + io_context(io_context), + acceptor(io_context, tcp::endpoint(tcp::v4(), port)) {} + + void async_accept() { + socket.emplace(io_context); + + acceptor.async_accept(*socket, [&](error_code error) { + auto client = std::make_shared(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(); + }); + } + + void post(const std::string& message) { + for (auto& client : clients) { + client->post(message); + } + } + + private: + io::io_context& io_context; + tcp::acceptor acceptor; + std::optional socket; + std::unordered_set> clients; +}; + +int main() { + io::io_context io_context; + server srv(io_context, 15001); + srv.async_accept(); + io_context.run(); + return 0; +}