UDP 网络¶
概述¶
与 TCP 不同,UDP 没有连接的概念。UDP 套接字可以接收来自网络上任何服务器的数据报,并发送数据报到网络上的任何主机。此外,数据报可能以任何顺序到达,可能根本不到达,或者在传输过程中被复制。
由于没有连接,我们只使用一个对象,即协议,来处理每个 UDP 套接字。然后,我们使用 reactor 将此协议连接到 UDP 传输,使用 twisted.internet.interfaces.IReactorUDP
reactor API。
DatagramProtocol¶
您实际实现协议解析和处理的类通常会继承自 twisted.internet.protocol.DatagramProtocol
或其便利子类之一。 DatagramProtocol
类接收数据报,并可以将数据报发送到网络。接收到的数据报包含它们发送的地址。发送数据报时,必须指定目标地址。
以下是一个简单的示例
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。这意味着如果它被添加到应用程序中,它可能会被持久化,因此它具有 startProtocol
和 stopProtocol
方法,这些方法将在协议连接到 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。
以下是一个简单的示例
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 协议不同,我们不需要指定发送数据报的位置,也不需要知道数据报来自哪里,因为它们只能来自套接字“连接”到的地址。
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()
,例如
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 套接字都可以发送到组播地址。以下是一个简单的服务器示例
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
可以接收组播数据报,也可以接收直接发送到其地址的单播数据报。上面的示例中的服务器在回复它从客户端接收到的组播消息时,会发送这样的单播消息。
客户端代码可能如下所示
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。多播传输提供的其他功能包括 setOutgoingInterface
和 setLoopbackMode
- 有关更多信息,请参阅 IMulticastTransport
。
要测试您的多播设置,您需要在一个终端中启动服务器,在其他终端中启动几个客户端。如果一切正常,您应该在所有其他连接的客户端的日志中看到每个客户端发送的“Ping”消息。
广播 UDP¶
广播允许以不同的方式联系多个未知主机。通过 UDP 进行广播通过发送到一个神奇的广播地址 ("<broadcast>"
) 来将数据包发送到本地网络上的所有主机。默认情况下,路由器会过滤此广播,并且没有像多播那样的“组”,只有不同的端口。
通过将 True
传递给端口上的 setBroadcastAllowed
来启用广播。可以使用端口上的 getBroadcastAllowed
检查广播状态。
有关此功能的完整示例,请参阅 udpbroadcast.py
。
IPv6¶
UDP 套接字也可以绑定到 IPv6 地址以支持通过 IPv6 发送和接收数据报。通过将 IPv6 地址传递给 listenUDP
的 interface
参数,反应器将启动一个 IPv6 套接字,该套接字可用于发送和接收 UDP 数据报。
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()