这是一个重在实践的计算机网络通信教程,旨在让读者快速写出相关功能的Python代码,因而没有详尽的概念与原理介绍。
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。在Python中,TCP和UDP的通信都是基于Socket的。
在Python中,可以通过import socket来使用Socket。
UDP(User Datagram Protocol)协议是一种无连接、不可靠、快速传输的协议。它不区分服务端和客户端(通信的两台机器是对等的),在发送数据前不会创建连接,也不会保证数据能够到达。当强调传输性能而不是传输的完整性时,如:音频和多媒体应用,UDP是最好的选择。
UDP数据包的格式如下:(图片来自百度百科)

由图可知,IPv4数据报的最大长度为$2^{16}-1=65535$字节,去除IP头部20字节,则IP数据报的数据部分长度最多65515字节。对于UDP数据报,其头部长度为8字节,因此一个数据报最多传输65507字节的数据。但在实际使用过程中,由于不同设备的最大传输单元(Maximum Transmission Unit, MTU)的限制,并不能一次传输这么多数据,超过MTU限制的数据会被分片传输。默认的以太网MTU是1500字节。为了对避免IPv4数据包进行分片,对于UDP包,我们需要限制每个包的大小,一般不要超过1472字节,即以太网MTU(1500)-UDP首部(8)-IP首部(20)。
在Python中,为了使用UDP协议通信,我们需要创建UDP类型的套接字:
import socket # 导入socket
sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 创建数据报型socket
绑定本机的IP地址和端口:
local_address=('',8088) # (IP地址,端口),IP地址留空表示本机地址
sock.bind(local_address) # 设置本地地址
这一步可以省略。在省略的情况下,会用随机端口发送数据。
remote_address=('127.0.0.1',8080) # 指定发送目标地址和端口
# 远程地址也可以是域名:remote_address=('www.example.com',8080)
sock.sendto("hello".encode(),remote_address) # UDP发送
方式一:阻塞地接收数据
阻塞意味着如果没有收到数据,程序会一直在这里等待而不会继续向下运行:
data,from_address=sock.recvfrom(1024) # 接受数据,缓冲区长度1024字节
print(from_address,data) # 打印远程地址和数据
方式二:阻塞地接收数据,但设置超时时间
这意味着如果没有收到数据,程序会一直在这里等待,直到等待超过指定的时间。如果在等待期内收到数据,则继续向下运行;如果在等待期内没有收到数据,则引发socket.timeout异常。
首先,在绑定地址和端口前,添加如下代码:
sock.settimeout(1) # 设置超时时间为1s,时间可以是小数
然后使用下列代码接收数据:
try:
data,from_address=sock.recvfrom(1024) # 接受数据,缓冲区长度1024字节
except socket.timeout: # 如果此时没有数据发来,会引发错误
data,from_address=b'','error'
print(from_address,data.decode()) # 打印远程地址和数据
方式三:非阻塞地接收数据
非阻塞意味着如果没有收到数据,程序不会在此等待,而是引发一个BlockingIOError的异常。
首先,在绑定地址和端口前,添加如下代码:
sock.setblocking(0)
然后使用下列代码非阻塞地接收数据:
try:
data,from_address=sock.recvfrom(1024) # 接受数据,缓冲区长度1024字节
except BlockingIOError: # 如果此时没有数据发来,会引发错误
data,from_address=b'','error'
print(from_address,data.decode()) # 打印远程地址和数据
数据发送和接受完毕后,需要关闭套接字:
sock.close()
以下代码完整的构建了一个UDP Socket的类,可以实现UDP收发数据的功能:
import socket
class udpsocket:
# 以本机IP和指定端口初始化,指定是否阻塞和超时时间(单位:秒,可以是小数)
def __init__(self,port:int,blocking:bool=True,timeout:float=-1):
self.sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
if blocking: # 阻塞时设置超时
if timeout>0: self.sock.settimeout(timeout)
else: # 不阻塞则超时无效
self.sock.setblocking(blocking)
self.sock.bind(('',port))
# 发送已经编码的数据
def send(self,encoded_data):
self.sock.send(encoded_data)
# 接收数据,需要自行解码
def recv(self,buf_size:int=1024):
try:
data,from_address=self.sock.recvfrom(buf_size)
except BlockingIOError: # 非阻塞未收到信号处理
data,from_address=None,None
except socket.timeout: # 超时处理
data,from_address=None,None
return data,from_address
# 关闭UDP套接字
def close(self):
self.sock.close()