📜 ⬆️ ⬇️

Features work with Multicast

I want to share some experience with multicast data transfer technology, or UDP Multicast, and the problem of a feature that arises when writing cross-platform code.


I’ll just make a reservation that in this article the technology and the UDP protocol will not be considered, for this it is better to turn to UDP and then to Multicast .

So, all the work begins with the creation of a socket and its "settings". In general, it looks like this
1. create a socket
2. make a bind
3. connect with multicast group.
')
Now in order

Socket creation


It's all simple and no dirty tricks
sockfd = socket(AF_INET, SOCK_DGRAM, 0); 


Socket binding


The first thing we need to do is allow PORT to reuse, because besides us, someone else can work with this port.
 const int optval = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); 

The setsockopt function allows you to set options for a socket. An interesting point is that the option value is passed by pointer to void, since Some options require not just the on / off flag, but structures with additional data.
Next, we need to bind the socket to the port. Or maybe an address? Here the first problem comes into force - a different ideology of the Windows and Linux kernels . Namely, that under Windows we cannot bind to the address of the Multicast group (we get an error) and must bind to INADDR_ANY
 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)); 

On Linux, we can also bind to INADDR_ANY, but in this case we will receive all the datagrams that came to the port that we zabindenny. In order to receive datagrams only from the necessary group, it is necessary to bind to the address of this group (well, of course, the port will not be forgotten).

Connect to group


The main feature of Multicast is that our host will not receive data until we connect to the Multicast group. User level connection looks like setting options for a socket.
 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)); 

where ip_mreq.imr_multiaddr is the multicast group address. And ip_mreq.imr_interface is the address of the interface at which we expect to receive datagrams. INADDR_ANY in this case means that we reserve this right for the kernel, which will select the interface based on the routing table. Next, the kernel will check if we are already connected to this group, and if not, then send a request to the nearest Multicast server. That, in turn, further, etc. After that, we will receive datagrams sent to this Multicast group to the host.

But what to do with Windows which is on INADDR_ANY? Will he receive all the datagrams sent to the listening port? Apparently, the Windows kernel, when specifying this option for a socket, itself filters the received data. And in this way, only the subscribed groups are delivered to the socket.

On Linux, in order to receive data on the socket only from the connected Multicast group, you need to bind to its address.

If you do not believe me (and I didn’t believe it at the beginning) then trust him
Unix Network Programming 3 edition. chapter 21.6 page 599
- To receive a multicast datagram, the process must join the group, and also bind the UDP socket with the port number to be used as the destination port number for the datagrams sent to the group.
...
When connecting a port, the application indicates to UDP that it is required to receive datagrams sent to this port. Some applications, in addition to port binding, also associate a multicast address with a socket using the bind function. This prevents any other datagrams that may have been received for this port from being delivered to the socket.


And how in boost'e?


I will disappoint you, in boost everything is similar to the API, no adaptation to the same logic is made. So, using it to write a cross-platform application, you still have to do
 #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)); 


Multiple groups for one socket


While writing the post, I realized that the path suggested for Linux hides one limitation. We have to work on the principle of one socket - one multicast group. Since it is hardly possible to bind at once not several addresses.

Describe the situation. There is a server, it broadcasts data in several multicast groups on one port.

There is a client, he wants to receive data from the server only in a few groups. Then he needs to take the following actions:

1. bend on INADDR_ANY
2. then filter all received datagrams manually defining their destination 1 address.

The process of filtering received at the destination address datagrams on the socket follows from the fact that there is a situation when there is another software on the client’s machine that connects to some other groups but with the same port and then both sockets will receive datagrams from all connected groups on this port .
But at the same time face the problem of data redundancy.

Well, actually I wanted to tell you about these features of working with Multicast, without knowing which you risk stepping on a rake.

Ps. In order to feel all this I propose to do the following steps.
1. Assemble the receiver
2. Collect sender
Next on Linux run
3. Start: receiver 0.0.0.0 239.192.100.1
4. Run: receiver 0.0.0.0 239.192.100.2
5. Run: sender 239.192.100.1
6. Ensure that both receivers will receive data
Next, also run on Windows
7. Make sure that the data will receive only the one to whom we sent them.



1 Another UDP feature is that the sender and recipient address is not explicitly indicated in the UDP message header. But it is taken into account when calculating a chex, thus, receiving a UDP datagram, the module must compile its pseudo header , calculate the cheksum and compare it with the received one, make a decision that the datagram was addressed to us. Apparently Linux does this check, using the address to which we bind as the destination, which is why the problem described above turns out to be a feature.

Source: https://habr.com/ru/post/141021/


All Articles