Хочу поделиться небольшим опытом работы с технологией групповой передачи данных, или иначе UDP Multicast, и проблемой особенностью, которая возникает при написании кроссплатформенного кода.
Сразу оговорюсь, что в этой статье не будет рассматривать сама технология и протокол UDP, для этого лучше обратиться к UDP, а потом к Multicast.
Итак, вся работа начинается с создания сокета и его «настройки». В общих чертах выглядит это так
1. создать сокет
2. сделать bind
3. подключится с Multicast группе.
Теперь по порядку
Тут все просто и без подвохов
Первое что мы должны сделать, это позволить использовать PORT повторно, т.к. помимо нас кто-то еще может работать с этим портом.
Функция setsockopt позволяет задать опции для сокета. Интересным моментом является то, что значение опции передается по указателю на void, т.к. некоторые опции требует не просто флаг включено/выключено, а структуры с дополнительными то данными.
Далее нам необходимо связать сокет с портом. А может еще и адресом? Тут вступает в силу перваяпроблема особенность — различная идеология ядер Windows и Linux. А именно то, что под Windows мы не можем забиндиться на адрес Multicast группы (получаем ошибку) и должны биндиться на INADDR_ANY
На Linux мы можем так же забиндиться на INADDR_ANY, но в этом случае мы будем получать все дейтаграммы пришедшие на забинденный нами порт. Для того чтобы получать дейтаграммы только из нужной группы, необходимо забиндиться именно на адрес этой группы (ну и порт конечно не забыть).
Главной особенностью Multicast является то, что наш хост не будет получать данных до тех пор пока мы не подключимся к Multicast группе. Подключение на уровне пользователя выглядит как установка опции для сокета.
где ip_mreq.imr_multiaddr это адрес мультикаст группы. А ip_mreq.imr_interface это адрес интерфейса, на котором мы ожидаем получать дейтаграммы. INADDR_ANY в данном случае говорит о том, что мы оставляем это право за ядром, которое выберет интерфейс исходя из таблицы роутинга. Далее ядро проверит не подключены ли мы уже к этой группе, и если нет, то отправит запрос на ближайший Multicast сервер. Тот в свою очередь дальше и т.д. После чего нам на хост будут отправлять дейтаграммы, отправляемые в данную Multicast группу.
Но что делать с Windows который забинден на INADDR_ANY? Он что будет получать все дейтаграммы отправленные на слушаемый порт? Судя по всему ядро Windows, при указании этой опции для сокета, само производит фильтрацию получаемых данных. И таким образом на сокет доставляются данные только из подписанных групп.
На Linux для того, чтобы получать данные на сокет только от подключенной Multicast группы, необходимо забиндиться на ее же адрес.
Если вы мне не верите (а я сам в это в начале не поверил) то поверьте ему
Разочарую вас, в boost все аналогично API, никакой адаптации под единую логику не производится. Таким образом, используя его для написание кроссплатформенного приложение вам все равно придется делать
Пока писал пост, понял, что путь, предложенный для Linux, скрывает одно ограничение. Мы должны работать по принципу один сокет — одна Multicast группа. Так как навряд ли возможно забиндиться сразу не несколько адресов.
Обрисую ситуацию. Есть сервер, он вещает данные в несколько мультикаст групп на один порт.
Есть клиент, он желает получать от сервера данные только в нескольких группах. Тогда ему нужно предпринять следующие действия:
1. биндиться на INADDR_ANY
2. далее фильтровать все полученные дейтаграммы в ручную определяя их destination1 адрес.
Процесс фильтрации полученных на сокете дейтаграмм по destination адресу вытекает из того, что возможна ситуация когда на машине клиента есть еще другой софт который подключается к каким то другим группам но с тем же портом и тогда оба сокета будут получать дейтаграммы со всех подключенных групп на этом порту.
Но при этом на лицо проблема избыточности данных.
Ну собственно об тих особенностях работы с Multicast я и хотел вам рассказать, не зная которых вы рискуете наступить на грабли.
PS. Для того что бы все это прочувствовать предлагаю проделать следующие шаги.
1. Собрать receiver
2. Собрать sender
Далее на Linux запустить
3. Запустить: receiver 0.0.0.0 239.192.100.1
4. Запустить: receiver 0.0.0.0 239.192.100.2
5. Запустить: sender 239.192.100.1
6. Убедить что оба receiver'a получат данные
Далее тоже запустить на Windows
7. Убедиться что данные получит только тот кому мы их отправили.
1 Еще одной особенностью UDP является то, что адрес отправителя и получателя явным образом не указывается в заголовке UDP сообщения. Но он учитывается при подсчете чексумы, таким образом получая дейтаграмму UDP модуль должен составить свой псевдо заголовок, рассчитать чексуму и сравнив ее с полученной, принять решении о том что дейтаграмма адресовалась именно нам. Судя по всему Linux делает эту проверку, используя в качестве destination адрес на который мы забиндились, из за чего и получается описанная вышепроблема особенность.
Сразу оговорюсь, что в этой статье не будет рассматривать сама технология и протокол UDP, для этого лучше обратиться к UDP, а потом к Multicast.
Итак, вся работа начинается с создания сокета и его «настройки». В общих чертах выглядит это так
1. создать сокет
2. сделать bind
3. подключится с Multicast группе.
Теперь по порядку
Создание сокета
Тут все просто и без подвохов
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
Связывание сокета
Первое что мы должны сделать, это позволить использовать PORT повторно, т.к. помимо нас кто-то еще может работать с этим портом.
const int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
Функция setsockopt позволяет задать опции для сокета. Интересным моментом является то, что значение опции передается по указателю на void, т.к. некоторые опции требует не просто флаг включено/выключено, а структуры с дополнительными то данными.
Далее нам необходимо связать сокет с портом. А может еще и адресом? Тут вступает в силу первая
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sockfd, (sockaddr *)&addr, sizeof(addr));
На Linux мы можем так же забиндиться на INADDR_ANY, но в этом случае мы будем получать все дейтаграммы пришедшие на забинденный нами порт. Для того чтобы получать дейтаграммы только из нужной группы, необходимо забиндиться именно на адрес этой группы (ну и порт конечно не забыть).
Подключение к группе
Главной особенностью Multicast является то, что наш хост не будет получать данных до тех пор пока мы не подключимся к Multicast группе. Подключение на уровне пользователя выглядит как установка опции для сокета.
struct ip_mreq mreq;
inet_aton(ip_addr, &(mreq.imr_multiaddr));
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
где ip_mreq.imr_multiaddr это адрес мультикаст группы. А ip_mreq.imr_interface это адрес интерфейса, на котором мы ожидаем получать дейтаграммы. INADDR_ANY в данном случае говорит о том, что мы оставляем это право за ядром, которое выберет интерфейс исходя из таблицы роутинга. Далее ядро проверит не подключены ли мы уже к этой группе, и если нет, то отправит запрос на ближайший Multicast сервер. Тот в свою очередь дальше и т.д. После чего нам на хост будут отправлять дейтаграммы, отправляемые в данную Multicast группу.
Но что делать с Windows который забинден на INADDR_ANY? Он что будет получать все дейтаграммы отправленные на слушаемый порт? Судя по всему ядро Windows, при указании этой опции для сокета, само производит фильтрацию получаемых данных. И таким образом на сокет доставляются данные только из подписанных групп.
На Linux для того, чтобы получать данные на сокет только от подключенной Multicast группы, необходимо забиндиться на ее же адрес.
Если вы мне не верите (а я сам в это в начале не поверил) то поверьте ему
Unix Network Programming 3 издание. глава 21.6 страница 599
— Чтобы получить дейтаграмму многоадресной передачи, процесс должен присоединиться к группе, а также связать при помощи функции bind сокет UDP с номером порта, который будет использоваться как номер порта получателя для дейтаграмм, отсылаемых данной группе.
…
Связывая порт, приложение указывает UDP, что требуется получать отправляемые на этот порт дейтаграммы. Некоторые приложения в дополнения к связыванию порта также связывают при помощи функции bind адрес многоадресной передачи с сокетом. Это предотвращает доставку сокету любых других дейтаграмм, которые могли быть получены для этого порта.
А как в boost'e ?
Разочарую вас, в boost все аналогично API, никакой адаптации под единую логику не производится. Таким образом, используя его для написание кроссплатформенного приложение вам все равно придется делать
#ifdef WIN32
boost::asio::ip::udp::endpoint listen_endpoint("0.0.0.0", multicast_port);
#else
boost::asio::ip::udp::endpoint listen_endpoint(multicast_address, multicast_port);
#endif
socket_.open(listen_endpoint.protocol());
socket_.set_option(boost::asio::ip::udp::socket::reuse_address(true));
socket_.bind(listen_endpoint);
ocket_.set_option(boost::asio::ip::multicast::join_group(multicast_address));
Несколько групп для одного сокета
Пока писал пост, понял, что путь, предложенный для Linux, скрывает одно ограничение. Мы должны работать по принципу один сокет — одна Multicast группа. Так как навряд ли возможно забиндиться сразу не несколько адресов.
Обрисую ситуацию. Есть сервер, он вещает данные в несколько мультикаст групп на один порт.
Есть клиент, он желает получать от сервера данные только в нескольких группах. Тогда ему нужно предпринять следующие действия:
1. биндиться на INADDR_ANY
2. далее фильтровать все полученные дейтаграммы в ручную определяя их destination1 адрес.
Процесс фильтрации полученных на сокете дейтаграмм по destination адресу вытекает из того, что возможна ситуация когда на машине клиента есть еще другой софт который подключается к каким то другим группам но с тем же портом и тогда оба сокета будут получать дейтаграммы со всех подключенных групп на этом порту.
Но при этом на лицо проблема избыточности данных.
Ну собственно об тих особенностях работы с Multicast я и хотел вам рассказать, не зная которых вы рискуете наступить на грабли.
PS. Для того что бы все это прочувствовать предлагаю проделать следующие шаги.
1. Собрать receiver
2. Собрать sender
Далее на Linux запустить
3. Запустить: receiver 0.0.0.0 239.192.100.1
4. Запустить: receiver 0.0.0.0 239.192.100.2
5. Запустить: sender 239.192.100.1
6. Убедить что оба receiver'a получат данные
Далее тоже запустить на Windows
7. Убедиться что данные получит только тот кому мы их отправили.
1 Еще одной особенностью UDP является то, что адрес отправителя и получателя явным образом не указывается в заголовке UDP сообщения. Но он учитывается при подсчете чексумы, таким образом получая дейтаграмму UDP модуль должен составить свой псевдо заголовок, рассчитать чексуму и сравнив ее с полученной, принять решении о том что дейтаграмма адресовалась именно нам. Судя по всему Linux делает эту проверку, используя в качестве destination адрес на который мы забиндились, из за чего и получается описанная выше