├── .clang-format ├── Makefile ├── README.md ├── TUTORIAL.md ├── code ├── simple_server.cpp ├── tcp_async_server.cpp └── tcp_chat_server.cpp └── norg ├── readme.norg └── tutorial.norg /.clang-format: -------------------------------------------------------------------------------- 1 | AccessModifierOffset: -2 2 | AlignAfterOpenBracket: AlwaysBreak 3 | AlignConsecutiveMacros: false 4 | AlignConsecutiveAssignments: false 5 | AlignConsecutiveDeclarations: false 6 | AlignEscapedNewlines: DontAlign 7 | AlignOperands: false 8 | AlignTrailingComments: false 9 | AllowAllArgumentsOnNextLine: false 10 | AllowAllConstructorInitializersOnNextLine: false 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: Empty 15 | AllowShortIfStatementsOnASingleLine: Never 16 | AllowShortLambdasOnASingleLine: All 17 | AllowShortLoopsOnASingleLine: false 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: true 20 | AlwaysBreakTemplateDeclarations: Yes 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BreakBeforeBinaryOperators: NonAssignment 24 | BreakBeforeBraces: Attach 25 | BreakBeforeTernaryOperators: true 26 | BreakConstructorInitializers: AfterColon 27 | BreakInheritanceList: AfterColon 28 | BreakStringLiterals: false 29 | ColumnLimit: 80 30 | CompactNamespaces: false 31 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 32 | ConstructorInitializerIndentWidth: 4 33 | ContinuationIndentWidth: 4 34 | Cpp11BracedListStyle: true 35 | DerivePointerAlignment: false 36 | FixNamespaceComments: true 37 | IncludeBlocks: Regroup 38 | IncludeCategories: 39 | - Regex: '^' 40 | Priority: 2 41 | SortPriority: 0 42 | CaseSensitive: false 43 | - Regex: '^<.*\.h>' 44 | Priority: 1 45 | SortPriority: 0 46 | CaseSensitive: false 47 | - Regex: '^<.*' 48 | Priority: 2 49 | SortPriority: 0 50 | CaseSensitive: false 51 | - Regex: '.*' 52 | Priority: 3 53 | SortPriority: 0 54 | CaseSensitive: false 55 | IncludeIsMainRegex: '([-_](test|unittest))?$' 56 | IndentCaseLabels: true 57 | IndentPPDirectives: BeforeHash 58 | IndentWidth: 4 59 | IndentWrappedFunctionNames: false 60 | KeepEmptyLinesAtTheStartOfBlocks: false 61 | MaxEmptyLinesToKeep: 1 62 | NamespaceIndentation: Inner 63 | PointerAlignment: Left 64 | ReflowComments: false 65 | SortIncludes: true 66 | SortUsingDeclarations: true 67 | SpaceAfterCStyleCast: false 68 | SpaceAfterLogicalNot: false 69 | SpaceAfterTemplateKeyword: false 70 | SpaceBeforeAssignmentOperators: true 71 | SpaceBeforeCpp11BracedList: true 72 | SpaceBeforeCtorInitializerColon: true 73 | SpaceBeforeInheritanceColon: false 74 | SpaceBeforeParens: ControlStatements 75 | SpaceBeforeRangeBasedForLoopColon: true 76 | SpaceInEmptyParentheses: false 77 | SpacesBeforeTrailingComments: 2 78 | SpacesInAngles: false 79 | SpacesInCStyleCastParentheses: false 80 | SpacesInContainerLiterals: false 81 | SpacesInParentheses: false 82 | SpacesInSquareBrackets: false 83 | Standard: Cpp11 84 | TabWidth: 4 85 | UseTab: Never 86 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-md: 2 | nvim --headless -c "execute 'Neorg export to-file TUTORIAL.md' | sleep 100m | q" norg/tutorial.norg 3 | markdown-toc -i TUTORIAL.md 4 | nvim --headless -c "execute 'Neorg export to-file README.md' | sleep 100m | q" norg/readme.norg 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📚 Boost.Asio: Асинхронность в C++ 2 | 3 | Руководство по созданию клиент-серверных систем с помощью C++, библиотек Boost.Asio и Boost.Beast. 4 | 5 |

6 | 7 | [ЧИТАТЬ ОНЛАЙН](./TUTORIAL.md) 8 | 9 |

