[python] How do you UDP multicast in Python?

Just another answer to explain some subtle points in the code of the other answers:

  • socket.INADDR_ANY - (Edited) In the context of IP_ADD_MEMBERSHIP, this doesn't really bind the socket to all interfaces but just choose the default interface where multicast is up (according to routing table)
  • Joining a multicast group isn't the same as binding a socket to a local interface address

see What does it mean to bind a multicast (UDP) socket? for more on how multicast works

Multicast receiver:

import socket
import struct
import argparse


def run(groups, port, iface=None, bind_group=None):
    # generally speaking you want to bind to one of the groups you joined in
    # this script,
    # but it is also possible to bind to group which is added by some other
    # programs (like another python program instance of this)

    # assert bind_group in groups + [None], \
    #     'bind group not in groups to join'
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)

    # allow reuse of socket (to allow another instance of python running this
    # script binding to the same ip/port)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    sock.bind(('' if bind_group is None else bind_group, port))
    for group in groups:
        mreq = struct.pack(
            '4sl' if iface is None else '4s4s',
            socket.inet_aton(group),
            socket.INADDR_ANY if iface is None else socket.inet_aton(iface))

        sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

    while True:
        print(sock.recv(10240))


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--port', type=int, default=19900)
    parser.add_argument('--join-mcast-groups', default=[], nargs='*',
                        help='multicast groups (ip addrs) to listen to join')
    parser.add_argument(
        '--iface', default=None,
        help='local interface to use for listening to multicast data; '
        'if unspecified, any interface would be chosen')
    parser.add_argument(
        '--bind-group', default=None,
        help='multicast groups (ip addrs) to bind to for the udp socket; '
        'should be one of the multicast groups joined globally '
        '(not necessarily joined in this python program) '
        'in the interface specified by --iface. '
        'If unspecified, bind to 0.0.0.0 '
        '(all addresses (all multicast addresses) of that interface)')
    args = parser.parse_args()
    run(args.join_mcast_groups, args.port, args.iface, args.bind_group)

sample usage: (run the below in two consoles and choose your own --iface (must be same as the interface that receives the multicast data))

python3 multicast_recv.py --iface='192.168.56.102' --join-mcast-groups '224.1.1.1' '224.1.1.2' '224.1.1.3' --bind-group '224.1.1.2'

python3 multicast_recv.py --iface='192.168.56.102' --join-mcast-groups '224.1.1.4'

Multicast sender:

import socket
import argparse


def run(group, port):
    MULTICAST_TTL = 20
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, MULTICAST_TTL)
    sock.sendto(b'from multicast_send.py: ' +
                f'group: {group}, port: {port}'.encode(), (group, port))


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--mcast-group', default='224.1.1.1')
    parser.add_argument('--port', default=19900)
    args = parser.parse_args()
    run(args.mcast_group, args.port)

sample usage: # assume the receiver binds to the below multicast group address and that some program requests to join that group. And to simplify the case, assume the receiver and the sender are under the same subnet

python3 multicast_send.py --mcast-group '224.1.1.2'

python3 multicast_send.py --mcast-group '224.1.1.4'