Как вы делаете многоадресную рассылку UDP в Python?

86

Как вы отправляете и получаете многоадресную рассылку UDP в Python? Есть ли для этого стандартная библиотека?

NoName
источник

Ответы:

98

Это работает для меня:

Получить

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)

послать

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

Он основан на примерах из http://wiki.python.org/moin/UdpCommunication, которые не работали.

Моя система ... Linux 2.6.31-15-generic # 50-Ubuntu SMP Вт 10 ноября 14:54:29 UTC 2009 i686 GNU / Linux Python 2.6.4

Гордон Ригли
источник
6
Для mac os x вам необходимо использовать параметр socket.SO_REUSEPORT в качестве альтернативы socket.SO_REUSEADDR в приведенном выше примере, чтобы разрешить несколько прослушивателей на одной и той же комбинации адресов многоадресного порта.
атикат 08
Для отправки мне также понадобился «sock.bind ((<local ip>, 0))», потому что мой слушатель многоадресной рассылки был привязан к определенному адаптеру.
Марк Форман
2
для многоадресной рассылки udp вам необходимо привязать к группе / порту многоадресной рассылки, а не к порту локальной группы sock.bind((MCAST_GRP, MCAST_PORT)), ваш код может работать, а может и не работать, он может не работать, если у вас несколько
сетевых адаптеров
@atikat: Спасибо !! Хотя зачем нам это на MAC, а не на Ubuntu?
Кьюби
2
@RandallCook: Когда я заменяю '' на MCAST_GRP, я получаю socket.error: [Errno 10049] Запрошенный адрес недействителен в его контексте
stewbasic
17

Отправитель многоадресной рассылки, транслирующий в группу многоадресной рассылки:

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

Приемник многоадресной рассылки, который читает из группы многоадресной рассылки и выводит на консоль шестнадцатеричные данные:

#!/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()
Ниранджан Тулпуле
источник
Я пробовал это, не вышло. В Wireshark я вижу передачу, но я не вижу никаких материалов для присоединения IGMP и ничего не получаю.
Гордон Ригли
1
вам нужно привязаться к группе / порту многоадресной рассылки, а не к локальному порту на многоадресном адресе,sock.bind((MCAST_GRP, MCAST_PORT))
stefanB
1
Этот пример мне не подходит по непонятной причине. Использование socket.gethostbyname (socket.gethostname ()) для выбора интерфейса не всегда выбирает внешний интерфейс - на самом деле, в системах Debian он имеет тенденцию выбирать адрес обратной связи. Debian добавляет запись 127.0.1.1 в таблицу хостов для имени хоста. Вместо этого более эффективно использовать socket.INADDR_ANY, который ответ с более высоким рейтингом использует с помощью оператора pack (что более правильно, чем «+»). Кроме того, использование IP_MULTICAST_IF не требуется, поскольку ответ с более высоким рейтингом правильно указывает.
Брайан Булковски,
1
@BrianBulkowski: есть много программистов, которые используют socket.INADDR_ANY, к большому огорчению и ужасу тех из нас, у кого есть несколько интерфейсов, которым нужны данные многоадресной рассылки, которые поступают на конкретный интерфейс. Решение - не socket.INADDR_ANY. Это необходимо для выбора подходящего интерфейса по IP-адресу, который вы считаете лучшим (файл конфигурации, запрашивающий у конечного пользователя, в зависимости от потребностей вашего приложения). socket.INADDR_ANY вернет вам данные многоадресной рассылки, правда, и это проще всего, если вы предполагаете, что хост является однодомным, но я думаю, что это менее правильно.
Mike S
@MikeS, хотя я согласен с вами в некотором принципе, идея использования IP-адресов для выбора интерфейсов ужасно, ужасно чревата. Я хорошо знаю проблему, но в динамичном мире IP-адрес не является решением. Итак, вам нужно написать код, который повторяет все и выбирает по имени интерфейса, смотрит на имя интерфейса, выбирает текущий IP-адрес и использует его. Надеюсь, IP-адрес за это время не изменился. Я бы хотел, чтобы Linux / Unix стандартизировал использование повсюду имен интерфейсов, а также языки программирования, которые сделали бы конфигурационный файл более разумным.
Брайан Булковски
13

