UDP 网络

概述

与 TCP 不同,UDP 没有连接的概念。UDP 套接字可以接收来自网络上任何服务器的数据报,并发送数据报到网络上的任何主机。此外,数据报可能以任何顺序到达,可能根本不到达,或者在传输过程中被复制。

由于没有连接,我们只使用一个对象,即协议,来处理每个 UDP 套接字。然后,我们使用 reactor 将此协议连接到 UDP 传输,使用 twisted.internet.interfaces.IReactorUDP reactor API。

DatagramProtocol

您实际实现协议解析和处理的类通常会继承自 twisted.internet.protocol.DatagramProtocol 或其便利子类之一。 DatagramProtocol 类接收数据报,并可以将数据报发送到网络。接收到的数据报包含它们发送的地址。发送数据报时,必须指定目标地址。

以下是一个简单的示例

basic_example.py

from twisted.internet import reactor
from twisted.internet.protocol import DatagramProtocol


class Echo(DatagramProtocol):
    def datagramReceived(self, data, addr):
        print(f"received {data!r} from {addr}")
        self.transport.write(data, addr)


reactor.listenUDP(9999, Echo())
reactor.run()

如您所见,协议已注册到 reactor。这意味着如果它被添加到应用程序中,它可能会被持久化,因此它具有 startProtocolstopProtocol 方法,这些方法将在协议连接到 UDP 套接字和断开连接时被调用。

协议的 transport 属性将实现 twisted.internet.interfaces.IUDPTransport 接口。请注意,addr 参数传递给 self.transport.write 应该是一个包含 IP 地址和端口号的元组。元组的第一个元素必须是 IP 地址,而不是主机名。如果您只有主机名,请使用 reactor.resolve() 来解析地址(请参阅 twisted.internet.interfaces.IReactorCore.resolve())。

需要注意的是,写入传输的数据必须是字节。尝试写入字符串在 Python 2 中可能可以正常工作,但在使用 Python 3 时会失败。

要确认套接字确实在监听,您可以尝试以下命令行单行代码。

> echo "Hello World!" | nc -4u -w1 localhost 9999

如果一切正常,您的“服务器”日志应该打印

received b'Hello World!\n' from ('127.0.0.1', 32844) # where 32844 is some random port number

采用数据报端口

默认情况下,reactor.listenUDP() 调用将为您创建合适的套接字,但也可以使用 adoptDatagramPort API 将现有 SOCK_DGRAM 文件描述符添加到 reactor。

以下是一个简单的示例

adopt_datagram_port.py

import socket

from twisted.internet import reactor
from twisted.internet.protocol import DatagramProtocol


class Echo(DatagramProtocol):
    def datagramReceived(self, data, addr):
        print(f"received {data!r} from {addr}")
        self.transport.write(data, addr)


# Create new socket that will be passed to reactor later.
portSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Make the port non-blocking and start it listening.
portSocket.setblocking(False)
portSocket.bind(("127.0.0.1", 9999))

# Now pass the port file descriptor to the reactor.
port = reactor.adoptDatagramPort(portSocket.fileno(), socket.AF_INET, Echo())

# The portSocket should be cleaned up by the process that creates it.
portSocket.close()

reactor.run()

注意

  • 您必须确保套接字是非阻塞的,然后才能将其文件描述符传递给 adoptDatagramPort

  • adoptDatagramPort 无法 (目前) 检测到已采用套接字的族,因此您必须确保传递了正确的套接字族参数。

  • reactor 不会关闭套接字。创建套接字的进程有责任在不再需要时关闭和清理套接字。

连接的 UDP

连接的 UDP 套接字与标准 UDP 套接字略有不同,因为它只能发送和接收来自单个地址的数据报。但是,这并不意味着存在连接,因为数据报可能仍然以任何顺序到达,并且另一端的端口可能没有监听。连接的 UDP 套接字的优点是它 **可能** 提供未送达数据包的通知。这取决于许多因素(几乎所有因素都超出了应用程序的控制范围),但仍然具有一定的优势,有时会使其有用。

与常规 UDP 协议不同,我们不需要指定发送数据报的位置,也不需要知道数据报来自哪里,因为它们只能来自套接字“连接”到的地址。

connected_udp.py

from twisted.internet import reactor
from twisted.internet.protocol import DatagramProtocol


class Helloer(DatagramProtocol):
    def startProtocol(self):
        host = "192.168.1.1"
        port = 1234

        self.transport.connect(host, port)
        print("now we can only send to host %s port %d" % (host, port))
        self.transport.write(b"hello")  # no need for address

    def datagramReceived(self, data, addr):
        print(f"received {data!r} from {addr}")

    # Possibly invoked if there is no server listening on the
    # address to which we are sending.
    def connectionRefused(self):
        print("No one listening")


# 0 means any port, we don't care in this case
reactor.listenUDP(0, Helloer())
reactor.run()

