Уже неоднократно в блоге поднимал тему того, как использование возможностей C++ упрощает жизнь разработчику по сравнению с использованием теплой и ламповой "сишечки". Выдалось время показать еще один пример из недавней практики.
Временами при работе с сокетами нужно устанавливать те или иные опции. Если нужно просто дергать setsockopt, то в этом нет ничего сложного. Хотя, если нужно дернуть setsockopt много раз подряд для установки разных опций, то тиражирование простого кода методом копипасты наверняка приведет к тому, что где-то будут перепутаны значения level и optname.
Еще же веселее ситуация становится, когда кроме setsockopt приходится работать с msghdr, sendmsg и recvmsg. Тут мы быстро приходим к простому коду вида:
for (cm = CMSG_FIRSTHDR (&msg); cm; cm = CMSG_NXTHDR (&msg, cm)) { void *ptr = CMSG_DATA (cm); if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMP) { struct timeval *tv = (struct timeval *) ptr; ... // bla-bla-bla } } else if (cm->cmsg_level == SOL_IP && cm->cmsg_type == IP_RECVERR) { struct sock_extended_err * ee = (struct sock_extended_err *) ptr; ... // bla-bla-bla } } else if (...) ... } |
Код-то простой. Но, как мне представляется, писать и сопровождать его готовы люди, которые не боятся сложностей. Я, например, боюсь :)
Тут ведь есть не только грабли с постоянными проверками cmsg_type и cmsg_level. Гораздо веселее -- это расчет размера буфера, который нужен для приема cmsghdr структур. Тут совсем несложно заблудиться в трех соснах. Например, взять опцию SOL_SOCKET/SO_TIMESTAMP. При вызове setsockopt для ее установки нужно задействовать int, который будет работать как bool. А при расчете размера cmsghdr нужно отводить место под timeval. При этом не забывая еще и про CMSG_LEN.
В общем, колупаться с такими подробностями в стиле plain old C -- это то еще удовольствие, как по мне. Поэтому...
...попробовал расширить идею с SocketOption из Asio. С тем, чтобы через шаблоны можно было задавать все эти детали. Всего один раз. А затем их переиспользовать. В результате работа с cmsghdr выглядит вот так:
for( cmsghdr * cm = CMSG_FIRSTHDR( &msg ); cm; cm = CMSG_NXTHDR( &msg, cm ) ) { if( auto tv = try_header_type< timestamp_option >( cm ) ) { // tv -- is a pointer to timeval. ... // bla-bla-bla } else if( auto err = try_header_type< recverr_option >( cm ) ) { // err -- is a pointer to sock_extended_err. ... // bla-bla-bla } } |
А вот как выглядит вычисление размера буфера под набор cmsghdr, а так же установка опций через setsockopt (используя Asio):
union { char data_[ recverr_option::traits::cmsg_len // recverr + timestamp_option::traits::cmsg_len // timestamp + ... // bla-bla-bla. ]; struct cmsghdr align_; } control_data; m_socket.set_option( recverr_option(true) ); m_socket.set_option( timestamp_option(true) ); ... // bla-bla-bla. |
Ну а вот так выглядят объявления этих самых recverr_option и timestamp_option:
using recverr_option = asio_tools::boolean_option< asio_tools::option_traits< int, SOL_IP, IP_RECVERR, sock_extended_err, sizeof(sock_extended_err) + sizeof(sockaddr_in) > >; using timestamp_option = asio_tools::boolean_option< asio_tools::option_traits< int, SOL_SOCKET, SO_TIMESTAMP, timeval > >; |
Тут через asio_tools::option_traits задаются самые необходимые вещи: тип, который нужно использовать для setsockopt, значения level и optname, тип, который будет ожидаться в cmsghdr. Плюс, как в случае с sock_extended_err, размер данных в cmsghdr (не всегда этот размер определяется размером типа значения в cmsghdr).
Чистой воды декларации, которые затем везде используются автоматически. Что при вызове socket.set_option, что при попытке распознать тип cmsghdr. Кстати говоря, функция try_header_type выглядит вот так:
template< typename T > typename T::traits::cmsg_payload_type * try_header_type( cmsghdr * cm ) { if( cm->cmsg_level == T::traits::level && cm->cmsg_type == T::traits::name ) return reinterpret_cast< typename T::traits::cmsg_payload_type * >( CMSG_DATA(cm)); else return nullptr; } |
Ну а сама кухня с SocketOptions выглядит как-то так (на примере boolean_option, первоначальный вариант которого был тупо слизан с аналога из дебрей реализации Asio-шного boolean_option, но затем был доработан под мои нужды):
constexpr std::size_t ct_cmsg_len( std::size_t len ) { return CMSG_LEN(len); } template< typename VAL_TYPE, int LEVEL, int NAME, typename CMSG_PAYLOAD_TYPE = VAL_TYPE, std::size_t CMSG_PAYLOAD_LEN = sizeof(CMSG_PAYLOAD_TYPE) > struct option_traits { using value_type = VAL_TYPE; static constexpr int level = LEVEL; static constexpr int name = NAME; static constexpr std::size_t size = sizeof(value_type); using cmsg_payload_type = CMSG_PAYLOAD_TYPE; static constexpr std::size_t cmsg_len = ct_cmsg_len(CMSG_PAYLOAD_LEN); }; template< typename TRAITS > class boolean_option { public : using traits = TRAITS; private : typename traits::value_type m_value; public : boolean_option( bool v ) : m_value( v ? 1 : 0 ) {} template< typename P > int level( const P & ) const { return traits::level; } template< typename P > int name( const P & ) const { return traits::name; } template< typename P > std::size_t size( const P & ) const { return traits::size; } template< typename P > auto data( const P & ) { return &m_value; } template< typename P > auto data( const P & ) const { return &m_value; } }; |
Главное отличие от Asio-шного варианта в том, что здесь вводится понятие option_traits, которым параметризуются классы boolean_option и integer_option (этот класс не показан для экономии места). За счет этого в compile-time становятся доступны все главные свойства конкретной опции для сокетов: типы, значения level и optname, размеры данных и пр.
В общем, использую эту кухню уже несколько недель. Пока нравится. Забот с контролем низкоуровневых деталей никаких. А все эти шаблонные классы-функции, не смотря на свою кажущуюся сложность, на самом деле тривиальны и пишутся буквально за 15-20 минут. Тем более, что их идея была подсказана исходниками Asio.
Комментариев нет:
Отправить комментарий