¿Cómo se realiza la multidifusión UDP en Python?

86

¿Cómo se envía y recibe multidifusión UDP en Python? ¿Existe una biblioteca estándar para hacerlo?

NoName
fuente

Respuestas:

98

Esto funciona para mi:

Recibir

import socket
import struct

MCAST_GRP = '224.1.1.1'
MCAST_PORT = 5007
IS_ALL_GROUPS = True

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if IS_ALL_GROUPS:
    # on this port, receives ALL multicast groups
    sock.bind(('', MCAST_PORT))
else:
    # on this port, listen ONLY to MCAST_GRP
    sock.bind((MCAST_GRP, MCAST_PORT))
mreq = struct.pack("4sl", socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)

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

while True:
  # For Python 3, change next line to "print(sock.recv(10240))"
  print sock.recv(10240)

Enviar

import socket

MCAST_GRP = '224.1.1.1'
MCAST_PORT = 5007
# regarding socket.IP_MULTICAST_TTL
# ---------------------------------
# for all packets sent, after two hops on the network the packet will not 
# be re-sent/broadcast (see https://www.tldp.org/HOWTO/Multicast-HOWTO-6.html)
MULTICAST_TTL = 2

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, MULTICAST_TTL)

# For Python 3, change next line to 'sock.sendto(b"robot", ...' to avoid the
# "bytes-like object is required" msg (https://stackoverflow.com/a/42612820)
sock.sendto("robot", (MCAST_GRP, MCAST_PORT))

Se basa en los ejemplos de http://wiki.python.org/moin/UdpCommunication que no funcionó.

Mi sistema es ... Linux 2.6.31-15-generic # 50-Ubuntu SMP Tue Nov 10 14:54:29 UTC 2009 i686 GNU / Linux Python 2.6.4

Gordon Wrigley
fuente
6
Para mac os x, debe utilizar la opción socket.SO_REUSEPORT como alternativa a socket.SO_REUSEADDR en el ejemplo anterior, para permitir múltiples oyentes en la misma combinación de direcciones de puerto de multidifusión.
atikat
Para enviar, también necesitaba "sock.bind ((<IP local>, 0))" porque mi oyente de multidifusión estaba vinculado a un adaptador específico.
Mark Foreman
2
para la multidifusión UDP que necesitan de obligar a grupo multicast / no puerto del puerto grupo local, sock.bind((MCAST_GRP, MCAST_PORT)), tu fuerza de código y no podría funcionar, puede que no funcione cuando hay varias NIC
stefanb
@atikat: ¡¡Gracias !! Aunque, ¿por qué necesitamos esto en MAC pero no en Ubuntu?
Kyuubi
2
@RandallCook: Cuando reemplazo '' por MCAST_GRP obtengo socket.error: [Errno 10049] La dirección solicitada no es válida en su contexto
stewbasic
17

Remitente de multidifusión que transmite a un grupo de multidifusión:

#!/usr/bin/env python

import socket
import struct

def main():
  MCAST_GRP = '224.1.1.1'
  MCAST_PORT = 5007
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
  sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32)
  sock.sendto('Hello World!', (MCAST_GRP, MCAST_PORT))

if __name__ == '__main__':
  main()

Receptor de multidifusión que lee de un grupo de multidifusión e imprime datos hexadecimales en la consola:

#!/usr/bin/env python

import socket
import binascii

def main():
  MCAST_GRP = '224.1.1.1' 
  MCAST_PORT = 5007
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
  try:
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  except AttributeError:
    pass
  sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 32) 
  sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)

  sock.bind((MCAST_GRP, MCAST_PORT))
  host = socket.gethostbyname(socket.gethostname())
  sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(host))
  sock.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, 
                   socket.inet_aton(MCAST_GRP) + socket.inet_aton(host))

  while 1:
    try:
      data, addr = sock.recvfrom(1024)
    except socket.error, e:
      print 'Expection'
      hexdata = binascii.hexlify(data)
      print 'Data = %s' % hexdata

