前言
UDP协议是除TCP以外,最为常见的协议,一般用于DNS解析、P2P通信等,目前比较火的QUIC
协议也是基于UDP的。
UDP协议是一种无连接的、不可靠的协议,不需要维护状态,处理速度较快,但是需要使用者自己解决丢包、重传、排序等问题。
由于UDP协议在NAT打洞
上的成功率高于TCP协议,而且UDP协议在同等网络质量下可以获得更高的通信速率,因此绝大多数的P2P
程序都是使用了UDP协议。
使用python进行UDP通信和进行TCP通信差别不是太大。
创建UDP服务端
1. 1. 创建socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
这一步与在TCP中的唯一差别,在于将socket.SOCK_STREAM
改成了socket.SOCK_DGRAM
,表示使用UDP协议。
2. 2. 绑定监听端口
s.bind(('0.0.0.0', 8000))
这步操作与TCP通信是一致的,唯一的差别是UDP中不需要进行listen
操作就可以直接使用了。这是因为UDP没有连接,不像TCP需要进行三次握手。
创建UDP客户端
由于UDP不需要进行连接操作,因此只需要进行创建socket对象
后,就可以直接发送数据了。
客户端和服务端收发数据
1. 1. 接收数据
服务端由于进行了bind
操作,因此可以直接接收数据。
buff, addr = s.recvfrom(4096)
参数含义与TCP的recv
函数一致。返回值是(buff, addr)
二元组,buff
是接收到的数据,addr
是发送方的(ip, port)
二元组。
但是客户端如果直接调用recvfrom
会报错,因为没有绑定任何端口。例如Windows会报如下错误:
socket.error: [Errno 10022]
客户端只需要先向服务端发送一次数据,底层就会临时绑定一个端口,此时调用recvfrom
就正常了。
2. 2. 发送数据
s.sendto(buff, (ip, port))
由于没有进行连接,发送数据需要指定服务端的地址和端口。从这里可以看出,一个UDP的socket对象,是可以向多个服务端发送数据的;而在TCP中一旦建立连接,通信双方就已经确定了。
3. 3. 关闭socket对象
s.close()
这步是和TCP一致的,差别在于TCP会关闭连接,而UDP只是释放资源。
简单的例子
1. 服务端代码
def create_server(port):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', port))
while True:
buffer, addr = s.recvfrom(4096)
print('Recv %r from %s:%d' % (buffer, addr[0], addr[1]))
if buffer == b'exit':
break
s.sendto(buffer, addr)
s.close()
2. 客户端代码
def create_client(port):
addr = ('127.0.0.1', port)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b'Hello python!!!', addr)
buffer, _ = s.recvfrom(4096)
print('Recv %r from server' % buffer)
s.sendto(b'exit', addr)
s.close()
3. 结果显示如下
服务端:
Recv 'Hello python!!!' from 127.0.0.1:56618
Recv 'exit' from 127.0.0.1:56618
客户端:
Recv 'Hello python!!!' from server
使用有连接的方式通信
事实上,UDP也可以使用和TCP相同的方式进行通信。服务端代码一致,客户端代码修改为:
def create_client_with_connection(port):
addr = ('127.0.0.1', port)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(addr)
s.send(b'Hello python!!!')
buffer = s.recv(4096)
print('Recv %r from server' % buffer)
s.send(b'exit')
s.close()
效果完全一致。需要注意的是:这里的connect
并不是真的连接,只是底层做了套接字与端口的绑定。由于此时服务端并不知道客户端的地址,因此无法直接向客户端发送数据。而客户端在connect
之后直接调用recv
也会一直处于阻塞状态,因此还是需要先调用一次send
,才能真正建立与服务端的通信。
这种用法比较适合那些客户端只与固定服务端通信的场景,可以让代码更简洁。