请注意,connect()write() 一样,只接受 IP 地址,不接受未解析的主机名。要获取主机名的 IP,请使用 reactor.resolve(),例如

getting_ip.py

from twisted.internet import reactor


def gotIP(ip):
    print("IP of 'localhost' is", ip)
    reactor.stop()


reactor.resolve("localhost").addCallback(gotIP)
reactor.run()

在之前的连接之后连接到新地址,或者使连接的端口断开连接,目前不支持,但将来可能会支持。

组播 UDP

组播允许进程使用单个数据包联系多个主机,而无需知道任何主机的特定 IP 地址。这与普通 UDP 或单播 UDP 形成对比,在单播 UDP 中,每个数据报都有一个唯一的 IP 作为其目标。组播数据报被发送到特殊的组播组地址(在 IPv4 范围 224.0.0.0 到 239.255.255.255 中),以及相应的端口。为了接收组播数据报,您必须加入该特定组地址。但是,任何 UDP 套接字都可以发送到组播地址。以下是一个简单的服务器示例

MulticastServer.py

from twisted.internet import reactor
from twisted.internet.protocol import DatagramProtocol


class MulticastPingPong(DatagramProtocol):
    def startProtocol(self):
        """
        Called after protocol has started listening.
        """
        # Set the TTL>1 so multicast will cross router hops:
        self.transport.setTTL(5)
        # Join a specific multicast group:
        self.transport.joinGroup("228.0.0.5")

    def datagramReceived(self, datagram, address):
        print(f"Datagram {repr(datagram)} received from {repr(address)}")
        if datagram == b"Client: Ping" or datagram == "Client: Ping":
            # Rather than replying to the group multicast address, we send the
            # reply directly (unicast) to the originating port:
            self.transport.write(b"Server: Pong", address)


# We use listenMultiple=True so that we can run MulticastServer.py and
# MulticastClient.py on same machine:
reactor.listenMulticast(9999, MulticastPingPong(), listenMultiple=True)
reactor.run()

与 UDP 一样,组播在协议级别没有服务器/客户端区分。我们的服务器示例非常简单,与正常的 listenUDP 协议实现非常相似。主要区别在于,不是使用 listenUDP,而是使用 listenMulticast,并传入端口号。服务器调用 joinGroup 来加入组播组。一个使用组播监听并已加入组的 DatagramProtocol 可以接收组播数据报,也可以接收直接发送到其地址的单播数据报。上面的示例中的服务器在回复它从客户端接收到的组播消息时,会发送这样的单播消息。

客户端代码可能如下所示

MulticastClient.py

from twisted.internet import reactor
from twisted.internet.protocol import DatagramProtocol


class MulticastPingClient(DatagramProtocol):
    def startProtocol(self):
        # Join the multicast address, so we can receive replies:
        self.transport.joinGroup("228.0.0.5")
        # Send to 228.0.0.5:9999 - all listeners on the multicast address
        # (including us) will receive this message.
        self.transport.write(b"Client: Ping", ("228.0.0.5", 9999))

    def datagramReceived(self, datagram, address):
        print(f"Datagram {repr(datagram)} received from {repr(address)}")


reactor.listenMulticast(9999, MulticastPingClient(), listenMultiple=True)
reactor.run()

请注意,多播套接字的默认 TTL(生存时间)为 1。也就是说,数据报不会跨越超过一个路由器跳跃,除非使用 setTTL 设置了更高的 TTL。多播传输提供的其他功能包括 setOutgoingInterfacesetLoopbackMode - 有关更多信息,请参阅 IMulticastTransport

要测试您的多播设置,您需要在一个终端中启动服务器,在其他终端中启动几个客户端。如果一切正常,您应该在所有其他连接的客户端的日志中看到每个客户端发送的“Ping”消息。

广播 UDP

广播允许以不同的方式联系多个未知主机。通过 UDP 进行广播通过发送到一个神奇的广播地址 ("<broadcast>") 来将数据包发送到本地网络上的所有主机。默认情况下,路由器会过滤此广播,并且没有像多播那样的“组”,只有不同的端口。

通过将 True 传递给端口上的 setBroadcastAllowed 来启用广播。可以使用端口上的 getBroadcastAllowed 检查广播状态。

有关此功能的完整示例,请参阅 udpbroadcast.py

IPv6

UDP 套接字也可以绑定到 IPv6 地址以支持通过 IPv6 发送和接收数据报。通过将 IPv6 地址传递给 listenUDPinterface 参数,反应器将启动一个 IPv6 套接字,该套接字可用于发送和接收 UDP 数据报。

ipv6_listen.py

from twisted.internet import reactor
from twisted.internet.protocol import DatagramProtocol


class Echo(DatagramProtocol):
    def datagramReceived(self, data, addr):
        print(f"received {data!r} from {addr}")
        self.transport.write(data, addr)


reactor.listenUDP(9999, Echo(), interface="::")
reactor.run()