if __name__ == '__main__':
  main()
Niranjan Tulpule
fuente
Intenté esto, no funcionó. En Wireshark puedo ver la transmisión, pero no veo ningún elemento de unión IGMP y no recibo nada.
Gordon Wrigley
1
debe enlazar al grupo / puerto de multidifusión, no al puerto local en la dirección de multidifusión,sock.bind((MCAST_GRP, MCAST_PORT))
stefanB
1
Este ejemplo no me funciona, por una extraña razón. El uso de socket.gethostbyname (socket.gethostname ()) para seleccionar la interfaz no siempre elige la interfaz externa; de hecho, en los sistemas Debian, tiende a seleccionar la dirección de loopback. Debian agrega una entrada 127.0.1.1 en la tabla de hosts para el nombre de host. En cambio, es más efectivo usar socket.INADDR_ANY, que la respuesta de mayor rango usa a través de la declaración 'paquete' (que es más correcta que el '+'). Además, no se requiere el uso de IP_MULTICAST_IF, como lo indica correctamente la respuesta de mayor rango.
Brian Bulkowski
1
@BrianBulkowski hay muchos programadores que usan socket.INADDR_ANY, para gran pesar y consternación de aquellos de nosotros con múltiples interfaces, que necesitamos que los datos de multidifusión lleguen a una interfaz en particular. La solución no es socket.INADDR_ANY. Es seleccionar la interfaz adecuada por dirección IP, como crea que es mejor (un archivo de configuración, preguntando al usuario final, como usted elija para las necesidades de su aplicación). socket.INADDR_ANY le proporcionará los datos de multidifusión, verdadero, y es más fácil si asume un host de un solo hogar, pero creo que es menos correcto.
Mike S
@MikeS, aunque estoy de acuerdo con usted en algún principio, la idea de usar direcciones IP para seleccionar interfaces es tremendamente, terriblemente tensa. Conozco bien el problema, pero en un mundo dinámico y la dirección IP no es la respuesta. Por lo tanto, debe escribir un código que repita todo y elija por nombre de interfaz, observe el nombre de interfaz, elija la dirección IP actual y la use. Con suerte, la dirección IP no ha cambiado mientras tanto. Desearía que Linux / Unix se hubiera estandarizado en el uso de nombres de interfaz en todas partes, y los lenguajes de programación lo hubieran hecho, eso haría que un archivo de configuración sea más sensible.
Brian Bulkowski
13

Mejor uso:

sock.bind((MCAST_GRP, MCAST_PORT))

en vez de:

sock.bind(('', MCAST_PORT))

porque, si desea escuchar varios grupos de multidifusión en el mismo puerto, obtendrá todos los mensajes de todos los oyentes.

st0ne
fuente
6

Para unirse al grupo de multidifusión, Python utiliza la interfaz de socket del sistema operativo nativo. Debido a la portabilidad y estabilidad del entorno Python, muchas de las opciones de socket se reenvían directamente a la llamada de setsockopt de socket nativo. El modo de funcionamiento de multidifusión, como unirse y eliminar la pertenencia a un grupo, solo se puede lograr con setsockopt.

El programa básico para recibir paquetes IP de multidifusión puede tener este aspecto:

from socket import *

multicast_port  = 55555
multicast_group = "224.1.1.1"
interface_ip    = "10.11.1.43"

s = socket(AF_INET, SOCK_DGRAM )
s.bind(("", multicast_port ))
mreq = inet_aton(multicast_group) + inet_aton(interface_ip)
s.setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, str(mreq))

while 1:
    print s.recv(1500)

En primer lugar, crea socket, lo vincula y activa la unión de grupos de multidifusión mediante la emisión setsockopt. Al final, recibe paquetes para siempre.

El envío de tramas IP de multidifusión es sencillo. Si tiene una sola NIC en su sistema, el envío de dichos paquetes no difiere del envío habitual de tramas UDP. Todo lo que necesita es configurar la dirección IP de destino correcta en el sendto()método.

