Jak wykonać multiemisję UDP w Pythonie?


86

Jak wysyłać i odbierać multiemisję UDP w Pythonie? Czy jest do tego standardowa biblioteka?

Odpowiedzi:


98

To działa dla mnie:

Otrzymać

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)

Wysłać

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))

Opiera się na przykładach z http://wiki.python.org/moin/UdpCommunication, które nie działały.

Mój system to ... Linux 2.6.31-15-generic # 50-Ubuntu SMP Wt 10 Lis 14:54:29 UTC 2009 i686 GNU / Linux Python 2.6.4


6
W przypadku systemu Mac OS X należy użyć opcji socket.SO_REUSEPORT jako alternatywy dla socket.SO_REUSEADDR w powyższym przykładzie, aby zezwolić na wiele odbiorników na tej samej kombinacji adresów portów multiemisji.
atikat

Do wysyłania potrzebowałem również „sock.bind ((<local ip>, 0))”, ponieważ mój odbiornik multiemisji był powiązany z określoną kartą.
Mark Foreman

2
w przypadku multiemisji UDP musisz połączyć się z grupą / portem multiemisji, a nie z portem grupy lokalnej sock.bind((MCAST_GRP, MCAST_PORT)), Twój kod może i może nie działać, może nie działać, gdy masz wiele
kart sieciowych

@atikat: Dzięki !! Chociaż dlaczego potrzebujemy tego na MAC, a nie na Ubuntu?
Kyuubi

2
@RandallCook: Kiedy zastępuję „” przez MCAST_GRP, otrzymuję socket.error: [Errno 10049] Żądany adres nie jest prawidłowy w swoim kontekście
stewbasic

17

Nadawca multiemisji, który rozgłasza do grupy multiemisji:

#!/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()

Odbiornik multiemisji, który czyta z grupy multiemisji i drukuje dane szesnastkowe na konsoli:

#!/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()

Próbowałem tego, nie udało się. W Wireshark widzę transmisję, ale nie widzę żadnych sprzężeń IGMP i nic nie otrzymuję.
Gordon Wrigley,

1
musisz połączyć się z grupą / portem multiemisji, a nie portem lokalnym na adresie multiemisji,sock.bind((MCAST_GRP, MCAST_PORT))
stefanB

1
Ten przykład nie działa dla mnie z niejasnego powodu. Użycie socket.gethostbyname (socket.gethostname ()) do wybierania interfejsu nie zawsze wybiera interfejs zewnętrzny - w rzeczywistości w systemach debian ma tendencję do wybierania adresu pętli zwrotnej. Debian dodaje wpis 127.0.1.1 do tabeli hostów dla nazwy hosta. Zamiast tego bardziej efektywne jest użycie funkcji socket.INADDR_ANY, której odpowiedź o wyższym rankingu używa za pomocą instrukcji „pack” (która jest bardziej poprawna niż „+”). Ponadto użycie IP_MULTICAST_IF nie jest wymagane, ponieważ odpowiedź o wyższym rankingu jest poprawna.
Brian Bulkowski,

1
@BrianBulkowski jest wielu programistów, którzy używają gniazda.INADDR_ANY, ku wielkiemu nieszczęściu i konsternacji tych z nas, którzy mają wiele interfejsów, którzy potrzebują danych multicastowych, aby dotrzeć do określonego interfejsu. Rozwiązaniem nie jest gniazdo.INADDR_ANY. Polega na wybraniu odpowiedniego interfejsu na podstawie adresu IP, jakkolwiek uważasz za najlepszy (plik konfiguracyjny z pytaniem do użytkownika końcowego, jakkolwiek wybierzesz dla potrzeb swojej aplikacji). Gniazdo.INADDR_ANY dostanie dane multiemisji, prawda, i jest najłatwiejsze, jeśli założysz host z jednym adresem, ale myślę, że jest mniej poprawny.
Mike S

@MikeS Chociaż zgadzam się z Tobą co do zasady, pomysł wykorzystania adresów IP do wybierania interfejsów jest strasznie, strasznie napięty. Znam problem dobrze, ale w dynamicznym świecie, a adres IP nie jest odpowiedzią. Musisz więc napisać kod, który iteruje wszystko i wybiera według nazwy interfejsu, sprawdza nazwę interfejsu, wybiera aktualny adres IP i używa go. Miejmy nadzieję, że adres IP nie zmienił się w międzyczasie. Żałuję, że Linux / Unix ustandaryzował używanie nazw interfejsów wszędzie, a języki programowania tak, to uczyniłoby plik konfiguracyjny bardziej sensownym.
Brian Bulkowski