10 | 11 |
12 | 13 | Руководство находится в стадии перевода 14 | 15 |
16 | 17 | 18 | 19 | # 💡 О руководстве 20 | 21 | Это руководство является вольным переводом [серии туториалов](https://dens.website/tutorials/cpp-asio). Поскольку перевод вольный, в нем могут быть некоторые отступления от изначального текста. 22 | 23 | 24 | # 🎈 Помощь в переводе 25 | 26 | В переводе могут быть некоторые неточности, которые неправильно преподносят суть тех или иных вещей. Если вы нашли подобную неточность, откройте Issue или создайте Pull Request. Спасибо! -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | # Содержание 2 | 3 | 4 | 5 | - [Введение](#%D0%B2%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5) 6 | - [TCP и UDP](#tcp-%D0%B8-udp) 7 | * [Transmission Control Protocol — TCP](#transmission-control-protocol--tcp) 8 | * [User Datagram Protocol — UDP](#user-datagram-protocol--udp) 9 | - [Самый простой сервер](#%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) 10 | - [Прощаемся с синхронностью](#%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) 11 | - [Асинхронный 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) 12 | - [Обработка ошибок](#%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) 13 | * [Синхронные функции](#%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) 14 | + [Пример. Исключения](#%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) 15 | + [Пример. Возвращаемое значение](#%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) 16 | * [Асинхронные функции](#%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) 17 | * [error_code](#error_code) 18 | - [Дальнейшее изучение](#%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) 19 | - [TCP чат-сервер](#tcp-%D1%87%D0%B0%D1%82-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80) 20 | - [Упрощаем код](#%D1%83%D0%BF%D1%80%D0%BE%D1%89%D0%B0%D0%B5%D0%BC-%D0%BA%D0%BE%D0%B4) 21 | 22 | 23 | 24 | # Введение 25 | 26 | Данное руководство посвящено работе асинхронного ввода-вывода, который в 27 | основном используется для сетевого взаимодействия. Для лучшего понимания 28 | происходящего, вы должны быть знакомы с современным C++, STL и Boost, а также 29 | с базовыми принципами сетевого взаимодействия и многопоточности. 30 | 31 | Мы будем использовать `Boost.Asio`, `Boost.Beast`, а также `C++20 Networking 32 | library`. Чтобы добиться асинхронности, мы будем использовать _обработчики 33 | завершения_, _сопрограммы_ (или корутины) и _фиберы_. 34 | 35 | Чтобы скомпилировать исходный код из примеров, вам понадобиться установить 36 | компилятор, поддерживающий стандарт `C++17`, а также библиотеку `Boost`. При 37 | компиляции вам потребуется добавить `Boost` в `include directories` и 38 | слинковать исходный код вашего приложения с ним. 39 | 40 | На самом деле, для большинства примеров достаточно скомпилировать 41 | `boost/libs/system/src/error_code.cpp`, поскольку остальная часть исходного 42 | кода библиотеки `Boost` — это header-only библиотеки. 43 | 44 | Обычно сетевое взаимодействие считается очень сложным предметом для изучения. 45 | Неужели это действительно так сложно? Что ж, ответ — и да, и нет. Потребуется 46 | время, чтобы стать экспертом в этой области, однако мы попробуем сделать так, 47 | чтобы вам было понятно то, что происходит в этом руководстве. 48 | 49 | Когда вы разрабатываете какое-либо приложение, вам следует использовать 50 | пространства имен (namespaces) и псевдонимы типов (type aliases), чтобы код 51 | было удобно читать. Мы начнем это делать позднее, после того, как у вас 52 | появится четкое понимание откуда берутся те или иные вещи. Поэтому первое 53 | время вы будете видеть что-то по типу `boost::asio::ip::tcp::socket`. Конечно 54 | же, в реальном коде это должно быть заменено на что-то вроде `tcp::socket`. 55 | 56 | Двумя важными элементами сетевого взаимодействия являются _клиенты_ и 57 | _серверы_. Обычно подобные руководства начинаются с изучения работы клиента, 58 | поскольку это более простая тема для рассмотрения. Однако в этом руководстве 59 | мы начнем с серверов. Почему? Во-первых, сервера — это то место, где C++ 60 | проявляет себя с наилучшей стороны, а во-вторых, сервера не так страшны, как 61 | кажутся на первый взгляд. 62 | 63 | На этом вступление окончено. Теперь вы готовы приступить к погружению в 64 | сетевое программирование на C++. 65 | 66 | 67 | # TCP и UDP 68 | 69 | 70 | Существует два основных протокола транспортного уровня, которые мы будем 71 | использовать — TCP и UDP. Протокол — это набор соглашений о том, как должны 72 | передаваться данные по сети. 73 | 74 | 75 | ## Transmission Control Protocol — TCP 76 | 77 | 78 | TCP-соединение очень похоже на файл: мы открываем его, считываем из него 79 | какие-то данные, записываем какие-то данные и закрываем его. Однако 80 | существуют некоторые ограничения: 81 | - При работе с файлом мы можем узнать его размер. В случае TCP-соединения 82 | это невозможно. 83 | - Вы можете изменять положение указателя, когда работаете с файлом. Этот 84 | трюк также нельзя провернуть с TCP-соединением. 85 | 86 | Другими словами, файл предоставляет вам произвольный доступ, в то время как 87 | TCP-соединение представляет собой двунаправленный последовательный поток. 88 | 89 | 90 | ## User Datagram Protocol — UDP 91 | 92 | 93 | Информация, передаваемая по протоколу UDP, представляет собой непрерывный 94 | кусок данных. По сравнению с TCP, у UDP нет соединений. Невозможно получить 95 | только часть данных, отправленных приложением. Вы либо получите все данные, 96 | либо ничего. На данный момент вам нужно знать о UDP следующее: 97 | - В UDP отсутствуют соединения, поскольку это не поток данных. Из этого 98 | следует, что нет необходимости создавать или закрывать UDP-сокет. Все, что 99 | вам требуется — это отправлять или получать данные. 100 | - Буфер, используемый для получения UDP-пакета должен быть достаточно 101 | большим, чтобы вместить весь пакет целиком. В противном случае, вы ничего 102 | не получите. Из этого следует, что необходимо заранее знать верхнюю 103 | границу размера пакетов, которые вы собрались получать. 104 | - Порядок входящих пакетов, как правило, не соответствует порядку их 105 | отправки. Это означает, что необходимо самостоятельно контролировать 106 | порядок пакетов. 107 | - Нет никаких гарантий, что все отправленные пакеты будут доставлены. Это 108 | означает, что потеря UDP-пакетов — обычное дело. Следовательно, необходимо 109 | самостоятельно контролировать, что все отправленные UDP-пакеты доставлены. 110 | 111 | Как вы можете понять, UDP немного сложнее в использовании, чем TCP. Тем не 112 | менее, у UDP есть свои преимущества, которые мы обсудим позднее. 113 | 114 | Это все, что вам необходимо знать о протоколах на данный момент. Значит, мы 115 | можем двигаться дальше. 116 | 117 | 118 | 119 | # Самый простой сервер 120 | 121 | 122 | Согласно [Википедии](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)), 123 | > Сервер — программный компонент вычислительной системы, выполняющий 124 | сервисные (обслуживающие) функции по запросу клиента, предоставляя ему доступ 125 | к определённым ресурсам или услугам. 126 | 127 | Это определение очень точно подмечает тот факт, что сервер — это всего лишь 128 | приложение, которое получает какие-то данные от других приложений и 129 | возвращает некоторые данные обратно. 130 | 131 | Мы начнем с самого простого сервера, который приходит на ум — эхо UDP-сервер. 132 | Он выполняет следующие действия: 133 | - Получает любые данные, которые были отправлены на UDP-порт 15001. 134 | - Отправляет полученные данные обратно отправителю «как есть». 135 | 136 | На самом деле вы можете выбрать практически любой порт для вашего сервера. 137 | Существует множество часто используемых портов для различных служб, которые 138 | вы можете найти здесь: 139 | [Список портов 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). 140 | Однако, как правило, только несколько из этих служб используется 141 | одновременно в недавно установленной ОС. 142 | 143 | Теперь давайте взглянем на следующий [исходный код](./code/simple_server.cpp): 144 | 145 | ```cpp 146 | #include 147 | 148 | int main() { 149 | std::uint16_t port = 15001; 150 | 151 | boost::asio::io_context io_context; 152 | boost::asio::ip::udp::endpoint receiver(boost::asio::ip::udp::v4(), port); 153 | boost::asio::ip::udp::socket socket(io_context, receiver); 154 | 155 | while (true) { 156 | char buffer[65536]; 157 | boost::asio::ip::udp::endpoint sender; 158 | std::size_t bytes_transferred = 159 | socket.receive_from(boost::asio::buffer(buffer), sender); 160 | socket.send_to(boost::asio::buffer(buffer, bytes_transferred), sender); 161 | } 162 | 163 | return 0; 164 | } 165 | ``` 166 | 167 | Вам даже не обязательно отдельно скачивать `.cpp` файл сервера, поскольку 168 | вышеприведенный код — это полноценный эхо UDP-сервер. Мы не реализовали 169 | логирование и обработку ошибок, чтобы код выглядел максимально просто. Об 170 | обработке ошибок мы поговорим позднее. Давайте разберемся, что происходит в 171 | этом коде: 172 | - `boost::asio::io_context` — основной поставщик услуг ввода-вывода. В данный 173 | момент вы можете рассматривать его как исполнителя (executor) 174 | запланированных задач. Вы поймете его назначение сразу после того, как мы 175 | перейдем к асинхронному потоку управления, что произойдет очень скоро. 176 | - `boost::asio::ip::udp::endpoint` — это пара IP-адреса и порта. 177 | - `boost::asio::ip::udp::socket` — это сокет. Вы можете рассматривать его как 178 | дескриптор файла, предназначенный для сетевого взаимодействия. Обычно, 179 | когда вы открываете файл, вы получаете дескриптор файла. Когда вы 180 | взаимодействуете по сети, вы используете сокет. 181 | - Каждый сокет прикреплен к некоторому `io_context`, а потому каждый сокет 182 | конструируется с помощью ссылки на `io_context`. Второй параметр 183 | конструктора сокета — `endpoint` — IP-адрес и порт, который используется 184 | для получения входящих дейтаграмм (в случае UDP) или соединений (в случае 185 | TCP). 186 | - `boost::asio::ip::udp::v4()` возвращает объект, который в данный момент вы 187 | должны рассматривать как просто сетевой интерфейс UDP по умолчанию. 188 | - `boost::asio::buffer()` — это представление буфера, которое содержит 189 | указатель и размер, причем это представление не владеет памятью. В нашем 190 | случае оно указывает на массив `char`. 191 | - `socket::receive_from` ожидает входящий UDP-пакет, заполняет `buffer` 192 | полученными данными, а также заполняет `sender` информацией об отправителе, 193 | которая также включает в себя пару IP-адреса и порта. 194 | - `socket::send_to` отправляет UDP-пакет, используя данные из представления 195 | буфера. Получатель пакета передается вторым аргументом. В нашем случае 196 | получателем является отправитель, поскольку речь идет об эхо-сервере. 197 | 198 | Итак, мы сделали следующее: 199 | - Создали UDP-сокет и настроили его на ожидание UDP-пакетов на порту 15001. 200 | - Запустили бесконечный цикл, в котором ожидаем входящие UDP-пакеты, а после 201 | получения отправляем их обратно отправителю. 202 | 203 | Поздравляем! Вы только что создали ваш первый сервер с помощью C++ и 204 | Boost.Asio! 205 | 206 | 207 | # Прощаемся с синхронностью 208 | 209 | 210 | В реальной жизни синхронный ввод-вывод практически бесполезен. Даже если вы 211 | пишите простой клиент с единственным сетевым подключением, то скорее всего 212 | ваше приложение будет выполнять такие функции как управление пользовательским 213 | интерфейсом, чтение пользовательского ввода и т. п. Однако использование 214 | синхронного ввода-вывода означает, что все его операции являются 215 | блокирующими. Следовательно ваше приложение не сможет выполнять каких либо 216 | операций до тех пор, пока не завершатся операции с вводом-выводом. 217 | 218 | Вы можете обойти это ограничение с помощью создания дополнительных потоков. 219 | Например, один поток может обрабатывать ввод-вывод, а другой управлять 220 | пользовательским интерфейсом. Однако такой подход приведет к усложнению 221 | вашего приложения, поскольку в какой-то момент вам придется синхронизировать 222 | эти потоки. Более того, не существует безопасного способа отменить 223 | блокирующую операцию ввода-вывода из другого потока. Хотя это и может 224 | работать так, как вы ожидаете, но в целом это не безопасная операция. А 225 | потому вы можете столкнуться с неопределенным поведением, если что-нибудь 226 | измениться в вашем рабочем окружении (например, если вы скомпилируете код для 227 | новой платформы, с которой вы раньше не работали). 228 | 229 | Асинхронный подход лишен этих недостатков. Проще говоря, выполнение 230 | асинхронного кода можно представить так: «Начни делать это в фоновом режиме, 231 | а после того, как закончишь, вызови эту функцию. Тем временем я займусь 232 | другими задачами, которые необходимо выполнить». Таким образом, выполнение 233 | асинхронного кода — это неблокирующая операция, а значит вы можете совершать 234 | другие действия, пока ваши задачи выполняются в фоновом режиме. Кроме того, 235 | асинхронные задачи могут быть безопасно отменены в любое время. 236 | 237 | Вспомним [код из предыдущего раздела](#самый-простой-сервер), в котором 238 | используется синхронный подход: 239 | ```cpp 240 | // Эта операция заблокирует поток управление до тех пор, пока не будет получено сообщение 241 | std::size_t bytes_transferred = socket.receive_from(buffer, sender); 242 | std::cout << "Message is received, message size is " << bytes_transferred; 243 | ``` 244 | 245 | Асинхронные версии функций ввода-вывода в Boost.Asio начинаются с приставки 246 | `async_`. Теперь взгляните на тот же код, переписанный в асинхронном стиле: 247 | ```cpp 248 | // Эта операция не блокирующая: выполнение кода продолжится сразу после вызова функции 249 | socket.async_receive_from( 250 | buffer, 251 | sender, 252 | [&](boost::system::error_code error, std::size_t bytes_transferred) { 253 | // Эта лямбда-функция будет вызвана после получения сообщения 254 | std::cout << "Message is received, message size is " 255 | << bytes_transferred; 256 | }); 257 | ``` 258 | 259 | В C++ нам нравится держать все под контролем. Первое, что вы должны спросить: 260 | «Эй, где именно выполняется это фоновая задача? Должны ли мы создавать поток 261 | для нее?». Вы получите ответ на этот вопрос в следующем разделе. А пока, 262 | пришло время сказать «прощай» синхронному коду и двигаться дальше. 263 | 264 | 265 | # Асинхронный TCP-сервер 266 | 267 | 268 | Пришло время взглянуть на наш первый асинхронный TCP-сервер. Это последний 269 | раз, когда мы не используем пространства имен (namespaces) и псевдонимы типов 270 | (type aliases). В дальнейшем вы уже должны понимать откуда берутся те или 271 | иные вещи. 272 | 273 | Теперь наш сервер будет делать следующее: 274 | - Слушать порт 15001 и ожидать входящее TCP-соединение. 275 | - Принимать входящее соединение. 276 | - Читать данные из соединения до тех пор, пока не встретится символ конца 277 | строки (т. е. символ `\n`). 278 | - Выводить полученные данные в стандартный вывод. 279 | - Закрывать соединение. 280 | 281 | Теперь давайте взглянем на полноценный пример такого сервера. Ниже мы все 282 | разложим по полочкам и посмотрим как все устроено. Как и прежде, мы 283 | пренебрегаем обработкой ошибок, чтобы код выглядел более понятным. Об обработке 284 | ошибок мы поговорим позже. 285 | 286 | ```cpp 287 | #include 288 | #include 289 | #include 290 | 291 | class session: public std::enable_shared_from_this { 292 | public: 293 | session(boost::asio::ip::tcp::socket&& socket) : 294 | socket(std::move(socket)) {} 295 | 296 | void start() { 297 | boost::asio::async_read_until( 298 | socket, 299 | streambuf, 300 | '\n', 301 | [self = shared_from_this()]( 302 | boost::system::error_code error, 303 | std::size_t bytes_transferred) { 304 | std::cout << std::istream(&self->streambuf).rdbuf(); 305 | }); 306 | } 307 | 308 | private: 309 | boost::asio::ip::tcp::socket socket; 310 | boost::asio::streambuf streambuf; 311 | }; 312 | 313 | class server { 314 | public: 315 | server(boost::asio::io_context& io_context, std::uint16_t port) : 316 | io_context(io_context), 317 | acceptor( 318 | io_context, 319 | boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) {} 320 | 321 | void async_accept() { 322 | socket.emplace(io_context); 323 | 324 | acceptor.async_accept(*socket, [&](boost::system::error_code error) { 325 | std::make_shared(std::move(*socket))->start(); 326 | async_accept(); 327 | }); 328 | } 329 | 330 | private: 331 | boost::asio::io_context& io_context; 332 | boost::asio::ip::tcp::acceptor acceptor; 333 | std::optional socket; 334 | }; 335 | 336 | int main() { 337 | boost::asio::io_context io_context; 338 | server srv(io_context, 15001); 339 | srv.async_accept(); 340 | io_context.run(); 341 | return 0; 342 | } 343 | ``` 344 | 345 | По сравнению с предыдущим сервером, этот код занимает значительно больше 346 | места. Но не стоит паниковать, здесь всего 57 строк кода, которые 347 | представляют из себя полноценный асинхронный TCP-сервер. 348 | 349 | В прошлый раз мы упомянули, что все функции с приставкой `async_` выполняются 350 | в фоновом режиме. Так где же находится этот «фоновый режим»? Что ж, фоновый 351 | режим находится _где-то_ внутри операционной системы. На самом деле, вам не 352 | нужно заботиться о том, как это происходит. Единственное, что должно вас 353 | волновать — откуда вызываются обработчики завершения. И это происходит внутри 354 | `io_context.run()`. Давайте взглянем на функцию `main`: 355 | ```cpp 356 | int main() { 357 | boost::asio::io_context io_context; 358 | server srv(io_context, 15001); 359 | srv.async_accept(); 360 | io_context.run(); 361 | return 0; 362 | } 363 | ``` 364 | 365 | Функция `boost::asio::io_context::run` — это своего рода функция цикла 366 | событий (event loop), которая управляет всеми операциями ввода-вывода. При 367 | вызове функции `run` поток управления блокируется до тех пор, пока не 368 | выполнятся все асинхронные операции, связанные с `io_context`. Все операции с 369 | приставкой `async_` связаны с каким-либо `io_context`. В некоторых языках 370 | программирования (например, JavaScript) функция цикла событий спрятана от 371 | разработчика. Но в C++ нам нравится все держать под контролем, поэтому мы 372 | решаем, где именно функция цикла событий будет запущена. 373 | 374 | Теперь давайте рассмотрим класс `server`. Здесь встречается сразу несколько 375 | новых вещей, которые находятся в `private` секции класса: 376 | - `boost::asio::ip::tcp::socket` — этот тот же самый сокет, что и до этого, 377 | только теперь он работает в рамках протокола TCP (вместо UDP, как это было 378 | ранее). 379 | - `boost::asio::ip::tcp::acceptor` — это объект, который принимает входящие 380 | соединения. 381 | 382 | Если вы посмотрите на конструктор класса `acceptor`, вы увидите то, что он 383 | очень похож на метод `receive_from` у UDP-сокета: 384 | ```cpp 385 | acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) 386 | ``` 387 | 388 | Передав такие аргументы конструктору, мы получим, что `acceptor` будет 389 | слушать входящие TCP-соединения на любом сетевом интерфейсе на указанном 390 | порту. 391 | 392 | Теперь давайте рассмотрим вызов функцию `async_accept` у `acceptor`: 393 | ```cpp 394 | acceptor.async_accept(*socket, [&](boost::system::error_code error) { 395 | std::make_shared(std::move(*socket))->start(); 396 | async_accept(); 397 | }); 398 | ``` 399 | 400 | Словами это можно описать так: «Ожидай входящее соединение, а после того как 401 | установишь его, свяжи это соединение с сокетом и вызови обработчик 402 | завершения». Как вы помните, функции с приставкой `async_` не блокируют 403 | вызывающий поток. 404 | 405 | Итак, сервер ожидает входящее соединение После установления соединения сервер 406 | создает объект сессии. При создании мы перемещаем сокет, связанный с 407 | установленным соединением, внутрь объекта сессии. После этого сервер начинает 408 | ожидать следующее входящее соединение. 409 | 410 | Обратите внимание, что серверу все равно, что происходит с установленным 411 | соединением. Сервер сразу же начинает ожидать следующее входящее соединение 412 | не беспокоясь о том, что происходит с предыдущим соединением. Установленные 413 | соединения выполняются в фоновом режиме. Одновременно может существовать 414 | почти любое количество соединений (количество открытых файловых дескрипторов 415 | ограничено ОС), выполняемых в фоновом режиме. Это и есть принцип работы 416 | асинхронного ввода-вывода. 417 | 418 | Отлично, теперь мы знаем, как работает наш сервер. Давайте рассмотрим класс 419 | `session`. Сессия — это класс, который поддерживает соединение. Сессия 420 | содержит некоторые данные, связанные с соединением и предоставляет некоторый 421 | набор функций, связанный с соединением. Давайте рассмотрим функцию `start`: 422 | ```cpp 423 | void start() { 424 | boost::asio::async_read_until( 425 | socket, 426 | streambuf, 427 | '\n', 428 | [self = shared_from_this()]( 429 | boost::system::error_code error, 430 | std::size_t bytes_transferred) { 431 | std::cout << std::istream(&self->streambuf).rdbuf(); 432 | }); 433 | } 434 | ``` 435 | 436 | Дословно, код выполняет следующее: «Читай данные из сокета в `streambuf`, а 437 | когда встретишь символ `"\n"`, остановись и вызови обработчик завершения». 438 | 439 | `boost::asio::streambuf` — это класс, унаследованный от `std::streambuf`. 440 | Можете рассматривать его как реализацию `streambuf` в библиотеке 441 | `Boost.Asio`. 442 | 443 | Итого, сессия считывает данные из сокета до тех пор, пока не встретит символ 444 | `"\n"`, а после записывает полученные данные в стандартный вывод. 445 | 446 | Обратите внимание, что класс `session` унаследован от класса 447 | `std::enable_shared_from_this`. Также заметим, что сессия захватывает в 448 | лямбду обработчика завершения указатель на разделяемую копию себя посредством 449 | `shared_from_this`. Мы делаем это, чтобы продлить время жизни сессии до тех 450 | пор, пока не будет вызван обработчик завершения. После этого нам не нужно 451 | ничего делать — указатель на разделяемый объект выйдет из области видимости и 452 | сразу же уничтожится после завершения работы обработчика. В большинстве 453 | случаев (но не во всех), это обычный способ для работы с сессиями. 454 | 455 | Теперь вы знаете как написать простой асинхронный TCP-сервер и как он 456 | работает. Последнее, что нам необходимо сделать — протестировать в реальной 457 | жизни. Запустим сервер в терминале: 458 | ``` 459 | ./server 460 | ``` 461 | 462 | Теперь в другом терминале запустим `telnet`, подключимся к 15001 порту, 463 | введем `Hello asio!` и нажмем `Enter` (который введет ожидаемый символ 464 | `"\n"`): 465 | ``` 466 | telnet localhost 15001 467 | Hello asio! 468 | ``` 469 | 470 | В первом терминале вы должны увидеть это сообщение: 471 | ``` 472 | ./server 473 | Hello asio! 474 | ``` 475 | 476 | Круто! Вы только начали, а уже знаете, как написать почти любой 477 | профессиональный асинхронный TCP-сервер с помощью современного C++ и 478 | Boost.Asio. Поздравляем! 479 | 480 | 481 | # Обработка ошибок 482 | 483 | 484 | 485 | ## Синхронные функции 486 | 487 | 488 | В Boost.Asio все синхронные функции ввода-вывода имеют две перегрузки для 489 | обработки ошибок: первая выбрасывает исключение, а вторая возвращает ошибку 490 | по ссылке (подход с возвращением значения). 491 | 492 | Исключения, которые выбрасывается из функций Boost.Asio, являются 493 | экземплярами класса `boost::system::system_error`, который, в свою очередь, 494 | унаследован от класса `std::runtime_error`. 495 | 496 | При возврате ошибки по ссылке, используется экземпляр класса 497 | `boost::system::error_code`. 498 | 499 | 500 | ### Пример. Исключения 501 | 502 | 503 | ```cpp 504 | try { 505 | socket.connect(endpoint); 506 | } catch (const boost::system::system_error& e) { 507 | std::cerr << e.what() << "\n"; 508 | } 509 | ``` 510 | 511 | Вы можете получить `error_code` из `system_error`, вызвав метод `code()`: 512 | ```cpp 513 | catch (const boost::system::system_error& e) { 514 | boost::system::error_code error = e.code(); 515 | std::cerr << error.message() << "\n"; 516 | } 517 | ``` 518 | 519 | 520 | ### Пример. Возвращаемое значение 521 | 522 | 523 | Если вы не хотите возиться с исключениями, вы можете использовать 524 | перегрузку, чтобы получить ошибку по ссылке: 525 | ```cpp 526 | boost::system::error_code error; 527 | socket.connect(endpoint, error); 528 | 529 | if (error) { 530 | std::cerr << error.message() << "\n"; 531 | } 532 | ``` 533 | 534 | 535 | 536 | ## Асинхронные функции 537 | 538 | 539 | Асинхронные функции ввода-вывода не выбрасывают исключений. Вместо этого, 540 | они передают `boost::system::error_code` в обработчик завершения. Поэтому 541 | для того, чтобы проверить, была ли операция завершена успешно, вы должны 542 | написать что-то наподобие этого: 543 | ```cpp 544 | socket.async_connect(endpoint, [&](boost::system::error_code error) { 545 | if (!error) { 546 | // Асинхронная операция успешно завершена 547 | } else { 548 | // Что-то пошло не так 549 | std::cerr << error.message() << "\n"; 550 | } 551 | }); 552 | ``` 553 | 554 | 555 | 556 | ## error_code 557 | 558 | 559 | Рассмотрим некоторый функционал `error_code`, который может вам пригодиться 560 | при работе с ним. 561 | - Если вы хотите получить удобочитаемое описание ошибки из 562 | `boost::system::error_code`, нужно вызвать метод `message()`, который 563 | вернет `std::string` с описанием ошибки. 564 | - Если вы не хотите выделять дополнительную память для `std::string`, вы 565 | можете использовать перегрузку `message(char const* buffer, std::size_t 566 | size)`. 567 | - Если вы хотите получить системный код (который имеет тип `int`) ошибки из 568 | `error_code`, вызовите метод `value()`. 569 | 570 | Если удаленное соединение было закрыто, то будет выброшена `end-of-file` 571 | ошибка. В некоторых случаях вы не хотите рассматривать `end-of-file` ошибку 572 | как ошибку приложения. Например, вы хотите получить некоторое сообщение от 573 | удаленного хоста. После передачи сообщения хост разрывает соединение, что 574 | является нормальным поведением. Для обработки такой ситуации вы могли бы 575 | написать: 576 | ```cpp 577 | socket.async_receive( 578 | buffer, 579 | [&](boost::system::error_code error, std::size_t bytes_transferred) { 580 | if (!error) { 581 | // Асинхронная операция выполнена успешно. 582 | // Соединение все еще установлено 583 | } else if (error == boost::asio::error::eof) { 584 | // Соединение было разорвано. 585 | // В буфере по-прежнему хранятся полученные данные, 586 | // численно равные `bytes_transferred` (в байтах) 587 | } else { 588 | // Что-то пошло не так 589 | std::cerr << error.message() << "\n"; 590 | } 591 | }); 592 | ``` 593 | 594 | У вас может возникнуть вопрос: как передавать `boost::system::error_code` в 595 | функции? По ссылке или по значению? С одной стороны, если вы откроете 596 | документацию Boost.Asio, то увидите, что автор библиотеки передает 597 | `error_code` по ссылке. С другой стороны, `error_code` содержит в себе один 598 | `int`, один `bool` и один сырой указатель. 599 | 600 | Вспомним класс `std::string_view`: он содержит в себе один `std::size_t` и 601 | один сырой указатель. Поскольку его размер невелик, его стоит передавать по 602 | значению. В нашем случае `error_code` занимает столько же места, сколько и 603 | `std::string_view` (в большинстве случаев это так, зависит от платформы). 604 | Поэтому в нашем коде мы передаем `error_code` по значению. 605 | 606 | 607 | # Дальнейшее изучение 608 | 609 | 610 | В следующем разделе мы рассмотрим пример более масштабного сервера — TCP 611 | чат-сервера. Но перед этим вы должны кое-что узнать. 612 | 613 | У сокета есть функционал, о котором мы еще не говорили. Мы можем узнать у 614 | сокета о его конечных устройствах с обеих сторон соединения: 615 | ```cpp 616 | boost::asio::ip::tcp::endpoint endpoint; 617 | endpoint = socket.local_endpoint(); // IP:порт локальной стороны соединения 618 | endpoint = socket.remote_endpoint(); // IP:порт удаленной стороны соединения 619 | ``` 620 | 621 | Обратите внимание, что эти функции могут выбросить исключение. Если вы не 622 | хотите возиться с исключениями, вы можете использовать перегрузку, чтобы 623 | получить ошибку по ссылке: 624 | ```cpp 625 | boost::system:error_code error; 626 | auto endpoint = socket.remote_endpoint(error); 627 | ``` 628 | 629 | `endpoint` можно использовать с `iostreams`: 630 | ```cpp 631 | boost::system:error_code error; 632 | auto endpoint = socket.remote_endpoint(error); 633 | std::cout << "Remote endpoint: " << endpoint << "\n"; 634 | ``` 635 | 636 | Если запустить этот код, вы скорее всего увидите что-то на подобии этого: 637 | ```sh 638 | Remote endpoint: 127.0.0.1:38529 639 | ``` 640 | 641 | Иногда вам может понадобиться отменить асинхронную операцию, которая была 642 | запланирована ранее. Единственный надежный и переносимый способ сделать это — 643 | закрыть связанный с операцией сокет. 644 | ```cpp 645 | boost::asio::async_read(socket, buffer, completion_handler); 646 | // ... 647 | socket.close(); 648 | ``` 649 | 650 | Обратите внимание, что `socket::close` может выбросить исключение. Как 651 | всегда, существует перегрузка для получения ошибки по ссылке: 652 | ```cpp 653 | boost::system::error_code error; 654 | socket.close(error); 655 | ``` 656 | 657 | Также существует метод `socket::cancel`, который позволяет отменить 658 | выполняемую в данный момент асинхронную операцию без закрытия сокета. Однако 659 | это поведение специфично для конкретной платформы. Метод может работать так, 660 | как вы этого ожидание, а может быть и проигнорирован ОС. Такая особенность 661 | почти наверняка говорит о том, что система имеет плохой дизайн. Старайтесь 662 | избегать этой операции. 663 | 664 | Когда вы отправляете данные по сети, вы всегда знаете точно, сколько байт 665 | должно быть передано. Когда вы получите данные,вы также можете ожидать 666 | получения некоторого фиксированного количества байтов. Однако в некоторых 667 | случаях вам нужно считывать данные до тех пор, пока не выполнится какое-то 668 | условие. Например, до тех пор, пока не встретится определенная 669 | последовательность (например, символ `"\n"`). В этом случае использование 670 | `boost::asio::streambuf` может быть более удобным, чем использование буфера с 671 | фиксированным размером. При использовании этого контейнера вы должны 672 | установить верхнюю границу размера `streambuf`, передав максимальный размер в 673 | конструктор: 674 | ```cpp 675 | boost::asio::streambuf streambuf(65536); 676 | ``` 677 | 678 | В противном случае, размер буфера может расти до тех пор, пока не закончится 679 | память. После того, как вы обработали часть полученных данных, необходимо 680 | стереть эти данные из `streambuf`, чтобы размер буфера увеличивался. Это 681 | можно сделать с помощью метода `consume()`: 682 | ```cpp 683 | boost::asio::async_read_until( 684 | socket, 685 | streambuf, 686 | "\n", 687 | [&](boost::system::error_code error, std::size_t bytes_transferred) { 688 | // Обработка полученных данных 689 | // ... 690 | streambuf.consume(bytes_transferred); 691 | }); 692 | ``` 693 | 694 | Библиотека самостоятельно не ставит в очередь асинхронные операции. Чтобы 695 | запланировать новую задачу, вам необходимо дождаться завершения текущей 696 | задачи. Это значит, что вы должны управлять очередью задач самостоятельно. 697 | Конечно, это проблема, если мы говорим об одном и том же типе операций: 698 | несколько чтений или несколько записей. Однако операции `async_read` и 699 | `async_write` могут быть запланированы параллельно без каких-либо проблем. 700 | 701 | Временем жизни объекта сессии можно управлять разными способами. Это зависит 702 | от логики сервера. Иногда достаточно захватить указатель на разделяемый 703 | объект в обработчик завершения. Таким образом мы можем продлить время жизни 704 | сессии до тех пор, пока не завершится текущая асинхронная операция. 705 | 706 | Однако иногда серверу нужно знать обо всех активных клиентах. Например, чтобы 707 | иметь возможность получить доступ к каждому клиенту. Этого можно добиться, 708 | поместив указатели на клиентов в некоторый специальный контейнер. 709 | 710 | Иногда объект сессии должен оставаться в памяти тогда, когда отсутствуют 711 | запланированные задачи, т. е. нет обработчиков завершения, которые могли бы 712 | хранить указатель на разделяемый объект. Этого можно добиться, если хранить 713 | указатель на разделяемый объект сессии где-нибудь в другом месте (например, в 714 | контейнере сервера). Также этого можно достичь использованием сырых 715 | указателей. Работа с сырыми указателями может показаться странной — в конце 716 | концов, мы говорим о C++. Однако в некоторых особых случаях этот способ очень 717 | хорош для управления асинхронным взаимодействием. Мы обсудим метод с сырыми 718 | указателями позднее. 719 | 720 | Обычно сервер знает о классе сессии, с которым он работает. Однако классу 721 | сессии также может понадобиться передать некоторую информацию серверу. Это 722 | приводит нас к необходимости цикличной видимости. Для достижения этого мы 723 | могли бы использовать предварительное объявление (forward declaration) класса 724 | сервера. После чего мы бы передавали ссылку на сервер в конструктор сессии. 725 | Однако это не очень хороший дизайн. Более хороший способ решить эту проблему 726 | — использовать функции обработчиков событий: 727 | 728 | ```cpp 729 | using message_handler = std::function; 730 | 731 | // На стороне сервера 732 | void server::create_session() { 733 | auto client = std::make_shared([&](const std::string& message) { 734 | std::cout << "We got a message: " << message; 735 | }); 736 | } 737 | 738 | // На стороне клиента 739 | void session::session(message_handler&& handler) : 740 | on_message(std::move(handler)) {} 741 | 742 | void session::async_receive() { 743 | boost::asio::async_receive(socket, [...](...) { on_message(some_buffer); }); 744 | } 745 | ``` 746 | 747 | Более хороший не означает, что лучший. Есть несколько способов передавать 748 | данные между сессиями и сервером. Какой из них является лучшим — зависит от 749 | деталей реализации вашего приложения. 750 | 751 | Отлично, теперь вы знаете все, что нужно знать, чтобы рассмотреть следующий 752 | пример — простой TCP чат-сервер. 753 | 754 | 755 | # TCP чат-сервер 756 | 757 | 758 | Вы уже знаете какие вещи откуда берутся, поэтому отныне мы будем использовать 759 | псевдонимы типов, чтобы сделать названия типов короче. 760 | 761 | В этом разделе мы рассмотрим очень простой чат-сервер. Этот сервер не будет 762 | поддерживать пользовательские ники, цвета и другие аспекты, специфичные для 763 | конкретного пользователя. Мы отказываемся от этого, чтобы сервер был более 764 | простым. 765 | 766 | В предыдущем разделе мы подробно обсудили новые вещи, которые будут 767 | использоваться в этом сервере. Поэтому в этом разделе мы рассмотрим сервер 768 | лишь вкратце. 769 | 770 | Полный исходный код, который мы будем разбирать в этом разделе, вы можете 771 | найти [здесь](./code/tcp_chat_server.cpp). После чтения этого раздела качайте 772 | его, скомпилируйте, посмотрите как он работает. Попытайтесь самостоятельно 773 | понять, как все работает, основываясь на том, что вы узнали за все это время. 774 | В конце концов, вам нужно научиться понимать код. 775 | 776 | Ну что ж, начнем. Первое, что вы увидите в исходном коде — это включение 777 | заголовков и использование псевдонимов типов: 778 | ```cpp 779 | #include 780 | 781 | #include 782 | #include 783 | #include 784 | 785 | namespace io = boost::asio; 786 | using tcp = io::ip::tcp; 787 | using error_code = boost::system::error_code; 788 | using namespace std::placeholders; 789 | 790 | using message_handler = std::function; 791 | using error_handler = std::function; 792 | ``` 793 | 794 | Пока что все должно быть очевидно. Функция `main` выглядит точно также, как и 795 | в предыдущем примере (за исключением использования псевдонимов типов): 796 | ```cpp 797 | int main() { 798 | io::io_context io_context; 799 | server srv(io_context, 15001); 800 | srv.async_accept(); 801 | io_context.run(); 802 | return 0; 803 | } 804 | ``` 805 | 806 | В этот раз класс сервера и класс сессии немного больше, но поскольку мы уже 807 | разобрали часть кода, рассмотрим только ключевые моменты. 808 | 809 | Давайте начнем с ключевых моментов сессии. Функция `start` теперь принимает 810 | обработчики событий: 811 | ```cpp 812 | void start(message_handler&& on_message, error_handler&& on_error) { 813 | this->on_message = std::move(on_message); 814 | this->on_error = std::move(on_error); 815 | async_read(); 816 | } 817 | ``` 818 | 819 | Функция `post` добавляет в очередь сообщение, адресованное клиенту. Отправка 820 | сообщения начинается, если в данный момент не отправляется предыдущее 821 | сообщение: 822 | ```cpp 823 | void post(const std::string& message) { 824 | bool idle = outgoing.empty(); 825 | outgoing.push(message); 826 | 827 | if (idle) { 828 | async_write(); 829 | } 830 | } 831 | ``` 832 | 833 | Асинхронные функции чтения и записи выделены в отдельные методы класса 834 | сессии. Функция `async_read` считывает данные с удаленного клиента в 835 | `streambuf`, а функция `async_write` отправляет первое в очереди сообщение 836 | удаленному клиенту: 837 | ```cpp 838 | void async_read() { 839 | io::async_read_until( 840 | socket, 841 | streambuf, 842 | "\n", 843 | std::bind(&session::on_read, shared_from_this(), _1, _2)); 844 | } 845 | 846 | void async_write() { 847 | io::async_write( 848 | socket, 849 | io::buffer(outgoing.front()), 850 | std::bind(&session::on_write, shared_from_this(), _1, _2)); 851 | } 852 | ``` 853 | 854 | Обработчик чтения выполняет следующие действия: 855 | 1. форматирует сообщение, полученное от клиента; 856 | 2. передает отформатированное сообщение в обработчик сообщений; 857 | 3. начинает ожидать следующего сообщения. 858 | 859 | Кроме того, он также выполняет обработку ошибок: 860 | ```cpp 861 | void on_read(error_code error, std::size_t bytes_transferred) { 862 | if (!error) { 863 | std::stringstream message; 864 | message << socket.remote_endpoint(error) << ": " 865 | << std::istream(&streambuf).rdbuf(); 866 | streambuf.consume(bytes_transferred); 867 | on_message(message.str()); 868 | async_read(); 869 | } else { 870 | socket.close(error); 871 | on_error(); 872 | } 873 | } 874 | ``` 875 | 876 | Обработчик записи работает так: 877 | 1. удаляет сообщение из очереди; 878 | 2. если в очереди еще остались сообщения, начинает отправку следующего 879 | сообщения. 880 | 881 | Он также выполняет обработку ошибок: 882 | ```cpp 883 | void on_write(error_code error, std::size_t bytes_transferred) { 884 | if (!error) { 885 | outgoing.pop(); 886 | 887 | if (!outgoing.empty()) { 888 | async_write(); 889 | } 890 | } else { 891 | socket.close(error); 892 | on_error(); 893 | } 894 | } 895 | ``` 896 | 897 | У класса сессии следующие атрибуты: 898 | ```cpp 899 | tcp::socket socket; // Сокет клиента 900 | io::streambuf streambuf; // Буфер для входящих данных 901 | std::queue outgoing; // Очередь исходящих сообщений 902 | message_handler on_message; // Обработчик сообщений 903 | error_handler on_error; // Обработчик ошибок 904 | ``` 905 | 906 | Теперь давайте рассмотрим класс сервера. Начнем с атрибутов: 907 | ```cpp 908 | io::io_context& io_context; 909 | tcp::acceptor acceptor; 910 | std::optional socket; 911 | std::unordered_set clients; // Список подключенных клиентов 912 | ``` 913 | 914 | Функция `post` рассылает сообщение всем подключенным клиентам. Эта функция 915 | также используется в качестве обработчика сообщений (см. далее): 916 | ```cpp 917 | void post(const std::string& message) { 918 | for (auto& client : clients) { 919 | client->post(message); 920 | } 921 | } 922 | ``` 923 | 924 | Функция `async_accept` приветствует только что подключившегося клиента и 925 | сообщает всем остальным клиентам о новоприбывшем. Здесь также реализована 926 | обработка ошибок, которая в случае чего удаляет сессию из списка клиентов, 927 | после чего уведомляет об этом остальных клиентов: 928 | ```cpp 929 | void async_accept() { 930 | socket.emplace(io_context); 931 | 932 | acceptor.async_accept(*socket, [&](error_code error) { 933 | auto client = std::make_shared(std::move(*socket)); 934 | client->post("Welcome to chat\n\r"); 935 | post("We have a newcomer\n\r"); 936 | 937 | clients.insert(client); 938 | 939 | client->start( 940 | std::bind(&server::post, this, _1), 941 | [&, weak = std::weak_ptr(client)] { 942 | if (auto shared = weak.lock(); 943 | shared && clients.erase(shared)) { 944 | post("We are one less\n\r"); 945 | } 946 | }); 947 | 948 | async_accept(); 949 | }); 950 | } 951 | ``` 952 | 953 | Теперь давайте запустим наш сервер: 954 | ``` 955 | ./server 956 | ``` 957 | 958 | Также запустим клиент `telnet`: 959 | ``` 960 | telnet localhost 15001 961 | 962 | Welcome to chat 963 | ``` 964 | 965 | Запустив второй клиент `telnet`, на первом клиенте вы увидите: 966 | ``` 967 | telnet localhost 15001 968 | 969 | Welcome to chat 970 | We have a newcomer 971 | ``` 972 | 973 | Запустите еще один клиент, напишите что-нибудь в чат и нажмите `Enter`: 974 | ``` 975 | telnet localhost 15001 976 | 977 | Welcome to chat 978 | Hello guys 979 | ``` 980 | 981 | Остальные клиенты должны увидеть что-то вроде этого: 982 | ``` 983 | telnet localhost 15001 984 | 985 | Welcome to chat 986 | We have a newcomer 987 | We have a newcomer 988 | 127.0.0.1:47235: Hello guys 989 | ``` 990 | 991 | 992 | # Упрощаем код 993 | 994 | В предыдущем разделе мы рассмотрели очень простой чат-сервер, занимающий 995 | всего лишь 131 строку кода. Однако, если бы мы писали такой же сервер на 996 | языке программирования более высокого уровня (например, Python или Erlang), у 997 | нас бы получилось гораздо меньше кода. 998 | 999 | Вы могли бы заметить: «Но C++ — это не Python и не Erlang. Разве C++ не 1000 | является низкоуровневым языком программирования?». Ответ: и да, и нет. C++ 1001 | очень гибкий язык, который позволяет работать и на низком, и на высоком 1002 | уровне. Однако такая свобода возлагает на программиста большую 1003 | ответственность: нужно быть аккуратным, чтобы код не превратился в непонятную 1004 | кашу. 1005 | 1006 | Вы конечно можете работать с сырой памятью и сырыми указателями. Вы можете 1007 | париться по поводу порядка определенных байтов. Ваш код может генерировать 1008 | неустранимые ошибки, которые приведут к падению вашего приложения. И еще 1009 | тысяча особенностей, с которыми вы не столкнетесь, если бы вы будете 1010 | использовать Python. Однако в C++ вы можете спроектировать свой код таким 1011 | образом, чтобы каждый слой абстракции имел узкий набор обязанностей. Тем 1012 | самым, вы можете сделать свой код таким же высокоуровневым, как Python или 1013 | Erlang. 1014 | 1015 | Обобщая вышесказанное, C++ — это язык программирования, используя который вы 1016 | должны делить код на определенные слои абстракции, причем делать это нужно 1017 | очень осторожно. 1018 | 1019 | Boost.Asio — это библиотека, которая предоставляет вам низкоуровневую 1020 | функциональность. В настоящем приложении вам не следует напрямую использовать 1021 | Boost.Asio, равно как и использовать мьютексы или функцию `fopen`. 1022 | Boost.Beast — это библиотека, основанная на Boost.Asio, которая предоставит 1023 | вам всю необходимую функциональность, связанную с HTTP и Web-сокетами. 1024 | Однако даже Boost.Beast вам не следует использовать напрямую в вашем 1025 | приложении. По словами Vinnie Falco (автор Boost.Beast), библиотека 1026 | Boost.Beast — это не готовый для использования сервер или клиент. Это набор 1027 | инструментов, который вы должны использовать, чтобы создавать свои 1028 | собственные библиотеки. Причем ваше приложение должно основываться на этих 1029 | библиотеках, основанных на Boost.Beast, который, в свою очередь, основан на 1030 | Boost.Asio. 1031 | 1032 | Со временем ваше приложение начнет расти, поэтому вам необходимо 1033 | структурировать ваш код таким образом, чтобы каждый слой абстракции решал 1034 | только тот круг задач, на который он рассчитан. Если переложить вышесказанное 1035 | на наш сервер, то его реализация могла бы выглядеть следующим образом: 1036 | ```cpp 1037 | #include 1038 | 1039 | using message_type = std::string; 1040 | using session_type = chat::session; 1041 | using server_type = chat::server; 1042 | 1043 | class server { 1044 | public: 1045 | server(io::io_context& io_context, std::uint16_t port) : 1046 | srv(io_context, port) { 1047 | srv.on_join([&](session_type& client) { 1048 | client.post("Welcome to chat"); 1049 | srv.broadcast("We have a newcomer"); 1050 | }); 1051 | 1052 | srv.on_leave([&] { srv.broadcast("We are one less"); }); 1053 | 1054 | srv.on_message( 1055 | [&](message_type const& message) { srv.broadcast(message); }); 1056 | } 1057 | 1058 | void start() { 1059 | srv.start(); 1060 | } 1061 | 1062 | private: 1063 | server_type srv; 1064 | }; 1065 | 1066 | int main() { 1067 | io::io_context io_context; 1068 | server srv(io_context, 15001); 1069 | srv.start(); 1070 | io_context.run(); 1071 | return 0; 1072 | } 1073 | ``` -------------------------------------------------------------------------------- /code/simple_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::uint16_t port = 15001; 5 | 6 | boost::asio::io_context io_context; 7 | boost::asio::ip::udp::endpoint receiver(boost::asio::ip::udp::v4(), port); 8 | boost::asio::ip::udp::socket socket(io_context, receiver); 9 | 10 | while (true) { 11 | char buffer[65536]; 12 | boost::asio::ip::udp::endpoint sender; 13 | std::size_t bytes_transferred = 14 | socket.receive_from(boost::asio::buffer(buffer), sender); 15 | socket.send_to(boost::asio::buffer(buffer, bytes_transferred), sender); 16 | } 17 | 18 | return 0; 19 | } -------------------------------------------------------------------------------- /code/tcp_async_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class session: public std::enable_shared_from_this { 6 | public: 7 | session(boost::asio::ip::tcp::socket&& socket) : 8 | socket(std::move(socket)) {} 9 | 10 | void start() { 11 | boost::asio::async_read_until( 12 | socket, 13 | streambuf, 14 | '\n', 15 | [self = shared_from_this()]( 16 | boost::system::error_code error, 17 | std::size_t bytes_transferred) { 18 | std::cout << std::istream(&self->streambuf).rdbuf(); 19 | }); 20 | } 21 | 22 | private: 23 | boost::asio::ip::tcp::socket socket; 24 | boost::asio::streambuf streambuf; 25 | }; 26 | 27 | class server { 28 | public: 29 | server(boost::asio::io_context& io_context, std::uint16_t port) : 30 | io_context(io_context), 31 | acceptor( 32 | io_context, 33 | boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) {} 34 | 35 | void async_accept() { 36 | socket.emplace(io_context); 37 | 38 | acceptor.async_accept(*socket, [&](boost::system::error_code error) { 39 | std::make_shared(std::move(*socket))->start(); 40 | async_accept(); 41 | }); 42 | } 43 | 44 | private: 45 | boost::asio::io_context& io_context; 46 | boost::asio::ip::tcp::acceptor acceptor; 47 | std::optional socket; 48 | }; 49 | 50 | int main() { 51 | boost::asio::io_context io_context; 52 | server srv(io_context, 15001); 53 | srv.async_accept(); 54 | io_context.run(); 55 | return 0; 56 | } -------------------------------------------------------------------------------- /code/tcp_chat_server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace io = boost::asio; 7 | using tcp = io::ip::tcp; 8 | using error_code = boost::system::error_code; 9 | using namespace std::placeholders; 10 | 11 | using message_handler = std::function; 12 | using error_handler = std::function; 13 | 14 | class session: public std::enable_shared_from_this { 15 | public: 16 | session(tcp::socket&& socket) : socket(std::move(socket)) {} 17 | 18 | void start(message_handler&& on_message, error_handler&& on_error) { 19 | this->on_message = std::move(on_message); 20 | this->on_error = std::move(on_error); 21 | async_read(); 22 | } 23 | 24 | void post(const std::string& message) { 25 | bool idle = outgoing.empty(); 26 | outgoing.push(message); 27 | 28 | if (idle) { 29 | async_write(); 30 | } 31 | } 32 | 33 | private: 34 | void async_read() { 35 | io::async_read_until( 36 | socket, 37 | streambuf, 38 | "\n", 39 | std::bind(&session::on_read, shared_from_this(), _1, _2)); 40 | } 41 | 42 | void on_read(error_code error, std::size_t bytes_transferred) { 43 | if (!error) { 44 | std::stringstream message; 45 | message << socket.remote_endpoint(error) << ": " 46 | << std::istream(&streambuf).rdbuf(); 47 | streambuf.consume(bytes_transferred); 48 | on_message(message.str()); 49 | async_read(); 50 | } else { 51 | socket.close(error); 52 | on_error(); 53 | } 54 | } 55 | 56 | void async_write() { 57 | io::async_write( 58 | socket, 59 | io::buffer(outgoing.front()), 60 | std::bind(&session::on_write, shared_from_this(), _1, _2)); 61 | } 62 | 63 | void on_write(error_code error, std::size_t bytes_transferred) { 64 | if (!error) { 65 | outgoing.pop(); 66 | 67 | if (!outgoing.empty()) { 68 | async_write(); 69 | } 70 | } else { 71 | socket.close(error); 72 | on_error(); 73 | } 74 | } 75 | 76 | tcp::socket socket; 77 | io::streambuf streambuf; 78 | std::queue outgoing; 79 | message_handler on_message; 80 | error_handler on_error; 81 | }; 82 | 83 | class server { 84 | public: 85 | server(io::io_context& io_context, std::uint16_t port) : 86 | io_context(io_context), 87 | acceptor(io_context, tcp::endpoint(tcp::v4(), port)) {} 88 | 89 | void async_accept() { 90 | socket.emplace(io_context); 91 | 92 | acceptor.async_accept(*socket, [&](error_code error) { 93 | auto client = std::make_shared(std::move(*socket)); 94 | client->post("Welcome to chat\n\r"); 95 | post("We have a newcomer\n\r"); 96 | 97 | clients.insert(client); 98 | 99 | client->start( 100 | std::bind(&server::post, this, _1), 101 | [&, weak = std::weak_ptr(client)] { 102 | if (auto shared = weak.lock(); 103 | shared && clients.erase(shared)) { 104 | post("We are one less\n\r"); 105 | } 106 | }); 107 | 108 | async_accept(); 109 | }); 110 | } 111 | 112 | void post(const std::string& message) { 113 | for (auto& client : clients) { 114 | client->post(message); 115 | } 116 | } 117 | 118 | private: 119 | io::io_context& io_context; 120 | tcp::acceptor acceptor; 121 | std::optional socket; 122 | std::unordered_set> clients; 123 | }; 124 | 125 | int main() { 126 | io::io_context io_context; 127 | server srv(io_context, 15001); 128 | srv.async_accept(); 129 | io_context.run(); 130 | return 0; 131 | } 132 | -------------------------------------------------------------------------------- /norg/readme.norg: -------------------------------------------------------------------------------- 1 | * 📚 Boost.Asio: Асинхронность в C++ 2 | Руководство по созданию клиент-серверных систем с помощью C++, библиотек Boost.Asio и Boost.Beast. 3 | 4 |