Лучше использовать:

sock.bind((MCAST_GRP, MCAST_PORT))

вместо того:

sock.bind(('', MCAST_PORT))

потому что, если вы хотите прослушивать несколько групп многоадресной рассылки на одном порту, вы получите все сообщения на всех слушателях.

st0ne
источник
6

Для присоединения к группе многоадресной рассылки Python использует собственный интерфейс сокета ОС. Благодаря переносимости и стабильности среды Python многие параметры сокетов напрямую перенаправляются на собственный вызов setsockopt сокета. Режим многоадресной рассылки, такой как присоединение и удаление членства в группах, может быть выполнен setsockoptтолько пользователем.

Базовая программа для получения многоадресного IP-пакета может выглядеть так:

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)

Сначала он создает сокет, связывает его и запускает триггерное присоединение к группе многоадресной рассылки путем выдачи setsockopt. В самом конце он получает пакеты навсегда.

Отправка многоадресных IP-кадров выполняется просто. Если в вашей системе есть одна сетевая карта, отправка таких пакетов не отличается от обычной отправки кадров UDP. Все, о чем вам нужно позаботиться, это просто установить правильный IP-адрес назначения вsendto() методе.

Я заметил, что множество примеров того, как Интернет работает случайно, на самом деле. Даже в официальной документации Python. Проблема для всех из них неправильно использует struct.pack. Обратите внимание, что в типичном примере используется4sl формат, и он не согласован с фактической структурой интерфейса сокета ОС.

Я попытаюсь описать, что происходит под капотом при выполнении вызова setsockopt для объекта сокета python.

Python перенаправляет вызов метода setsockopt на собственный интерфейс сокета C. В документации по сокетам Linux (см. man 7 ip) Представлены две формы ip_mreqnструктуры для параметра IP_ADD_MEMBERSHIP. Самая короткая форма имеет длину 8 байтов, а более длинная - 12 байтов. В приведенном выше примере генерируется 8-байтовый setsockoptвызов, в котором первые четыре байта определяют, multicast_groupа вторые четыре байта определяют interface_ip.

Лешек Войчик
источник
2

Взгляните на py-multicast . Сетевой модуль может проверить, поддерживает ли интерфейс многоадресную рассылку (по крайней мере, в 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

Возможно, проблемы с отсутствием просмотра IGMP были вызваны интерфейсом, не поддерживающим многоадресную рассылку?

оборота
источник
2

Еще один ответ, чтобы объяснить некоторые тонкости кода других ответов:

  • socket.INADDR_ANY- (Отредактировано) В контексте IP_ADD_MEMBERSHIP, это на самом деле не связывает сокет со всеми интерфейсами, а просто выбирает интерфейс по умолчанию, в котором работает многоадресная передача (согласно таблице маршрутизации)
  • Присоединение к группе многоадресной рассылки - это не то же самое, что привязка сокета к адресу локального интерфейса.

см. Что означает привязка многоадресного (UDP) сокета? для получения дополнительной информации о том, как работает многоадресная рассылка

Приемник многоадресной рассылки:

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)

пример использования: (запустите ниже на двух консолях и выберите свой --iface (должен быть таким же, как интерфейс, который получает данные многоадресной рассылки))

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'

Отправитель многоадресной рассылки:

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)

пример использования: # предположим, что получатель привязан к указанному ниже адресу группы многоадресной рассылки и что какая-то программа запрашивает присоединение к этой группе. И для упрощения предположим, что получатель и отправитель находятся в одной подсети.

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

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

оборота птеродракон
источник
INADDR_ANY не «выбирает один из локальных интерфейсов]».
Marquis of Lorne
0

Чтобы клиентский код (из tolomea) работал в Solaris, вам необходимо передать значение ttl для параметра IP_MULTICAST_TTLsocket в виде символа без знака. В противном случае вы получите ошибку. Это сработало для меня на Solaris 10 и 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

Ответ tolomea сработал для меня. Я тоже взломал его в socketserver.UDPServer :

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
источник