在计算机网络和电磁信号理论中,对共享同一通信的多个信号进行区分是个常见的问题。多路复用(multiplexing) 就是允许多个会话共享同一介质或机制的一种解决方案。UDP 的设计者从数字领域分析,为每个 UDP 数据包分配了一对无符号16位端口号(port number),从0到65536。源端口(source port)标识了源机器上发送数据包的特定程序或者进程,而目标端口(destination port)则标识了目标 IP 地址上进行该会话的特定应用程序。
Source(IP: port number) -> Destination(IP: port number)
UDP 就仅仅使用 IP 地址和端口号进行标识,将数据包发送至目标地址。客户端想要获悉这些需要连接的端口号,会采用下面的方法:
惯例:互联网号码分配机构(IANA, Internet Assigned Number Authority),为许多专用服务分配了官方端口,如 DNS 默认为53号 UDP 端口。一般0-1023都被分配给了最重要最常用的服务。而1024-49151这些注册端口,在操作系统层没有任何特别之处,你可以占用,但一般会有一些应用申请或者默认端口选择在这里。剩余的就是可以随意使用的端口,当客户端无需指定特殊的端口时,现代操作系统会维护一个端口池来随机选取端口提供给该应用。
defserver(port): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("127.0.0.1", port)) print("Listening at {}".format(sock.getsockname())) whileTrue: data, address = sock.recvfrom(MAX_BYTES) text = data.decode("ascii") print("The client at {} says {!r}".format(address, text)) text = "Your data was {} bytes long".format(len(data)) data = text.encode("ascii") sock.sendto(data, address)
defclient(port): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) text = "The time is {} seconds".format(datetime.now()) data = text.encode("ascii") sock.sendto(data, ("127.0.0.1", port)) print("The OS assigned me the address {}".format(sock.getsockname())) data, address = sock.recvfrom(MAX_BYTES) # !Danger text = data.decode("ascii") print("The server {} replied {!r}".format(address, text))
if __name__ == "__main__": choices = {"client": client, "server": server} # 函数列表 # argparse 是一个用来解析命令行参数的 Python 库 # 创建解析器对象并且加上描述 parser = argparse.ArgumentParser(description="Send and receive UDP locally") parser.add_argument("role", choices=choices.keys(), help="which role to play") parser.add_argument("-p", metavar="PORT", type=int, default=1060, help="UDP port (default 1060)") # action参数的'store_true'指的是:触发 action时为真,不触发则为假。即储存了一个bool变量,默认为 false,触发不用赋值即变为true # type 指定参数类别 默认是str 传入数字要定义 # help 是一些提示信息 # default 是默认值 # metavar 在 usage 说明中的参数名称,对于必选参数默认就是参数名称,对于可选参数默认是全大写的参数名称 # https://docs.python.org/zh-cn/3/library/argparse.html args = parser.parse_args() func = choices[args.role] func(args.p)
首先使用 socket() 创建了一个空套接字,标记了所属的特定的类别:协议族 AF_INET 以及数据类型 SOCK_DGRAM,后者表示在 IP 网络上使用 UDP 协议。需要注意的是,数据报(datagram)是用来表示应用层数据块传输的官方术语。操作系统的网络栈并不保证传输线路上的单个数据包实际表示的就是单个数据报。
(venv) D:\my_py36\Python-Web\udp>python udp_local.py server Listening at ('127.0.0.1', 1060) # 执行一次客户端 The client at ('127.0.0.1', 51384) says 'The time is 2020-09-28 16:16:34.259636 seconds' # 执行第二次客户端 The client at ('127.0.0.1', 51385) says 'The time is 2020-09-28 16:16:44.724149 seconds'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
(venv) D:\my_py36\Python-Web\udp>python udp_local.py server Traceback (most recent call last): File "udp_local.py", line 48, in <module> func(args.p) File "udp_local.py", line 11, in server sock.bind(("127.0.0.1", port)) OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 # 执行第一次客户端程序 (venv) D:\my_py36\Python-Web\udp>python udp_local.py client The OS assigned me the address ('0.0.0.0', 51384) The server ('127.0.0.1', 1060) replied 'Your data was 46 bytes long' # 执行第二次客户端程序 (venv) D:\my_py36\Python-Web\udp>python udp_local.py client The OS assigned me the address ('0.0.0.0', 51385) The server ('127.0.0.1', 1060) replied 'Your data was 46 bytes long'
import argparse import socket import random import sys from datetime import datetime
MAX_BYTES = 65536
defserver(interface, port): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((interface, port)) print("Listening at {}".format(sock.getsockname())) whileTrue: data, address = sock.recvfrom(MAX_BYTES) if random.random() < 0.5: print("Pretending to drop packet from {}".format(address)) continue
text = data.decode("ascii") print("The client at {} says {!r}".format(address, text)) text = "Your data was {} bytes long".format(len(data)) data = text.encode("ascii") sock.sendto(data, address)
defclient(hostname, port): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) hostname = sys.argv[2] sock.connect((hostname, port)) print("client socket name is {}".format(sock.getsockname())) text = "The time is {} seconds".format(datetime.now()) data = text.encode("ascii") delay = 0.1 whileTrue: sock.send(data) print("Waiting up to {} seconds for a reply".format(delay)) sock.settimeout(delay) try: data = sock.recv(MAX_BYTES) except socket.timeout: delay = delay * 2# 等待更久 if delay > 2.0: raise RuntimeError("May be the server is down") else: break print("The server says {!r}".format(data.decode("ascii")))
if __name__ == "__main__": choices = {"client": client, "server": server} parser = argparse.ArgumentParser(description="Send and receive UDP, pretending packet dropped in transmission") parser.add_argument("role", choices=choices.keys(), help="which role to take") parser.add_argument("host", help="interface the server listens at / host the client sends to") parser.add_argument("-p", metavar="PORT", type=int, default=1060, help="UDP port (default 1060)") args = parser.parse_args() func = choices[args.role] func(args.host, args.p)
UDP 支持广播,通过广播可以将数据的目标地址设置为本机连接的整个子网,然后使用物理网卡将数据报广播,这样就无需再复制该数据包并单独发给所有连接至该子网的主机了。由于有了**多播(multicast)**的技术,广播被认为是过时了,经过多播,现代操作系统能够更好的利用网络以及网络接口设备提供的许多只能信息,另外多播支持费本地子网上的主机。不过想用一种简单的方法在本地 LAN 上完成一些偶尔允许丢包的功能(如游戏客户端货值自动实时记分牌)的话,UDP 广播是个简单易行的选择。