Me di cuenta de que, de hecho, muchos ejemplos en torno a Internet funcionan por accidente. Incluso en la documentación oficial de Python. El problema para todos ellos es utilizar struct.pack incorrectamente. Tenga en cuenta que el ejemplo típico se usa 4slcomo formato y no está alineado con la estructura de interfaz de socket del sistema operativo real.

Intentaré describir lo que sucede debajo del capó cuando ejercito la llamada setockopt para el objeto de socket de python.

Python reenvía la llamada al método setsockopt a la interfaz de socket C nativa. La documentación del socket de Linux (ver man 7 ip) presenta dos formas de ip_mreqnestructura para la opción IP_ADD_MEMBERSHIP. La forma más corta tiene 8 bytes de longitud y la más larga tiene 12 bytes. El ejemplo anterior genera una setsockoptllamada de 8 bytes donde los primeros cuatro bytes definen multicast_groupy los segundos cuatro bytes definen interface_ip.

Leszek Wojcik
fuente
2

Eche un vistazo a py-multicast . El módulo de red puede comprobar si una interfaz admite multidifusión (al menos en Linux).

import multicast
from multicast import network

receiver = multicast.MulticastUDPReceiver ("eth0", "238.0.0.1", 1234 )
data = receiver.read()
receiver.close()

config = network.ifconfig()
print config['eth0'].addresses
# ['10.0.0.1']
print config['eth0'].multicast
#True - eth0 supports multicast
print config['eth0'].up
#True - eth0 is up

¿Quizás los problemas de no ver IGMP se debieron a una interfaz que no admite multidifusión?

injusticia
fuente
2

Solo otra respuesta para explicar algunos puntos sutiles en el código de las otras respuestas:

  • socket.INADDR_ANY- (Editado) En el contexto de IP_ADD_MEMBERSHIP, esto realmente no vincula el socket a todas las interfaces, sino que simplemente elige la interfaz predeterminada donde la multidifusión está activa (según la tabla de enrutamiento)
  • Unirse a un grupo de multidifusión no es lo mismo que vincular un socket a una dirección de interfaz local

consulte ¿Qué significa enlazar un socket de multidifusión (UDP)? para obtener más información sobre cómo funciona la multidifusión

Receptor de multidifusión:

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)

uso de muestra: (ejecute lo siguiente en dos consolas y elija su propia cara (debe ser la misma que la interfaz que recibe los datos de multidifusión))

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'

Remitente de multidifusión:

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)

uso de muestra: # suponga que el receptor se une a la siguiente dirección de grupo de multidifusión y que algunos programas solicitan unirse a ese grupo. Y para simplificar el caso, suponga que el receptor y el remitente están en la misma subred

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

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

pterodragón
fuente
INADDR_ANY no 'elige una de las interfaces locales]'.
Marqués de Lorne
0

Para que el código del cliente (de tolomea) funcione en Solaris, debe pasar el valor ttl para la IP_MULTICAST_TTLopción de socket como un carácter sin firmar. De lo contrario, obtendrá un error. Esto funcionó para mí en Solaris 10 y 11:

import socket
import struct

MCAST_GRP = '224.1.1.1'
MCAST_PORT = 5007
ttl = struct.pack('B', 2)

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
sock.sendto("robot", (MCAST_GRP, MCAST_PORT))
conejito
fuente
-1

La respuesta de Tolomea funcionó para mí. Lo pirateé en socketserver.UDPServer también:

class ThreadedMulticastServer(socketserver.ThreadingMixIn, socketserver.UDPServer):
    def __init__(self, *args):
        super().__init__(*args)
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind((MCAST_GRP, MCAST_PORT))
        mreq = struct.pack('4sl', socket.inet_aton(MCAST_GRP), socket.INADDR_ANY)
        self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
tompreston
fuente