Всем привет!
Продолжаю перевод книги John Torjo «Boost.Asio C++ Network Programming».
Содержание:
- Глава 1: Приступая к работе с Boost.Asio
- Глава 2: Основы Boost.Asio
- Глава 3: Echo Сервер/Клиент
- Глава 4: Клиент и Сервер
- Глава 5: Синхронное против асинхронного
- Глава 6: Boost.Asio – другие особенности
- Глава 7: Boost.Asio – дополнительные темы
В этой главе рассматриваются некоторые дополнительные темы Boost.Asio. Маловероятно, что вы будете использовать это каждый день, но, безусловно, будет не лишним это знать:
- Если отладка не удается, то вы увидите, что Boost.Asio поможет вам в этом
- Если вам придется работать с SSL, то посмотрите, что вам может предложить Boost.Asio
- Если вы пишите приложение под определенную OC, то посмотрите, какие дополнительные функции есть в Boost.Asio для вас
Asio против Boost.Asio
Авторы Boost.Asio так же обеспечили поддержку Asio. Вы можете думать об этом как об Asio, так как эта библиотека поставляется в двух вариантах, Asio (не Boost) и Boost.Asio. Авторы утверждают, что обновления будут сначала появляться в Asio, и периодически будут добавляться в дистрибутив Boost-а.
Если в двух словах, то различия в следующем:
- Asio определено в нэймспейсе
asio::
, в то время как Boost.Asio определено вboost::asio::
- Заголовочный файл в Asio это
asio.hpp
иboost/asio.hpp
в Boost.Asio - В Asio есть свой класс для запуска потоков (эквивалент
boost::thread
) - Asio предоставляет свои классы для кодов ошибок (
asio::error_code
вместоboost::system::error_cod
иasio::system_error
вместоboost::system::system_error
)
Больше информации по Asio вы можете получить по ссылке.
Вы должны решить для себя, какой вариант вы предпочтете; лично я предпочитаю Boost.Asio. Вот несколько вещей, на которые стоит обратить внимание, делая свой выбор:
- Новые версии Asio выходят чаще, чем новые версии Boost.Asio (новые дистрибутивы Boost выходят довольно редко)
- Asio чисто заголовочная (в то время как часть Boost.Asio зависит от других библиотек Boost, которые могут понадобиться для компиляции)
- Обе, и Asio и Boost.Asio вполне развитые, поэтому если вы не горите желанием использовать функции из нового релиза Asio, то Boost.Asio довольно хорошая альтернатива, тем более что в вашем распоряжении будут и другие библиотеки Boost.
Вы можете использовать Asio и Boost.Asio в одном приложении, хотя, я не рекомендую вам делать этого. Это может получиться не специально, в этом случае все будет в порядке, например, если вы используете Asio, а некоторые сторонние библиотеки используют Boost.Asio и наоборот.
Отладка
Отладка синхронного приложения, как правило, проще, чем асинхронного приложения. Для синхронного приложения, если оно заблокируется, вы просто войдете в отладку и получите картину того, где произошло (синхронное означает последовательное). Однако, когда вы программируете асинхронно, события не происходят последовательно, поэтому, если произойдет ошибка, то ее действительно трудно будет отловить.
Чтобы избежать этого, во-первых, вы должны очень хорошо разбираться в сопрограммах. Если программа будет реализована правильно, то у вас, практически, не будет проблем вообще.
Только в том случае, когда дело доходит до асинхронного программирования, Boost.Asio протянет вам руку помощи; Boost.Asio позволяет отслеживать обработчики, если определен макрос BOOST_ASIO_ENABLE_HANDLER_TRACKING
. Если это так, то Boost.Asio способствует выводу информации в стандартный поток вывода ошибок, записывая время, асинхронную операцию и, относящийся к ней, завершающий обработчик.
Информация отслеживания обработчиков
Информацию не так легко понять, но, тем не менее она очень полезна. На выходе Boost.Asio выдает следующее:
@asio|<timestamp>|<action>|<description>.
Первый тег всегда @asio
, вы можете использовать его, чтобы легко фильтровать сообщения, приходящие от Boost.Asio в случае, если другие источники пишут в стандартный поток ошибок (эквивалент std::cerr
). Экземпляр timestamp
считается в секундах и микросекундах, начиная с 1 января 1970 UTC. Экземпляр action
может быть чем-то из следующего:
>n
: используется, когда мы входим в обработчик с номером n. Экземплярdescription
содержит аргументы, передаваемые обработчику.-
<n
: используется, когда обработчик номер n закрывается. -
!n
: используется, когда мы вышли из обработчика n из-за исключения. -
~n
: используется, когда обработчик с номером n разрушается без вызова; наверное, потому что экземплярio_service
уничтожается слишком рано (до того, как n получит шанс вызваться). n*m
: используется, когда обработчик n создает новую асинхронную операцию с завершающим обработчиком под номером m. После старта запущенная асинхронная операция отобразится в экземпляреdescription
. Завершающий обработчик вызовется, когда вы увидите>m(start)
и<m(end)
.-
n
: используется, когда обработчик с номером n выполняет операцию, которая отображается вdescription
(которая может бытьclose
или операциейcancel
). Обычно, вы можете смело их игнорировать.
Всякий раз, когда n = 0, то снаружи выполняются все обработчики (асинхронно), обычно, вы видите, когда выполняется первая операция (операции) или в случае, если вы работаете с сигналами и срабатывает сигнал.
Вы должны обращать внимание на сообщения типа !n
и ~n
, которые возникают, когда есть ошибки в коде. В первом случае, асинхронная функция не выбросила исключение, таким образом, исключение должно быть сгенерировано вами, вы не должны допускать исключений при выходе из вашего завершающего обработчика. В последнем случае вы, вероятно, уничтожили экземпляр io_service
слишком рано, до завершения всех вызванных обработчиков.
Пример
Для того, чтобы показать вам пример вспомогательной информации, позвольте изменить пример из 6 главы. Все, что вам нужно сделать, это добавить дополнительный #define
перед включением boost/asio.hpp
:
#define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <boost/asio.hpp>
...
Так же мы выведем дамп в консоль, когда пользователь войдет в систему и получит первый список клиентов. Вывод будет следующий:
@asio|1355603116.602867|0*1|socket@008D4EF8.async_connect
@asio|1355603116.604867|>1|ec=system:0
@asio|1355603116.604867|1*2|socket@008D4EF8.async_send
@asio|1355603116.604867|<1|
@asio|1355603116.604867|>2|ec=system:0,bytes_transferred=11
@asio|1355603116.604867|2*3|socket@008D4EF8.async_receive
@asio|1355603116.604867|<2|
@asio|1355603116.605867|>3|ec=system:0,bytes_transferred=9
@asio|1355603116.605867|3*4|io_service@008D4BC8.post
@asio|1355603116.605867|<3|
@asio|1355603116.605867|>4|
John logged in
@asio|1355603116.606867|4*5|io_service@008D4BC8.post
@asio|1355603116.606867|<4|
@asio|1355603116.606867|>5|
@asio|1355603116.606867|5*6|socket@008D4EF8.async_send
@asio|1355603116.606867|<5|
@asio|1355603116.606867|>6|ec=system:0,bytes_transferred=12
@asio|1355603116.606867|6*7|socket@008D4EF8.async_receive
@asio|1355603116.606867|<6|
@asio|1355603116.606867|>7|ec=system:0,bytes_transferred=14
@asio|1355603116.606867|7*8|io_service@008D4BC8.post
@asio|1355603116.607867|<7|
@asio|1355603116.607867|>8|
John, new client list: John
Позвольте проанализировать каждую строчку:
- Мы вводим
async_connect
, которая создает обработчик 1(в нашем случае все обрабатываютtalk_to_svr::step
) - Вызывается обработчик 1 (после успешного подключения к серверу)
- Обработчик 1 вызывает
async_send
, которая создает обработчик 2 (здесь мы посылаем сообщение с логином на сервер) - Обработчик 1 закрывается
- Вызывается обработчик 2 и посылает 11 байт (
login John
) - Обработчик 2 вызывает
async_receive
, которая создает обработчик 3 (мы ждем, когда сервер ответит на наше сообщение с логином) - Обработчик 2 закрывается
- Вызывается обработчик 3 и получает 9 байт (
login ok
) - Обработчик 3 перенаправляет в
on_answer_from_server
(где создается обработчик 4) - Обработчик 3 закрывается
- Вызывается обработчик 4, который потом запишет в дамп
John logged in
- Обработчик 4 запускает еще один
step
(обработчик 5), который будет писатьask_clients
- Обработчик 4 закрывается
- Открывается обработчик 5
- Обработчик 5,
async_send
ask_clients
, создает обработчик 6 - Обработчик 5 закрывается
- Вводится обработчик 6 (мы успешно отправили
ask_clients
серверу) - Обработчик 6 вызывает
async_receive
, которая создает обработчик 7 (мы ждем, когда сервер отправит нам список существующих клиентов) - Обработчик 6 закрывается
- Вызывается обработчик 7, и мы принимаем список клиентов
- Обработчик 7 запускает
on_answer_from_serve
(где создается обработчик 8) - Обработчик 7 закрывается
- Открывается обработчик 8, и в дамп записывается список клиентов (
on_clients
)
Это займет некоторое время, чтобы привыкнуть, но, как только вы поймете это, вы сможете изолировать выходные данные, в которых содержится проблема и находить фактическую часть кода, которая должна быть исправлена.
Запись информации отслеживания обработчиков в файл
По умолчанию информация отслеживания обработчиков выводится в стандартный поток ошибок (эквивалент std::cerr
). Очень вероятно, что вы захотите перенаправить этот вывод в другое место. С одной стороны, по умолчанию, для консольных приложений, вывод и сброс ошибок происходит в одно место, то есть в консоль. Но для Windows (не консольных) приложений, поток ошибок по умолчанию является пустым.
Вы можете перенаправить вывод ошибок с помощью командной строки, например, так:
some_application 2>err.txt
Если вы не поленитесь, то можете сделать это программно, как показано в следующем фрагменте кода:
// for Windows
HANDLE h = CreateFile("err.txt", GENERIC_WRITE, 0, 0, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL , 0);
SetStdHandle(STD_ERROR_HANDLE, h);
// for Unix
int err_file = open("err.txt", O_WRONLY);
dup2(err_file, STDERR_FILENO);
SSL
Boost.Asio предоставляет классы для поддержки некоторых основных возможностей SSL. Внутри себя она использует OpenSSL
. Так что, если вы хотите использовать SSL, то сначала загрузите и соберите OpenSSL . Следует отметить, что, как правило, построение OpenSSL
задача не из легких, особенно, если у вас нет популярных компиляторов, таких как Visual Studio.
Если у вас есть успешно собранный OpenSSL
, то Boost.Asio имеет некоторые классы надстройки над ним:
-
ssl::stream
: используется вместо классаip::<protocol>::socket
-
ssl::context
: это контекст для начала сеанса -
ssl::rfc2818_verification
: этот класс является простым способом проверки сертификата по имени хоста в соответствии с правилами RFC 2818
Сначала нужно создать и инициализировать SSL контекст, затем открыть сокет, используя данный контекст и заданный узел, подключиться к удаленному хосту и произвести SSL «рукопожатие». После этого вы можете использовать независимые функции из Boost.Asio read*/write*
.
Вот простой пример HTTPS клиента, который подключается к Yahoo!:
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
using namespace boost::asio;
io_service service;
int main(int argc, char* argv[])
{
typedef ssl::stream<ip::tcp::socket> ssl_socket;
ssl::context ctx(ssl::context::sslv23);
ctx.set_default_verify_paths();
// Open an SSL socket to the given host
io_service service;
ssl_socket sock(service, ctx);
ip::tcp::resolver resolver(service);
std::string host = "www.yahoo.com";
ip::tcp::resolver::query query(host, "https");
connect(sock.lowest_layer(), resolver.resolve(query));
// The SSL handshake
sock.set_verify_mode(ssl::verify_none);
sock.set_verify_callback(ssl::rfc2818_verification(host));
sock.handshake(ssl_socket::client);
std::string req = "GET /index.html HTTP/1.0rnHost: "
+ host + "rnAccept: */*rnConnection: closernrn";
write(sock, buffer(req.c_str(), req.length()));
char buff[512];
boost::system::error_code ec;
while ( !ec)
{
int bytes = read(sock, buffer(buff), ec);
std::cout << std::string(buff, bytes);
}
}
Первые строки нуждаются в пояснении. При подключении к удаленному хосту, вы используете sock.lowest_layer()
, другими словами, вы используете основной сокет (ssl::stream
всего лишь врапер). Следующие три строки выполняют «рукопожатие». Как только это произойдет, вы сделаете HTTP запрос с помощью функции write()
из Boost.Asio и причитаете (функция read()
) входящие сообщения.
При реализации SSL серверов все становится немного сложнее. Boost.Asio поставляется с примером SSL- сервера, который вы можете найти в boost/libs/asio/example/ssl/server.cpp
.
Boost.Asio Windows компоненты
Особенности, которые стоит применять только в операционной системе Windows.
Дескрипторы потока
Boost.Asio позволяет создавать враперы над дескрипторами Windows, после чего вы можете использовать большинство независимых функций, таких как read(), read_until(), write(), async_read(), async_read_until()
, и async_write()
. Вот как можно прочитать строку из файла:
HANDLE file = ::CreateFile("readme.txt", GENERIC_READ, 0, 0,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);
windows::stream_handle h(service, file);
streambuf buf;
int bytes = read_until(h, buf, 'n');
std::istream in(&buf);
std::string line;
std::getline(in, line);
std::cout << line << std::endl;
Класс stream_handle
доступен только при завершении использования порта ввода/вывода (который используется по умолчанию). Если это так, то необходимо определить следующий макрос: BOOST_ASIO_HAS_WINDOWS_STREAM_HANDLE
.
Дескрипторы произвольного доступа
Boost.Asio позволяет вам делать произвольно-доступными операции чтения и записи в дескрипторы, которые относятся к обычными файлам. Опять же, вы создаете врапер над дескриптором, а затем используете независимые функции, такие как read_at(), write_at(), async_read_at()
, или async_write_at()
. Чтобы прочитать 50 символов, начиная с 1000-ого, вы можете использовать следующий код:
HANDLE file = ::CreateFile("readme.txt", GENERIC_READ, 0, 0,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);
windows::random_access_handle h(service, file);
char buf[50];
int bytes = read_at(h, 1000, buffer( buf));
std::string msg(buf, bytes);
std::cout << msg << std::endl;
Для Boost.Asio дескрипторы произвольного доступа обеспечивают только произвольный доступ, вы не можете использовать их как потоковые дескрипторы. Иными словами, независимые функции, такие как read(), read_until(), write()
, а так же их асинхронные коллеги не могут быть использованы для дескрипторов произвольного доступа. Класс random_access_handle
доступен только после завершения использования порта ввода/вывода (используемый по умолчанию). Если это так, то определен макрос BOOST_ASIO_HAS_WINDOWS_RANDOM_ACCESS_HANDLE.
Дескрипторы объекта
Вы можете ожидать дескрипторы Windows от объектов ядра, например, уведомления об изменении консольного ввода, событие, уведомление о ресурсах памяти, процессов, семафоров, потоков, таймеров. Или, проще говоря, вы можете вызвать WaitForSingleObject
. Для них вы можете создать врапер object_handle
и использовать wait()
или async_wait()
для них:
void on_wait_complete(boost::system::error_code err) {}
...
HANDLE evt = ::CreateEvent(0, true, true, 0);
windows::object_handle h(service, evt);
// synchronous wait
h.wait();
// asynchronous wait
h.async_wait(on_wait_complete);
Особенности Boost.Asio в POSIX системах
Особенности, которые стоит применять только в операционных системах Unix.
Локальные сокеты
Boost.Asio включает базовую поддержку локальных сокетов (так же известных как Unix сокеты).
Локальный сокет, это сокет, который можно получить только из приложения, которое работает на хост-машине. Вы можете использовать локальные сокеты для удобства межпроцессорной связи. Вы можете подключить как клиентский сокет, так и серверный. Для локальных сокетов endpoint это имя файла, например, /tmp/whatever
. Здорово уже то, что вы можете назначать права на данный файл, следовательно, вы можете запретить пользователям на вашей машине создавать сокеты на файл.
Вы можете подключить клиентский сокет следующим образом:
local::stream_protocol::endpoint ep("/tmp/my_cool_app");
local::stream_protocol::socket sock(service);
sock.connect(ep);
Так же вы можете создать серверный сокет как показано ниже:
::unlink("/tmp/my_cool_app");
local::stream_protocol::endpoint ep("/tmp/my_cool_app");
local::stream_protocol::acceptor acceptor(service, ep);
local::stream_protocol::socket sock(service);
acceptor.accept(sock);
Как только сокет успешно создан, вы можете использовать его как обычный сокет, он имеет те же функции, что и другие члены класса сокета, так же вы можете использовать независимые функции, использующие сокеты.
Обратите внимание, что локальные сокеты доступны для использования, только если их поддерживает целевая операционная система, а именно, если определен макрос BOOST_ASIO_HAS_LOCAL_SOCKETS
.
Подключение локальных сокетов
Наконец, вы можете соединить два сокета, либо без установления соединения (датаграммы), либо ориентированные на подключение (потоки):
// connection oriented
local::stream_protocol::socket s1(service);
local::stream_protocol::socket s2(service);
local::connect_pair(s1, s2);
// connection-less
local::datagram_protocol::socket s1(service);
local::datagram_protocol::socket s2(service);
local::connect_pair(s1, s2);
Внутри connect_pair
есть скверная POSIX функция socketpair(). Все, что она делает, это подключает два сокета без сложного процесса создания сокета; всего одна строчка кода и готово. Раньше это был самый простой способ межпотоковой коммуникации. В то время как в современном программировании вы можете этого избежать, вы можете найти ее полезной при работе со старым кодом, в котором используются сокеты.
Файловые дескрипторы POSIX
Boost.Asio позволяет использовать синхронные и асинхронные операции с использованием некоторых файловых дескрипторов POSIX, такие как пайпы, стандартные потоки ввода/вывода и другие устройства (но не для обычных файлов).
После создания экземпляра stream_descriptor
, например, файлового дескриптора POSIX, вы можете использовать некоторые независимые функции предоставляемые Boost.Asio, такие как read(), read_until(), write(), async_read(), async_read_until()
, и async_write()
.
Вот как вы будете читать одну строку из стандартного потока ввода и записывать ее в стандартный поток вывода:
size_t read_up_to_enter(error_code err, size_t bytes) { ... }
posix::stream_descriptor in(service, ::dup(STDIN_FILENO));
posix::stream_descriptor out(service, ::dup(STDOUT_FILENO));
char buff[512];
int bytes = read(in, buffer(buff), read_up_to_enter);
write(out, buffer(buff, bytes));
Класс stream_descriptor
доступен только если его поддерживает целевая операционная система, а именно, если определен макрос BOOST_ASIO_HAS_POSIX_STREAM_DESCRIPTOR
.
Fork
Boost.Asio поддерживает программы, которые используют вызов системной функции fork()
. Вы должны сообщить экземпляру io_service
, что собираетесь вызвать функцию fork()
и когда это произойдет. Смотрите следующий фрагмент кода:
service.notify_fork(io_service::fork_prepare);
if (fork() == 0)
{
// child
service.notify_fork(io_service::fork_child);
...
}
else
{
// parent
service.notify_fork(io_service::fork_parent);
...
}
Рекомендую использовать service
, который будет вызван в другом потоке. Хотя, Boost.Asio позволяет это, я настоятельно советую вам использовать потоки, которые используют boost::thread
.
Резюме
Стремитесь, чтобы ваш код был простым и легким для понимания. Изучайте и используйте сопрограммы. Это сведет к минимуму отладку того, что вам нужно сделать, но иногда происходят трудно-отлавливаемые ошибки, скрывающиеся в коде, Boost.Asio может помочь вам в этом, как было показано в разделе Отладка.
В случае, если вам придется иметь дело с SSL, Boost.Asio предоставляет основные классы для работы с ним.
Наконец, если ваше приложение ориентировано под определенную OC, то вы можете воспользоваться функциями из Boost.Asio, предоставляемые для конкретной операционной системы.
Сетевое программирование имеет весомое значение в настоящее время. Знание Boost.Asio является обязательным для любого C++ программиста 21 века. Мы разобрали теоретические и практические основы использования этой библиотеки, написали небольшую коллекцию примеров, которые вы можете легко понять, протестировать и расширить. Надеюсь, что вам было интересно читать. И, определенно, это было очень приятно переводить!
Ресурсы к этой статье: ссылка
В следующих нескольких статьях хочу предложить переводы первых глав нескольких книг (трех, возможно 4), какая больше придется вам по душе, ту и продолжу переводить первой.
Всем большое спасибо, до новых встреч!
Автор: Vasilui