5 | 6 | {./TUTORIAL.md}[ЧИТАТЬ ОНЛАЙН] 7 | 8 |

9 | 10 |
11 | 12 | Руководство находится в стадии перевода 13 | 14 |
15 | 16 | 17 | * 💡 О руководстве 18 | Это руководство является вольным переводом {https://dens.website/tutorials/cpp-asio}[серии туториалов]. Поскольку перевод вольный, в нем могут быть некоторые отступления от изначального текста. 19 | 20 | * 🎈 Помощь в переводе 21 | В переводе могут быть некоторые неточности, которые неправильно преподносят суть тех или иных вещей. Если вы нашли подобную неточность, откройте Issue или создайте Pull Request. Спасибо! 22 | -------------------------------------------------------------------------------- /norg/tutorial.norg: -------------------------------------------------------------------------------- 1 | * Содержание 2 | +toc+ 3 | 4 | * Введение 5 | Данное руководство посвящено работе асинхронного ввода-вывода, который в 6 | основном используется для сетевого взаимодействия. Для лучшего понимания 7 | происходящего, вы должны быть знакомы с современным C++, STL и Boost, а также 8 | с базовыми принципами сетевого взаимодействия и многопоточности. 9 | 10 | Мы будем использовать `Boost.Asio`, `Boost.Beast`, а также `C++20 Networking 11 | library`. Чтобы добиться асинхронности, мы будем использовать /обработчики 12 | завершения/, /сопрограммы/ (или корутины) и /фиберы/. 13 | 14 | Чтобы скомпилировать исходный код из примеров, вам понадобиться установить 15 | компилятор, поддерживающий стандарт `C++17`, а также библиотеку `Boost`. При 16 | компиляции вам потребуется добавить `Boost` в `include directories` и 17 | слинковать исходный код вашего приложения с ним. 18 | 19 | На самом деле, для большинства примеров достаточно скомпилировать 20 | `boost/libs/system/src/error_code.cpp`, поскольку остальная часть исходного 21 | кода библиотеки `Boost` — это header-only библиотеки. 22 | 23 | Обычно сетевое взаимодействие считается очень сложным предметом для изучения. 24 | Неужели это действительно так сложно? Что ж, ответ — и да, и нет. Потребуется 25 | время, чтобы стать экспертом в этой области, однако мы попробуем сделать так, 26 | чтобы вам было понятно то, что происходит в этом руководстве. 27 | 28 | Когда вы разрабатываете какое-либо приложение, вам следует использовать 29 | пространства имен (namespaces) и псевдонимы типов (type aliases), чтобы код 30 | было удобно читать. Мы начнем это делать позднее, после того, как у вас 31 | появится четкое понимание откуда берутся те или иные вещи. Поэтому первое 32 | время вы будете видеть что-то по типу `boost::asio::ip::tcp::socket`. Конечно 33 | же, в реальном коде это должно быть заменено на что-то вроде `tcp::socket`. 34 | 35 | Двумя важными элементами сетевого взаимодействия являются /клиенты/ и 36 | /серверы/. Обычно подобные руководства начинаются с изучения работы клиента, 37 | поскольку это более простая тема для рассмотрения. Однако в этом руководстве 38 | мы начнем с серверов. Почему? Во-первых, сервера — это то место, где C++ 39 | проявляет себя с наилучшей стороны, а во-вторых, сервера не так страшны, как 40 | кажутся на первый взгляд. 41 | 42 | На этом вступление окончено. Теперь вы готовы приступить к погружению в 43 | сетевое программирование на C++. 44 | 45 | * TCP и UDP 46 | 47 | Существует два основных протокола транспортного уровня, которые мы будем 48 | использовать — TCP и UDP. Протокол — это набор соглашений о том, как должны 49 | передаваться данные по сети. 50 | 51 | ** Transmission Control Protocol — TCP 52 | 53 | TCP-соединение очень похоже на файл: мы открываем его, считываем из него 54 | какие-то данные, записываем какие-то данные и закрываем его. Однако 55 | существуют некоторые ограничения: 56 | - При работе с файлом мы можем узнать его размер. В случае TCP-соединения 57 | это невозможно. 58 | - Вы можете изменять положение указателя, когда работаете с файлом. Этот 59 | трюк также нельзя провернуть с TCP-соединением. 60 | 61 | Другими словами, файл предоставляет вам произвольный доступ, в то время как 62 | TCP-соединение представляет собой двунаправленный последовательный поток. 63 | 64 | ** User Datagram Protocol — UDP 65 | 66 | Информация, передаваемая по протоколу UDP, представляет собой непрерывный 67 | кусок данных. По сравнению с TCP, у UDP нет соединений. Невозможно получить 68 | только часть данных, отправленных приложением. Вы либо получите все данные, 69 | либо ничего. На данный момент вам нужно знать о UDP следующее: 70 | - В UDP отсутствуют соединения, поскольку это не поток данных. Из этого 71 | следует, что нет необходимости создавать или закрывать UDP-сокет. Все, что 72 | вам требуется — это отправлять или получать данные. 73 | - Буфер, используемый для получения UDP-пакета должен быть достаточно 74 | большим, чтобы вместить весь пакет целиком. В противном случае, вы ничего 75 | не получите. Из этого следует, что необходимо заранее знать верхнюю 76 | границу размера пакетов, которые вы собрались получать. 77 | - Порядок входящих пакетов, как правило, не соответствует порядку их 78 | отправки. Это означает, что необходимо самостоятельно контролировать 79 | порядок пакетов. 80 | - Нет никаких гарантий, что все отправленные пакеты будут доставлены. Это 81 | означает, что потеря UDP-пакетов — обычное дело. Следовательно, необходимо 82 | самостоятельно контролировать, что все отправленные UDP-пакеты доставлены. 83 | 84 | Как вы можете понять, UDP немного сложнее в использовании, чем TCP. Тем не 85 | менее, у UDP есть свои преимущества, которые мы обсудим позднее. 86 | 87 | Это все, что вам необходимо знать о протоколах на данный момент. Значит, мы 88 | можем двигаться дальше. 89 | 90 | 91 | * Самый простой сервер 92 | 93 | Согласно {https://ru.wikipedia.org/wiki/Сервер_(программное_обеспечение)}[Википедии], 94 | > Сервер — программный компонент вычислительной системы, выполняющий 95 | сервисные (обслуживающие) функции по запросу клиента, предоставляя ему доступ 96 | к определённым ресурсам или услугам. 97 | 98 | Это определение очень точно подмечает тот факт, что сервер — это всего лишь 99 | приложение, которое получает какие-то данные от других приложений и 100 | возвращает некоторые данные обратно. 101 | 102 | Мы начнем с самого простого сервера, который приходит на ум — эхо UDP-сервер. 103 | Он выполняет следующие действия: 104 | - Получает любые данные, которые были отправлены на UDP-порт 15001. 105 | - Отправляет полученные данные обратно отправителю «как есть». 106 | 107 | На самом деле вы можете выбрать практически любой порт для вашего сервера. 108 | Существует множество часто используемых портов для различных служб, которые 109 | вы можете найти здесь: 110 | {https://ru.wikipedia.org/wiki/Список_портов_TCP_и_UDP}[Список портов TCP и UDP]. 111 | Однако, как правило, только несколько из этих служб используется 112 | одновременно в недавно установленной ОС. 113 | 114 | Теперь давайте взглянем на следующий {./code/simple_server.cpp}[исходный код]: 115 | 116 | #tangle code/simple_server.cpp 117 | @code cpp 118 | #include 119 | 120 | int main() { 121 | std::uint16_t port = 15001; 122 | 123 | boost::asio::io_context io_context; 124 | boost::asio::ip::udp::endpoint receiver(boost::asio::ip::udp::v4(), port); 125 | boost::asio::ip::udp::socket socket(io_context, receiver); 126 | 127 | while (true) { 128 | char buffer[65536]; 129 | boost::asio::ip::udp::endpoint sender; 130 | std::size_t bytes_transferred = 131 | socket.receive_from(boost::asio::buffer(buffer), sender); 132 | socket.send_to(boost::asio::buffer(buffer, bytes_transferred), sender); 133 | } 134 | 135 | return 0; 136 | } 137 | @end 138 | 139 | Вам даже не обязательно отдельно скачивать `.cpp` файл сервера, поскольку 140 | вышеприведенный код — это полноценный эхо UDP-сервер. Мы не реализовали 141 | логирование и обработку ошибок, чтобы код выглядел максимально просто. Об 142 | обработке ошибок мы поговорим позднее. Давайте разберемся, что происходит в 143 | этом коде: 144 | - `boost::asio::io_context` — основной поставщик услуг ввода-вывода. В данный 145 | момент вы можете рассматривать его как исполнителя (executor) 146 | запланированных задач. Вы поймете его назначение сразу после того, как мы 147 | перейдем к асинхронному потоку управления, что произойдет очень скоро. 148 | - `boost::asio::ip::udp::endpoint` — это пара IP-адреса и порта. 149 | - `boost::asio::ip::udp::socket` — это сокет. Вы можете рассматривать его как 150 | дескриптор файла, предназначенный для сетевого взаимодействия. Обычно, 151 | когда вы открываете файл, вы получаете дескриптор файла. Когда вы 152 | взаимодействуете по сети, вы используете сокет. 153 | - Каждый сокет прикреплен к некоторому `io_context`, а потому каждый сокет 154 | конструируется с помощью ссылки на `io_context`. Второй параметр 155 | конструктора сокета — `endpoint` — IP-адрес и порт, который используется 156 | для получения входящих дейтаграмм (в случае UDP) или соединений (в случае 157 | TCP). 158 | - `boost::asio::ip::udp::v4()` возвращает объект, который в данный момент вы 159 | должны рассматривать как просто сетевой интерфейс UDP по умолчанию. 160 | - `boost::asio::buffer()` — это представление буфера, которое содержит 161 | указатель и размер, причем это представление не владеет памятью. В нашем 162 | случае оно указывает на массив `char`. 163 | - `socket::receive_from` ожидает входящий UDP-пакет, заполняет `buffer` 164 | полученными данными, а также заполняет `sender` информацией об отправителе, 165 | которая также включает в себя пару IP-адреса и порта. 166 | - `socket::send_to` отправляет UDP-пакет, используя данные из представления 167 | буфера. Получатель пакета передается вторым аргументом. В нашем случае 168 | получателем является отправитель, поскольку речь идет об эхо-сервере. 169 | 170 | Итак, мы сделали следующее: 171 | - Создали UDP-сокет и настроили его на ожидание UDP-пакетов на порту 15001. 172 | - Запустили бесконечный цикл, в котором ожидаем входящие UDP-пакеты, а после 173 | получения отправляем их обратно отправителю. 174 | 175 | Поздравляем! Вы только что создали ваш первый сервер с помощью C++ и 176 | Boost.Asio! 177 | 178 | * Прощаемся с синхронностью 179 | 180 | В реальной жизни синхронный ввод-вывод практически бесполезен. Даже если вы 181 | пишите простой клиент с единственным сетевым подключением, то скорее всего 182 | ваше приложение будет выполнять такие функции как управление пользовательским 183 | интерфейсом, чтение пользовательского ввода и т. п. Однако использование 184 | синхронного ввода-вывода означает, что все его операции являются 185 | блокирующими. Следовательно ваше приложение не сможет выполнять каких либо 186 | операций до тех пор, пока не завершатся операции с вводом-выводом. 187 | 188 | Вы можете обойти это ограничение с помощью создания дополнительных потоков. 189 | Например, один поток может обрабатывать ввод-вывод, а другой управлять 190 | пользовательским интерфейсом. Однако такой подход приведет к усложнению 191 | вашего приложения, поскольку в какой-то момент вам придется синхронизировать 192 | эти потоки. Более того, не существует безопасного способа отменить 193 | блокирующую операцию ввода-вывода из другого потока. Хотя это и может 194 | работать так, как вы ожидаете, но в целом это не безопасная операция. А 195 | потому вы можете столкнуться с неопределенным поведением, если что-нибудь 196 | измениться в вашем рабочем окружении (например, если вы скомпилируете код для 197 | новой платформы, с которой вы раньше не работали). 198 | 199 | Асинхронный подход лишен этих недостатков. Проще говоря, выполнение 200 | асинхронного кода можно представить так: «Начни делать это в фоновом режиме, 201 | а после того, как закончишь, вызови эту функцию. Тем временем я займусь 202 | другими задачами, которые необходимо выполнить». Таким образом, выполнение 203 | асинхронного кода — это неблокирующая операция, а значит вы можете совершать 204 | другие действия, пока ваши задачи выполняются в фоновом режиме. Кроме того, 205 | асинхронные задачи могут быть безопасно отменены в любое время. 206 | 207 | Вспомним {* самый простой сервер}[код из предыдущего раздела], в котором 208 | используется синхронный подход: 209 | @code cpp 210 | // Эта операция заблокирует поток управление до тех пор, пока не будет получено сообщение 211 | std::size_t bytes_transferred = socket.receive_from(buffer, sender); 212 | std::cout << "Message is received, message size is " << bytes_transferred; 213 | @end 214 | 215 | Асинхронные версии функций ввода-вывода в Boost.Asio начинаются с приставки 216 | `async_`. Теперь взгляните на тот же код, переписанный в асинхронном стиле: 217 | @code cpp 218 | // Эта операция не блокирующая: выполнение кода продолжится сразу после вызова функции 219 | socket.async_receive_from( 220 | buffer, 221 | sender, 222 | [&](boost::system::error_code error, std::size_t bytes_transferred) { 223 | // Эта лямбда-функция будет вызвана после получения сообщения 224 | std::cout << "Message is received, message size is " 225 | << bytes_transferred; 226 | }); 227 | @end 228 | 229 | В C++ нам нравится держать все под контролем. Первое, что вы должны спросить: 230 | «Эй, где именно выполняется это фоновая задача? Должны ли мы создавать поток 231 | для нее?». Вы получите ответ на этот вопрос в следующем разделе. А пока, 232 | пришло время сказать «прощай» синхронному коду и двигаться дальше. 233 | 234 | * Асинхронный TCP-сервер 235 | 236 | Пришло время взглянуть на наш первый асинхронный TCP-сервер. Это последний 237 | раз, когда мы не используем пространства имен (namespaces) и псевдонимы типов 238 | (type aliases). В дальнейшем вы уже должны понимать откуда берутся те или 239 | иные вещи. 240 | 241 | Теперь наш сервер будет делать следующее: 242 | - Слушать порт 15001 и ожидать входящее TCP-соединение. 243 | - Принимать входящее соединение. 244 | - Читать данные из соединения до тех пор, пока не встретится символ конца 245 | строки (т. е. символ `\n`). 246 | - Выводить полученные данные в стандартный вывод. 247 | - Закрывать соединение. 248 | 249 | Теперь давайте взглянем на полноценный пример такого сервера. Ниже мы все 250 | разложим по полочкам и посмотрим как все устроено. Как и прежде, мы 251 | пренебрегаем обработкой ошибок, чтобы код выглядел более понятным. Об обработке 252 | ошибок мы поговорим позже. 253 | 254 | #tangle code/tcp_async_server.cpp 255 | @code cpp 256 | #include 257 | #include 258 | #include 259 | 260 | class session: public std::enable_shared_from_this { 261 | public: 262 | session(boost::asio::ip::tcp::socket&& socket) : 263 | socket(std::move(socket)) {} 264 | 265 | void start() { 266 | boost::asio::async_read_until( 267 | socket, 268 | streambuf, 269 | '\n', 270 | [self = shared_from_this()]( 271 | boost::system::error_code error, 272 | std::size_t bytes_transferred) { 273 | std::cout << std::istream(&self->streambuf).rdbuf(); 274 | }); 275 | } 276 | 277 | private: 278 | boost::asio::ip::tcp::socket socket; 279 | boost::asio::streambuf streambuf; 280 | }; 281 | 282 | class server { 283 | public: 284 | server(boost::asio::io_context& io_context, std::uint16_t port) : 285 | io_context(io_context), 286 | acceptor( 287 | io_context, 288 | boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) {} 289 | 290 | void async_accept() { 291 | socket.emplace(io_context); 292 | 293 | acceptor.async_accept(*socket, [&](boost::system::error_code error) { 294 | std::make_shared(std::move(*socket))->start(); 295 | async_accept(); 296 | }); 297 | } 298 | 299 | private: 300 | boost::asio::io_context& io_context; 301 | boost::asio::ip::tcp::acceptor acceptor; 302 | std::optional socket; 303 | }; 304 | 305 | int main() { 306 | boost::asio::io_context io_context; 307 | server srv(io_context, 15001); 308 | srv.async_accept(); 309 | io_context.run(); 310 | return 0; 311 | } 312 | @end 313 | 314 | По сравнению с предыдущим сервером, этот код занимает значительно больше 315 | места. Но не стоит паниковать, здесь всего 57 строк кода, которые 316 | представляют из себя полноценный асинхронный TCP-сервер. 317 | 318 | В прошлый раз мы упомянули, что все функции с приставкой `async_` выполняются 319 | в фоновом режиме. Так где же находится этот «фоновый режим»? Что ж, фоновый 320 | режим находится /где-то/ внутри операционной системы. На самом деле, вам не 321 | нужно заботиться о том, как это происходит. Единственное, что должно вас 322 | волновать — откуда вызываются обработчики завершения. И это происходит внутри 323 | `io_context.run()`. Давайте взглянем на функцию `main`: 324 | @code cpp 325 | int main() { 326 | boost::asio::io_context io_context; 327 | server srv(io_context, 15001); 328 | srv.async_accept(); 329 | io_context.run(); 330 | return 0; 331 | } 332 | @end 333 | 334 | Функция `boost::asio::io_context::run` — это своего рода функция цикла 335 | событий (event loop), которая управляет всеми операциями ввода-вывода. При 336 | вызове функции `run` поток управления блокируется до тех пор, пока не 337 | выполнятся все асинхронные операции, связанные с `io_context`. Все операции с 338 | приставкой `async_` связаны с каким-либо `io_context`. В некоторых языках 339 | программирования (например, JavaScript) функция цикла событий спрятана от 340 | разработчика. Но в C++ нам нравится все держать под контролем, поэтому мы 341 | решаем, где именно функция цикла событий будет запущена. 342 | 343 | Теперь давайте рассмотрим класс `server`. Здесь встречается сразу несколько 344 | новых вещей, которые находятся в `private` секции класса: 345 | - `boost::asio::ip::tcp::socket` — этот тот же самый сокет, что и до этого, 346 | только теперь он работает в рамках протокола TCP (вместо UDP, как это было 347 | ранее). 348 | - `boost::asio::ip::tcp::acceptor` — это объект, который принимает входящие 349 | соединения. 350 | 351 | Если вы посмотрите на конструктор класса `acceptor`, вы увидите то, что он 352 | очень похож на метод `receive_from` у UDP-сокета: 353 | @code cpp 354 | acceptor(io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)) 355 | @end 356 | 357 | Передав такие аргументы конструктору, мы получим, что `acceptor` будет 358 | слушать входящие TCP-соединения на любом сетевом интерфейсе на указанном 359 | порту. 360 | 361 | Теперь давайте рассмотрим вызов функцию `async_accept` у `acceptor`: 362 | @code cpp 363 | acceptor.async_accept(*socket, [&](boost::system::error_code error) { 364 | std::make_shared(std::move(*socket))->start(); 365 | async_accept(); 366 | }); 367 | @end 368 | 369 | Словами это можно описать так: «Ожидай входящее соединение, а после того как 370 | установишь его, свяжи это соединение с сокетом и вызови обработчик 371 | завершения». Как вы помните, функции с приставкой `async_` не блокируют 372 | вызывающий поток. 373 | 374 | Итак, сервер ожидает входящее соединение После установления соединения сервер 375 | создает объект сессии. При создании мы перемещаем сокет, связанный с 376 | установленным соединением, внутрь объекта сессии. После этого сервер начинает 377 | ожидать следующее входящее соединение. 378 | 379 | Обратите внимание, что серверу все равно, что происходит с установленным 380 | соединением. Сервер сразу же начинает ожидать следующее входящее соединение 381 | не беспокоясь о том, что происходит с предыдущим соединением. Установленные 382 | соединения выполняются в фоновом режиме. Одновременно может существовать 383 | почти любое количество соединений (количество открытых файловых дескрипторов 384 | ограничено ОС), выполняемых в фоновом режиме. Это и есть принцип работы 385 | асинхронного ввода-вывода. 386 | 387 | Отлично, теперь мы знаем, как работает наш сервер. Давайте рассмотрим класс 388 | `session`. Сессия — это класс, который поддерживает соединение. Сессия 389 | содержит некоторые данные, связанные с соединением и предоставляет некоторый 390 | набор функций, связанный с соединением. Давайте рассмотрим функцию `start`: 391 | @code cpp 392 | void start() { 393 | boost::asio::async_read_until( 394 | socket, 395 | streambuf, 396 | '\n', 397 | [self = shared_from_this()]( 398 | boost::system::error_code error, 399 | std::size_t bytes_transferred) { 400 | std::cout << std::istream(&self->streambuf).rdbuf(); 401 | }); 402 | } 403 | @end 404 | 405 | Дословно, код выполняет следующее: «Читай данные из сокета в `streambuf`, а 406 | когда встретишь символ `"\n"`, остановись и вызови обработчик завершения». 407 | 408 | `boost::asio::streambuf` — это класс, унаследованный от `std::streambuf`. 409 | Можете рассматривать его как реализацию `streambuf` в библиотеке 410 | `Boost.Asio`. 411 | 412 | Итого, сессия считывает данные из сокета до тех пор, пока не встретит символ 413 | `"\n"`, а после записывает полученные данные в стандартный вывод. 414 | 415 | Обратите внимание, что класс `session` унаследован от класса 416 | `std::enable_shared_from_this`. Также заметим, что сессия захватывает в 417 | лямбду обработчика завершения указатель на разделяемую копию себя посредством 418 | `shared_from_this`. Мы делаем это, чтобы продлить время жизни сессии до тех 419 | пор, пока не будет вызван обработчик завершения. После этого нам не нужно 420 | ничего делать — указатель на разделяемый объект выйдет из области видимости и 421 | сразу же уничтожится после завершения работы обработчика. В большинстве 422 | случаев (но не во всех), это обычный способ для работы с сессиями. 423 | 424 | Теперь вы знаете как написать простой асинхронный TCP-сервер и как он 425 | работает. Последнее, что нам необходимо сделать — протестировать в реальной 426 | жизни. Запустим сервер в терминале: 427 | @code 428 | ./server 429 | @end 430 | 431 | Теперь в другом терминале запустим `telnet`, подключимся к 15001 порту, 432 | введем `Hello asio!` и нажмем `Enter` (который введет ожидаемый символ 433 | `"\n"`): 434 | @code 435 | telnet localhost 15001 436 | Hello asio! 437 | @end 438 | 439 | В первом терминале вы должны увидеть это сообщение: 440 | @code 441 | ./server 442 | Hello asio! 443 | @end 444 | 445 | Круто! Вы только начали, а уже знаете, как написать почти любой 446 | профессиональный асинхронный TCP-сервер с помощью современного C++ и 447 | Boost.Asio. Поздравляем! 448 | 449 | * Обработка ошибок 450 | 451 | ** Синхронные функции 452 | 453 | В Boost.Asio все синхронные функции ввода-вывода имеют две перегрузки для 454 | обработки ошибок: первая выбрасывает исключение, а вторая возвращает ошибку 455 | по ссылке (подход с возвращением значения). 456 | 457 | Исключения, которые выбрасывается из функций Boost.Asio, являются 458 | экземплярами класса `boost::system::system_error`, который, в свою очередь, 459 | унаследован от класса `std::runtime_error`. 460 | 461 | При возврате ошибки по ссылке, используется экземпляр класса 462 | `boost::system::error_code`. 463 | 464 | *** Пример. Исключения 465 | 466 | @code cpp 467 | try { 468 | socket.connect(endpoint); 469 | } catch (const boost::system::system_error& e) { 470 | std::cerr << e.what() << "\n"; 471 | } 472 | @end 473 | 474 | Вы можете получить `error_code` из `system_error`, вызвав метод `code()`: 475 | @code cpp 476 | catch (const boost::system::system_error& e) { 477 | boost::system::error_code error = e.code(); 478 | std::cerr << error.message() << "\n"; 479 | } 480 | @end 481 | 482 | *** Пример. Возвращаемое значение 483 | 484 | Если вы не хотите возиться с исключениями, вы можете использовать 485 | перегрузку, чтобы получить ошибку по ссылке: 486 | @code cpp 487 | boost::system::error_code error; 488 | socket.connect(endpoint, error); 489 | 490 | if (error) { 491 | std::cerr << error.message() << "\n"; 492 | } 493 | @end 494 | 495 | 496 | ** Асинхронные функции 497 | 498 | Асинхронные функции ввода-вывода не выбрасывают исключений. Вместо этого, 499 | они передают `boost::system::error_code` в обработчик завершения. Поэтому 500 | для того, чтобы проверить, была ли операция завершена успешно, вы должны 501 | написать что-то наподобие этого: 502 | @code cpp 503 | socket.async_connect(endpoint, [&](boost::system::error_code error) { 504 | if (!error) { 505 | // Асинхронная операция успешно завершена 506 | } else { 507 | // Что-то пошло не так 508 | std::cerr << error.message() << "\n"; 509 | } 510 | }); 511 | @end 512 | 513 | 514 | ** error_code 515 | 516 | Рассмотрим некоторый функционал `error_code`, который может вам пригодиться 517 | при работе с ним. 518 | - Если вы хотите получить удобочитаемое описание ошибки из 519 | `boost::system::error_code`, нужно вызвать метод `message()`, который 520 | вернет `std::string` с описанием ошибки. 521 | - Если вы не хотите выделять дополнительную память для `std::string`, вы 522 | можете использовать перегрузку `message(char const* buffer, std::size_t 523 | size)`. 524 | - Если вы хотите получить системный код (который имеет тип `int`) ошибки из 525 | `error_code`, вызовите метод `value()`. 526 | 527 | Если удаленное соединение было закрыто, то будет выброшена `end-of-file` 528 | ошибка. В некоторых случаях вы не хотите рассматривать `end-of-file` ошибку 529 | как ошибку приложения. Например, вы хотите получить некоторое сообщение от 530 | удаленного хоста. После передачи сообщения хост разрывает соединение, что 531 | является нормальным поведением. Для обработки такой ситуации вы могли бы 532 | написать: 533 | @code cpp 534 | socket.async_receive( 535 | buffer, 536 | [&](boost::system::error_code error, std::size_t bytes_transferred) { 537 | if (!error) { 538 | // Асинхронная операция выполнена успешно. 539 | // Соединение все еще установлено 540 | } else if (error == boost::asio::error::eof) { 541 | // Соединение было разорвано. 542 | // В буфере по-прежнему хранятся полученные данные, 543 | // численно равные `bytes_transferred` (в байтах) 544 | } else { 545 | // Что-то пошло не так 546 | std::cerr << error.message() << "\n"; 547 | } 548 | }); 549 | @end 550 | 551 | У вас может возникнуть вопрос: как передавать `boost::system::error_code` в 552 | функции? По ссылке или по значению? С одной стороны, если вы откроете 553 | документацию Boost.Asio, то увидите, что автор библиотеки передает 554 | `error_code` по ссылке. С другой стороны, `error_code` содержит в себе один 555 | `int`, один `bool` и один сырой указатель. 556 | 557 | Вспомним класс `std::string_view`: он содержит в себе один `std::size_t` и 558 | один сырой указатель. Поскольку его размер невелик, его стоит передавать по 559 | значению. В нашем случае `error_code` занимает столько же места, сколько и 560 | `std::string_view` (в большинстве случаев это так, зависит от платформы). 561 | Поэтому в нашем коде мы передаем `error_code` по значению. 562 | 563 | * Дальнейшее изучение 564 | 565 | В следующем разделе мы рассмотрим пример более масштабного сервера — TCP 566 | чат-сервера. Но перед этим вы должны кое-что узнать. 567 | 568 | У сокета есть функционал, о котором мы еще не говорили. Мы можем узнать у 569 | сокета о его конечных устройствах с обеих сторон соединения: 570 | @code cpp 571 | boost::asio::ip::tcp::endpoint endpoint; 572 | endpoint = socket.local_endpoint(); // IP:порт локальной стороны соединения 573 | endpoint = socket.remote_endpoint(); // IP:порт удаленной стороны соединения 574 | @end 575 | 576 | Обратите внимание, что эти функции могут выбросить исключение. Если вы не 577 | хотите возиться с исключениями, вы можете использовать перегрузку, чтобы 578 | получить ошибку по ссылке: 579 | @code cpp 580 | boost::system:error_code error; 581 | auto endpoint = socket.remote_endpoint(error); 582 | @end 583 | 584 | `endpoint` можно использовать с `iostreams`: 585 | @code cpp 586 | boost::system:error_code error; 587 | auto endpoint = socket.remote_endpoint(error); 588 | std::cout << "Remote endpoint: " << endpoint << "\n"; 589 | @end 590 | 591 | Если запустить этот код, вы скорее всего увидите что-то на подобии этого: 592 | @code sh 593 | Remote endpoint: 127.0.0.1:38529 594 | @end 595 | 596 | Иногда вам может понадобиться отменить асинхронную операцию, которая была 597 | запланирована ранее. Единственный надежный и переносимый способ сделать это — 598 | закрыть связанный с операцией сокет. 599 | @code cpp 600 | boost::asio::async_read(socket, buffer, completion_handler); 601 | // ... 602 | socket.close(); 603 | @end 604 | 605 | Обратите внимание, что `socket::close` может выбросить исключение. Как 606 | всегда, существует перегрузка для получения ошибки по ссылке: 607 | @code cpp 608 | boost::system::error_code error; 609 | socket.close(error); 610 | @end 611 | 612 | Также существует метод `socket::cancel`, который позволяет отменить 613 | выполняемую в данный момент асинхронную операцию без закрытия сокета. Однако 614 | это поведение специфично для конкретной платформы. Метод может работать так, 615 | как вы этого ожидание, а может быть и проигнорирован ОС. Такая особенность 616 | почти наверняка говорит о том, что система имеет плохой дизайн. Старайтесь 617 | избегать этой операции. 618 | 619 | Когда вы отправляете данные по сети, вы всегда знаете точно, сколько байт 620 | должно быть передано. Когда вы получите данные,вы также можете ожидать 621 | получения некоторого фиксированного количества байтов. Однако в некоторых 622 | случаях вам нужно считывать данные до тех пор, пока не выполнится какое-то 623 | условие. Например, до тех пор, пока не встретится определенная 624 | последовательность (например, символ `"\n"`). В этом случае использование 625 | `boost::asio::streambuf` может быть более удобным, чем использование буфера с 626 | фиксированным размером. При использовании этого контейнера вы должны 627 | установить верхнюю границу размера `streambuf`, передав максимальный размер в 628 | конструктор: 629 | @code cpp 630 | boost::asio::streambuf streambuf(65536); 631 | @end 632 | 633 | В противном случае, размер буфера может расти до тех пор, пока не закончится 634 | память. После того, как вы обработали часть полученных данных, необходимо 635 | стереть эти данные из `streambuf`, чтобы размер буфера увеличивался. Это 636 | можно сделать с помощью метода `consume()`: 637 | @code cpp 638 | boost::asio::async_read_until( 639 | socket, 640 | streambuf, 641 | "\n", 642 | [&](boost::system::error_code error, std::size_t bytes_transferred) { 643 | // Обработка полученных данных 644 | // ... 645 | streambuf.consume(bytes_transferred); 646 | }); 647 | @end 648 | 649 | Библиотека самостоятельно не ставит в очередь асинхронные операции. Чтобы 650 | запланировать новую задачу, вам необходимо дождаться завершения текущей 651 | задачи. Это значит, что вы должны управлять очередью задач самостоятельно. 652 | Конечно, это проблема, если мы говорим об одном и том же типе операций: 653 | несколько чтений или несколько записей. Однако операции `async_read` и 654 | `async_write` могут быть запланированы параллельно без каких-либо проблем. 655 | 656 | Временем жизни объекта сессии можно управлять разными способами. Это зависит 657 | от логики сервера. Иногда достаточно захватить указатель на разделяемый 658 | объект в обработчик завершения. Таким образом мы можем продлить время жизни 659 | сессии до тех пор, пока не завершится текущая асинхронная операция. 660 | 661 | Однако иногда серверу нужно знать обо всех активных клиентах. Например, чтобы 662 | иметь возможность получить доступ к каждому клиенту. Этого можно добиться, 663 | поместив указатели на клиентов в некоторый специальный контейнер. 664 | 665 | Иногда объект сессии должен оставаться в памяти тогда, когда отсутствуют 666 | запланированные задачи, т. е. нет обработчиков завершения, которые могли бы 667 | хранить указатель на разделяемый объект. Этого можно добиться, если хранить 668 | указатель на разделяемый объект сессии где-нибудь в другом месте (например, в 669 | контейнере сервера). Также этого можно достичь использованием сырых 670 | указателей. Работа с сырыми указателями может показаться странной — в конце 671 | концов, мы говорим о C++. Однако в некоторых особых случаях этот способ очень 672 | хорош для управления асинхронным взаимодействием. Мы обсудим метод с сырыми 673 | указателями позднее. 674 | 675 | Обычно сервер знает о классе сессии, с которым он работает. Однако классу 676 | сессии также может понадобиться передать некоторую информацию серверу. Это 677 | приводит нас к необходимости цикличной видимости. Для достижения этого мы 678 | могли бы использовать предварительное объявление (forward declaration) класса 679 | сервера. После чего мы бы передавали ссылку на сервер в конструктор сессии. 680 | Однако это не очень хороший дизайн. Более хороший способ решить эту проблему 681 | — использовать функции обработчиков событий: 682 | 683 | @code cpp 684 | using message_handler = std::function; 685 | 686 | // На стороне сервера 687 | void server::create_session() { 688 | auto client = std::make_shared([&](const std::string& message) { 689 | std::cout << "We got a message: " << message; 690 | }); 691 | } 692 | 693 | // На стороне клиента 694 | void session::session(message_handler&& handler) : 695 | on_message(std::move(handler)) {} 696 | 697 | void session::async_receive() { 698 | boost::asio::async_receive(socket, [...](...) { on_message(some_buffer); }); 699 | } 700 | @end 701 | 702 | Более хороший не означает, что лучший. Есть несколько способов передавать 703 | данные между сессиями и сервером. Какой из них является лучшим — зависит от 704 | деталей реализации вашего приложения. 705 | 706 | Отлично, теперь вы знаете все, что нужно знать, чтобы рассмотреть следующий 707 | пример — простой TCP чат-сервер. 708 | 709 | * TCP чат-сервер 710 | 711 | Вы уже знаете какие вещи откуда берутся, поэтому отныне мы будем использовать 712 | псевдонимы типов, чтобы сделать названия типов короче. 713 | 714 | В этом разделе мы рассмотрим очень простой чат-сервер. Этот сервер не будет 715 | поддерживать пользовательские ники, цвета и другие аспекты, специфичные для 716 | конкретного пользователя. Мы отказываемся от этого, чтобы сервер был более 717 | простым. 718 | 719 | В предыдущем разделе мы подробно обсудили новые вещи, которые будут 720 | использоваться в этом сервере. Поэтому в этом разделе мы рассмотрим сервер 721 | лишь вкратце. 722 | 723 | Полный исходный код, который мы будем разбирать в этом разделе, вы можете 724 | найти {./code/tcp_chat_server.cpp}[здесь]. После чтения этого раздела качайте 725 | его, скомпилируйте, посмотрите как он работает. Попытайтесь самостоятельно 726 | понять, как все работает, основываясь на том, что вы узнали за все это время. 727 | В конце концов, вам нужно научиться понимать код. 728 | 729 | Ну что ж, начнем. Первое, что вы увидите в исходном коде — это включение 730 | заголовков и использование псевдонимов типов: 731 | @code cpp 732 | #include 733 | 734 | #include 735 | #include 736 | #include 737 | 738 | namespace io = boost::asio; 739 | using tcp = io::ip::tcp; 740 | using error_code = boost::system::error_code; 741 | using namespace std::placeholders; 742 | 743 | using message_handler = std::function; 744 | using error_handler = std::function; 745 | @end 746 | 747 | Пока что все должно быть очевидно. Функция `main` выглядит точно также, как и 748 | в предыдущем примере (за исключением использования псевдонимов типов): 749 | @code cpp 750 | int main() { 751 | io::io_context io_context; 752 | server srv(io_context, 15001); 753 | srv.async_accept(); 754 | io_context.run(); 755 | return 0; 756 | } 757 | @end 758 | 759 | В этот раз класс сервера и класс сессии немного больше, но поскольку мы уже 760 | разобрали часть кода, рассмотрим только ключевые моменты. 761 | 762 | Давайте начнем с ключевых моментов сессии. Функция `start` теперь принимает 763 | обработчики событий: 764 | @code cpp 765 | void start(message_handler&& on_message, error_handler&& on_error) { 766 | this->on_message = std::move(on_message); 767 | this->on_error = std::move(on_error); 768 | async_read(); 769 | } 770 | @end 771 | 772 | Функция `post` добавляет в очередь сообщение, адресованное клиенту. Отправка 773 | сообщения начинается, если в данный момент не отправляется предыдущее 774 | сообщение: 775 | @code cpp 776 | void post(const std::string& message) { 777 | bool idle = outgoing.empty(); 778 | outgoing.push(message); 779 | 780 | if (idle) { 781 | async_write(); 782 | } 783 | } 784 | @end 785 | 786 | Асинхронные функции чтения и записи выделены в отдельные методы класса 787 | сессии. Функция `async_read` считывает данные с удаленного клиента в 788 | `streambuf`, а функция `async_write` отправляет первое в очереди сообщение 789 | удаленному клиенту: 790 | @code cpp 791 | void async_read() { 792 | io::async_read_until( 793 | socket, 794 | streambuf, 795 | "\n", 796 | std::bind(&session::on_read, shared_from_this(), _1, _2)); 797 | } 798 | 799 | void async_write() { 800 | io::async_write( 801 | socket, 802 | io::buffer(outgoing.front()), 803 | std::bind(&session::on_write, shared_from_this(), _1, _2)); 804 | } 805 | @end 806 | 807 | Обработчик чтения выполняет следующие действия: 808 | ~ форматирует сообщение, полученное от клиента; 809 | ~ передает отформатированное сообщение в обработчик сообщений; 810 | ~ начинает ожидать следующего сообщения. 811 | 812 | Кроме того, он также выполняет обработку ошибок: 813 | @code cpp 814 | void on_read(error_code error, std::size_t bytes_transferred) { 815 | if (!error) { 816 | std::stringstream message; 817 | message << socket.remote_endpoint(error) << ": " 818 | << std::istream(&streambuf).rdbuf(); 819 | streambuf.consume(bytes_transferred); 820 | on_message(message.str()); 821 | async_read(); 822 | } else { 823 | socket.close(error); 824 | on_error(); 825 | } 826 | } 827 | @end 828 | 829 | Обработчик записи работает так: 830 | ~ удаляет сообщение из очереди; 831 | ~ если в очереди еще остались сообщения, начинает отправку следующего 832 | сообщения. 833 | 834 | Он также выполняет обработку ошибок: 835 | @code cpp 836 | void on_write(error_code error, std::size_t bytes_transferred) { 837 | if (!error) { 838 | outgoing.pop(); 839 | 840 | if (!outgoing.empty()) { 841 | async_write(); 842 | } 843 | } else { 844 | socket.close(error); 845 | on_error(); 846 | } 847 | } 848 | @end 849 | 850 | У класса сессии следующие атрибуты: 851 | @code cpp 852 | tcp::socket socket; // Сокет клиента 853 | io::streambuf streambuf; // Буфер для входящих данных 854 | std::queue outgoing; // Очередь исходящих сообщений 855 | message_handler on_message; // Обработчик сообщений 856 | error_handler on_error; // Обработчик ошибок 857 | @end 858 | 859 | Теперь давайте рассмотрим класс сервера. Начнем с атрибутов: 860 | @code cpp 861 | io::io_context& io_context; 862 | tcp::acceptor acceptor; 863 | std::optional socket; 864 | std::unordered_set clients; // Список подключенных клиентов 865 | @end 866 | 867 | Функция `post` рассылает сообщение всем подключенным клиентам. Эта функция 868 | также используется в качестве обработчика сообщений (см. далее): 869 | @code cpp 870 | void post(const std::string& message) { 871 | for (auto& client : clients) { 872 | client->post(message); 873 | } 874 | } 875 | @end 876 | 877 | Функция `async_accept` приветствует только что подключившегося клиента и 878 | сообщает всем остальным клиентам о новоприбывшем. Здесь также реализована 879 | обработка ошибок, которая в случае чего удаляет сессию из списка клиентов, 880 | после чего уведомляет об этом остальных клиентов: 881 | @code cpp 882 | void async_accept() { 883 | socket.emplace(io_context); 884 | 885 | acceptor.async_accept(*socket, [&](error_code error) { 886 | auto client = std::make_shared(std::move(*socket)); 887 | client->post("Welcome to chat\n\r"); 888 | post("We have a newcomer\n\r"); 889 | 890 | clients.insert(client); 891 | 892 | client->start( 893 | std::bind(&server::post, this, _1), 894 | [&, weak = std::weak_ptr(client)] { 895 | if (auto shared = weak.lock(); 896 | shared && clients.erase(shared)) { 897 | post("We are one less\n\r"); 898 | } 899 | }); 900 | 901 | async_accept(); 902 | }); 903 | } 904 | @end 905 | 906 | Теперь давайте запустим наш сервер: 907 | @code 908 | ./server 909 | @end 910 | 911 | Также запустим клиент `telnet`: 912 | @code 913 | telnet localhost 15001 914 | 915 | Welcome to chat 916 | @end 917 | 918 | Запустив второй клиент `telnet`, на первом клиенте вы увидите: 919 | @code 920 | telnet localhost 15001 921 | 922 | Welcome to chat 923 | We have a newcomer 924 | @end 925 | 926 | Запустите еще один клиент, напишите что-нибудь в чат и нажмите `Enter`: 927 | @code 928 | telnet localhost 15001 929 | 930 | Welcome to chat 931 | Hello guys 932 | @end 933 | 934 | Остальные клиенты должны увидеть что-то вроде этого: 935 | @code 936 | telnet localhost 15001 937 | 938 | Welcome to chat 939 | We have a newcomer 940 | We have a newcomer 941 | 127.0.0.1:47235: Hello guys 942 | @end 943 | 944 | * Упрощаем код 945 | В предыдущем разделе мы рассмотрели очень простой чат-сервер, занимающий 946 | всего лишь 131 строку кода. Однако, если бы мы писали такой же сервер на 947 | языке программирования более высокого уровня (например, Python или Erlang), у 948 | нас бы получилось гораздо меньше кода. 949 | 950 | Вы могли бы заметить: «Но C++ — это не Python и не Erlang. Разве C++ не 951 | является низкоуровневым языком программирования?». Ответ: и да, и нет. C++ 952 | очень гибкий язык, который позволяет работать и на низком, и на высоком 953 | уровне. Однако такая свобода возлагает на программиста большую 954 | ответственность: нужно быть аккуратным, чтобы код не превратился в непонятную 955 | кашу. 956 | 957 | Вы конечно можете работать с сырой памятью и сырыми указателями. Вы можете 958 | париться по поводу порядка определенных байтов. Ваш код может генерировать 959 | неустранимые ошибки, которые приведут к падению вашего приложения. И еще 960 | тысяча особенностей, с которыми вы не столкнетесь, если бы вы будете 961 | использовать Python. Однако в C++ вы можете спроектировать свой код таким 962 | образом, чтобы каждый слой абстракции имел узкий набор обязанностей. Тем 963 | самым, вы можете сделать свой код таким же высокоуровневым, как Python или 964 | Erlang. 965 | 966 | Обобщая вышесказанное, C++ — это язык программирования, используя который вы 967 | должны делить код на определенные слои абстракции, причем делать это нужно 968 | очень осторожно. 969 | 970 | Boost.Asio — это библиотека, которая предоставляет вам низкоуровневую 971 | функциональность. В настоящем приложении вам не следует напрямую использовать 972 | Boost.Asio, равно как и использовать мьютексы или функцию `fopen`. 973 | Boost.Beast — это библиотека, основанная на Boost.Asio, которая предоставит 974 | вам всю необходимую функциональность, связанную с HTTP и Web-сокетами. 975 | Однако даже Boost.Beast вам не следует использовать напрямую в вашем 976 | приложении. По словами Vinnie Falco (автор Boost.Beast), библиотека 977 | Boost.Beast — это не готовый для использования сервер или клиент. Это набор 978 | инструментов, который вы должны использовать, чтобы создавать свои 979 | собственные библиотеки. Причем ваше приложение должно основываться на этих 980 | библиотеках, основанных на Boost.Beast, который, в свою очередь, основан на 981 | Boost.Asio. 982 | 983 | Со временем ваше приложение начнет расти, поэтому вам необходимо 984 | структурировать ваш код таким образом, чтобы каждый слой абстракции решал 985 | только тот круг задач, на который он рассчитан. Если переложить вышесказанное 986 | на наш сервер, то его реализация могла бы выглядеть следующим образом: 987 | @code cpp 988 | #include 989 | 990 | using message_type = std::string; 991 | using session_type = chat::session; 992 | using server_type = chat::server; 993 | 994 | class server { 995 | public: 996 | server(io::io_context& io_context, std::uint16_t port) : 997 | srv(io_context, port) { 998 | srv.on_join([&](session_type& client) { 999 | client.post("Welcome to chat"); 1000 | srv.broadcast("We have a newcomer"); 1001 | }); 1002 | 1003 | srv.on_leave([&] { srv.broadcast("We are one less"); }); 1004 | 1005 | srv.on_message( 1006 | [&](message_type const& message) { srv.broadcast(message); }); 1007 | } 1008 | 1009 | void start() { 1010 | srv.start(); 1011 | } 1012 | 1013 | private: 1014 | server_type srv; 1015 | }; 1016 | 1017 | int main() { 1018 | io::io_context io_context; 1019 | server srv(io_context, 15001); 1020 | srv.start(); 1021 | io_context.run(); 1022 | return 0; 1023 | } 1024 | @end 1025 | --------------------------------------------------------------------------------