如何在Python中进行UDP组播?

103

在Python中如何发送和接收UDP多播数据?是否有标准库可供使用?

10个回答

123
这对我有用:
收到
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://dev59.com/YFgQ5IYBdhLWcg3wikeT#42612820)
sock.sendto("robot", (MCAST_GRP, MCAST_PORT))

这是基于http://wiki.python.org/moin/UdpCommunication上的示例进行修改的,但实际并未奏效。

我的系统环境为... Linux 2.6.31-15-generic #50-Ubuntu SMP Tue Nov 10 14:54:29 UTC 2009 i686 GNU/Linux Python 2.6.4


8
对于Mac OS X,您需要使用socket.SO_REUSEPORT选项作为socket.SO_REUSEADDR的替代选项,在上述示例中允许在相同的多播端口地址组合上有多个侦听器。 - atikat
1
为了发送,我还需要使用“sock.bind((<本地IP>, 0))”,因为我的多播监听器绑定到了特定的适配器。 - Mark Foreman
2
对于UDP多播,您需要绑定到多播组/端口而不是本地组/端口,sock.bind((MCAST_GRP,MCAST_PORT))。您的代码可能会工作,也可能不会在您有多个网卡时无法工作。 - stefanB
@atikat:谢谢!虽然为什么在MAC上需要这个但在Ubuntu上不需要呢? - Kyuubi
4
当我把''替换成MCAST_GRP时,我遇到了socket.error: [Errno 10049]The requested address is not valid in its context的错误。 - stewbasic
显示剩余6条评论

22

广播到组播组的多播发送者:

#!/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 'Exception'
    else:
      hexdata = binascii.hexlify(data)
      print 'Data = %s' % hexdata

if __name__ == '__main__':
  main()

1
我尝试了这个,但它没有起作用。在Wireshark中,我可以看到传输,但我没有看到任何IGMP加入的内容,也没有收到任何东西。 - Gordon Wrigley
2
您需要绑定到多播地址的组/端口,而不是本地端口, 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。 - Brian Bulkowski
1
@BrianBulkowski 有很多程序员使用socket.INADDR_ANY,这会让我们那些拥有多个接口的人感到痛苦和困惑,因为我们需要在特定的接口上接收组播数据。解决方案不是socket.INADDR_ANY。而是通过IP地址选择正确的接口,以您认为最好的方式(配置文件,询问最终用户,或者您为应用程序的需要选择的任何方式)。socket.INADDR_ANY可以让您获取组播数据,确实如此,并且如果您假设单向主机,则最简单,但我认为它不太正确。 - Mike S
@MikeS 虽然我在某些原则上同意你的观点,但使用IP地址来选择接口的想法非常非常棘手。我很了解这个问题,但在一个动态的世界中,IP地址不是答案。因此,您需要编写代码来迭代所有内容并通过接口名称进行选择,查看接口名称,挑选出当前IP地址并使用它。希望IP地址在此期间没有更改。我希望Linux/Unix已经在任何地方都标准化使用接口名称,并且编程语言也是如此,这将使配置文件更加合理。 - Brian Bulkowski
显示剩余3条评论

14

最好使用:

sock.bind((MCAST_GRP, MCAST_PORT))

使用以下代码:

sock.bind(('', MCAST_PORT))

因为如果你想在同一端口上监听多个多播组,你将会收到所有的消息。


10
为了加入组播组,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帧很简单。如果您的系统中只有单个NIC,则发送此类数据包与发送常规UDP数据帧没有区别。您需要注意的是,在sendto()方法中设置正确的目标IP地址。
我注意到互联网上许多示例实际上都是偶然运行的。即使在官方的Python文档中也是如此。它们的问题在于错误地使用了struct.pack。请注意,典型的示例使用4sl作为格式,它与实际操作系统套接字接口结构不对齐。
我将尝试描述在 python 套接字对象调用 setsockopt 时发生的底层情况。
Python 将 setsockopt 方法调用转发到本机 C 套接字接口。Linux 套接字文档(参见 man 7 ip)为 IP_ADD_MEMBERSHIP 选项引入了两种 ip_mreqn 结构形式。最短的表单为8个字节长,较长的表单为12个字节长。上面的示例生成8个字节的setsockopt调用,其中前四个字节定义 multicast_group,后四个字节定义interface_ip。

3

为了解释其他答案中代码中的一些微妙点,这里提供另一个答案:

  • socket.INADDR_ANY - (编辑)在IP_ADD_MEMBERSHIP的上下文中,这并不是真正地将套接字绑定到所有接口,而是选择组播已启用的默认接口(根据路由表)
  • 加入多播组并不等同于将套接字绑定到本地接口地址

有关多播如何工作的更多信息,请参见What does it mean to bind a multicast (UDP) socket?

多播接收器:

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并不是“选择本地接口之一”。 - user207421

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的问题是由于接口不支持组播引起的?

0
为了使来自tolomea的客户端代码在Solaris上运行,您需要将IP_MULTICAST_TTL套接字选项的ttl值传递为无符号字符。否则,您将会遇到错误。这对我在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))

0

GumD和GumC https://github.com/futzu/gumd

我主要使用GumD处理视频,但您可以用它来处理任何类型的文件。

    pip3 install  gumd

gumd(守护进程)

    >>>> from gumd import GumD
    >>>> gumd =GumD('235.35.3.5:3535',1)
    >>>> gumd.mcast("/home/a/stuff.txt") 
    # Can also use http(s), UDP and multicast
     stream uri: udp://@235.35.3.5:3535
     >>>>

使用gumc(客户端)


    >>>> from gumc import GumC
    >>>> gumc = GumC("udp://@235.35.3.5:3535")
    >>>> data = gumc.read(8)
    >>>> data
    b'Helloooo'

0
对于那些使用Windows的程序员,下面的代码片段可在Windows和Linux上都正常工作。
那些使用SOL_IP的示例,在Python版本大于3.x.12的Windows上会报错。
许多示例没有设置IP_MULTICAST_IF。这对于具有多个接口的系统是很重要的。在Windows上,需要使用IP_MULTICAST_IF来指定网络接口,因为Windows不能绑定到多播地址。
import socket
import platform
from contextlib import closing

address = "239.50.50.50"
port = 6000
network_adapter = "172.16.0.93"

with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)) as sock:
    # SO_REUSEADDR: allows binding to port potentially already in use
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # linux binds to multicast address, windows to interface address
    ip_bind = network_adapter if platform.system() == "Windows" else address
    sock.bind((ip_bind, port))
    
    # socket.IPPROTO_IP works on Linux and Windows
    # IP_MULTICAST_IF: force sending network traffic over specific network adapter
    # IP_ADD_MEMBERSHIP: join multicast group
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(network_adapter))
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(address) + socket.inet_aton(network_adapter))

    # send recv examples
    # sock.sendto(b"Hello World", (address, port))
    # data, server = sock.recvfrom(2**8)

1
我今天正在讨论这件事情,很高兴看到了你的帖子。我一直在推荐使用socket.IPPROTO_IP、socket.IP_MULTICAST_IF,但我没有一个好的反对使用SOL_SOCKET的理由,但你刚刚给了我一个,谢谢。 - Leroy Scandal

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接