13

Lepsze użycie:

sock.bind((MCAST_GRP, MCAST_PORT))

zamiast:

sock.bind(('', MCAST_PORT))

ponieważ jeśli chcesz nasłuchiwać wielu grup multiemisji na tym samym porcie, otrzymasz wszystkie wiadomości na wszystkich odbiornikach.


6

Aby dołączyć do grupy multiemisji, Python używa natywnego interfejsu gniazda systemu operacyjnego. Ze względu na przenośność i stabilność środowiska Pythona, wiele opcji gniazd jest bezpośrednio przekazywanych do natywnego wywołania setockopt gniazda. Tryb pracy w trybie multiemisji, taki jak dołączanie i usuwanie członkostwa w grupie, może być realizowany setsockopttylko przez .

Podstawowy program do odbierania pakietów multicast IP może wyglądać następująco:

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)

Najpierw tworzy gniazdo, wiąże je i wyzwala dołączanie do grup multiemisji przez wydanie setsockopt. Na samym końcu otrzymuje pakiety na zawsze.

Wysyłanie ramek IP w trybie multiemisji jest proste. Jeśli masz jedną kartę sieciową w swoim systemie, wysyłanie takich pakietów nie różni się od zwykłego wysyłania ramek UDP. Wystarczy, że w sendto()metodzie ustawisz poprawny docelowy adres IP .

Zauważyłem, że wiele przykładów z Internetu działa przypadkowo. Nawet w oficjalnej dokumentacji Pythona. Problem dla wszystkich z nich używa niepoprawnie struct.pack. Należy pamiętać, że typowy przykład używa 4sljako formatu i nie jest zgodny z rzeczywistą strukturą interfejsu gniazda systemu operacyjnego.

Postaram się opisać, co dzieje się pod maską podczas ćwiczenia wywołania setsockopt dla obiektu gniazda Pythona.

Python przekazuje wywołanie metody setsockopt do natywnego interfejsu gniazda C. Dokumentacja gniazd Linuksa (zobacz man 7 ip) wprowadza dwie formy ip_mreqnstruktury dla opcji IP_ADD_MEMBERSHIP. Najkrótszy formularz ma długość 8 bajtów, a dłuższy 12 bajtów. Powyższy przykład generuje setsockoptwywołanie 8-bajtowe, w którym zdefiniowane są pierwsze cztery bajty, multicast_groupa drugie cztery bajty interface_ip.


2

Spójrz na py-multicast . Moduł sieciowy może sprawdzić, czy interfejs obsługuje multiemisję (przynajmniej na Linuksie).

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

Być może problemy z brakiem widoczności IGMP były spowodowane przez interfejs nie obsługujący multiemisji?


2

Jeszcze jedna odpowiedź, aby wyjaśnić niektóre subtelne punkty w kodzie innych odpowiedzi:

  • socket.INADDR_ANY- (Edytowano) W kontekście IP_ADD_MEMBERSHIP, to tak naprawdę nie wiąże gniazda ze wszystkimi interfejsami, ale po prostu wybiera domyślny interfejs, w którym działa multiemisja (zgodnie z tabelą routingu)
  • Dołączenie do grupy multiemisji to nie to samo, co powiązanie gniazda z adresem interfejsu lokalnego

zobacz Co to znaczy wiązać gniazdo multiemisji (UDP)? aby uzyskać więcej informacji na temat działania multiemisji

Odbiornik multiemisji:

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)

przykładowe użycie: (uruchom poniższe na dwóch konsolach i wybierz własny --iface (musi być taki sam jak interfejs, który odbiera dane multiemisji))

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'

Nadawca multiemisji:

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)

przykładowe użycie: # załóżmy, że odbiorca łączy się z poniższym adresem grupowym multiemisji i że jakiś program żąda przyłączenia się do tej grupy. Aby uprościć sprawę, załóżmy, że odbiorca i nadawca znajdują się w tej samej podsieci

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

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


INADDR_ANY nie „wybiera jednego z lokalnych interfejsów]”.
Markiz Lorne

0

Aby kod klienta (z tolomea) działał w systemie Solaris, musisz przekazać wartość ttl dla IP_MULTICAST_TTLopcji gniazda jako znak bez znaku. W przeciwnym razie pojawi się błąd. To zadziałało dla mnie na Solarisie 10 i 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))

-1

odpowiedź tolomei zadziałała dla mnie. Włamałem go do socketserver.UDPServer też:

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)
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.