overview
不像TCP,UDP是没有连接的概念的。UDP socket可以接收任意网络服务器的报文和发送报文到任意网络主机。报文是无序的,并且可能会丢失或者重复。
由于没有连接,我们只需要为每个UDPsocket创建一个Protocol对象,然后使用reactor连接Protocol和UDP transport,使用twisted.internet.interfaces.IReactorUDP reactor API.
DatagramProtocol¶
用于实现Protocol解析和处理的类通常继承twisted.internet.protocol.DatagramProtocol或者它的子类。DatagramProtocol类通过网络接收和发送报文。接收的报文包括发送地址。当发送报文时必须指定目的地址。
from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor class Echo(DatagramProtocol): def datagramReceived(self, data, addr): print("received {!r} from {}".format(data, addr)) self.transport.write(data, addr) reactor.listenUDP(9999, Echo()) reactor.run()
从上图可见,Protocol注册到reactor,意味着它如果添加到应用就会被持久化,这样当Protocol连接或者断开连接UDP socket时,它就有startProtocol and stopProtocol方法可调用。
Protocol的transport属性是实现twisted.internet.interfaces.IUDPTransport接口的。 self.transport.write的参数addr是一个带IP地址和端口号的元组。第一个元素必须是IP地址,不能是主机名。如果只有主机名,则需要使用 reactor.resolve()方法解析出地址(参考twisted.internet.interfaces.IReactorCore.resolve())
另外需要注意的是写到transport的数据必须是bytes字节类型的。python2可以写string,但是python3会失败。
To confirm that socket is indeed listening you can try following command line one-liner.
> 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
Adopting Datagram Ports
默认reactor.listenUDP()会创建合适的socket,也可以通过 adoptDatagramPort API 添加已经存在的socket SOCK_DGRAM文件描述符到reactor。
import socket from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor class Echo(DatagramProtocol): def datagramReceived(self, data, addr): print("received {!r} from {}".format(data, 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文件描述符前设置socket是非阻塞。
adoptDatagramPort不能检测到当前adopted socket的family,所有要保证传送正确的family参数。
reactor不会关闭socket。当socket不再使用时,创建socket的进程需要关闭和清理socket。
Connected UDP
Connected UDP和标准的只是从单个地址发送和接收报文的socket有一点不太一样。但是并不意味着一个连接的数据报文会按照任何顺序到达,对端端口没有在监听。connected UDP socket的好处是它提供了未发送数据包通知。这依赖于很多脱离应用控制的factors,但是还是有时会有一些好处。
和常规UDP协议不一样,我们不需要指定发送和接收报文的地址,它们只会来自‘connected’socket的地址。
from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor 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("received {!r} from {}".format(data, 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地址,不解析主机名,可以通过reactor.resolve()获取主机名的IP地址。
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()
在已有连接情况下连接一个新的地址或者断开一个连接的端口当前是不支持的。
Multicast UDP
多播允许一个进程用单个数据包和多个主机通讯,不需要知道任何的主机地址。这是和单播有明显差异的,单播UDP只有一个目的IP地址。多播报文发送到指定的端口和多播组地址(224.0.0.0-239.255.255.255),如果想接收多播报文,就要加入指定的组地址。然而,任何UDP socket都可以发送到多播地址。这是一个简单服务器例子:
MulticastServer.py
from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor 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("Datagram {} received from {}".format(repr(datagram), 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 protocol实现。主要的区别是替换listenUDP,listenMulticast是通过端口号调用的。服务器调用joinGroup去加入多播组。DatagramProtocol监听加入的多播组可以接收多播报文,但是单播报文是直接发送到它的地址的。上面例子的服务器发送了一个单播消息到客户端以响应多播消息。
from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor 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("Datagram {} received from {}".format(repr(datagram), repr(address))) reactor.listenMulticast(9999, MulticastPingClient(), listenMultiple=True) reactor.run()
需要注意多播socket的默认TTL值是1.这样报文传输不会超过一跳路由,除非通过setTTL设置更大的TTL值。多播传输的其它功能包括setOutgoingInterface and setLoopbackMode ,参考 IMulticastTransport。
测试多播需要启动一个服务器和多个客户端,如果运行正常,可以看到ping消息在每个连接的客户端log中。
Broadcast UDP
广播是另外一个连接未知主机的方法。广播通过发送数据包到本地网络的广播地址。广播默认会被路由器过滤,没有多播的组概念,只有不同的端口。
广播通过 setBroadcastAllowed在端口设为True启用。检查广播状态可以在端口 getBroadcastAllowed 。
这些特性的完整例子参考udpbroadcast.py.
IPv6
UDP socket可以通过绑定IPV6地址发送和接收报文。给listenUDP接口传送IPV6地址作为参数,reactor就可以启动发送接收UDP报文的IPV6 socket。
from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor class Echo(DatagramProtocol): def datagramReceived(self, data, addr): print("received {!r} from {}".format(data, addr)) self.transport.write(data, addr) reactor.listenUDP(9999, Echo(), interface="::") reactor.run()