вторник, 26 июля 2016 г.

[prog.c++14] Упрощение себе жизни за счет возможностей современного C++. На примере работы с сокетами

Уже неоднократно в блоге поднимал тему того, как использование возможностей 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 ) )
{
   ifauto tv = try_header_type< timestamp_option >( cm ) )
   {
      // tv -- is a pointer to timeval.
      ... // bla-bla-bla
   }
   else ifauto 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 выглядит вот так:

templatetypename 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_casttypename 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);
};

templatetypename 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 ) {}

   templatetypename P >
   int level( const P & ) const { return traits::level; }

   templatetypename P >
   int name( const P & ) const { return traits::name; }

   templatetypename P >
   std::size_t size( const P & ) const { return traits::size; }

   templatetypename P >
   auto data( const P & ) { return &m_value; }

   templatetypename P >
   auto data( const P & ) const { return &m_value; }
};

Главное отличие от Asio-шного варианта в том, что здесь вводится понятие option_traits, которым параметризуются классы boolean_option и integer_option (этот класс не показан для экономии места). За счет этого в compile-time становятся доступны все главные свойства конкретной опции для сокетов: типы, значения level и optname, размеры данных и пр.

В общем, использую эту кухню уже несколько недель. Пока нравится. Забот с контролем низкоуровневых деталей никаких. А все эти шаблонные классы-функции, не смотря на свою кажущуюся сложность, на самом деле тривиальны и пишутся буквально за 15-20 минут. Тем более, что их идея была подсказана исходниками Asio.

Комментариев нет: