1. UDP编程
1.1 UDP编程流程
1.1.1 UDP服务端编程流程
- 创建socket对象,socket.SOCK_DGRAM
- 绑定IP和Port,bind()方法
- 传输数据:接收数据,socket.recvfrom(bufsize[, flags]),获得一个二元组(string, address);发送数据,socket.sendto(string, address),发送给某地址某信息
- 释放资源
import socket
import threading
event = threading.Event()
address = '0.0.0.0', 9999
server = socket.socket(type=socket.SOCK_DGRAM) # 数据报协议
server.bind(address) # 只能绑定一次
while not event.is_set():
data, client_info = server.recvfrom(1024) # 比recv安全,可以知道是谁发给你的
print(data)
# print(server.getpeername()) # 会报错OSError
msg = "{} from {}-{}".format(data.decode(), *client_info).encode()
# server.send(msg) # 会报错,不知道发送给谁
server.sendto(msg, client_info)
print('~' * 30)
event.set()
server.close()
1.1.2 UDP客户端编程流程
- 创建socket对象,socket.SOCK_DGRAM
- 发送数据,socket.sendto(string, address)发送某地址某信息
- 接收数据,socket.recvfrom(bufsize[, flags]),获得一个二元组(string, address)
- 释放资源
import socket
address = '127.0.0.1', 10001
client = socket.socket(type=socket.SOCK_DGRAM) # 数据报协议
client.connect(address) # 会解决本地address和远端地址address
print(1, client)
print(1.5, client)
ms = b'111222333444555'
# client.sendto(ms + b'~~~~~~~', ('127.0.0.1', 10000))
# 会帮你抢一个本地地址和端口(端口是临时的),没有此句,recvfrom会报错
# 可以自己玩,因为它有本地address, 它不会记录远端地址address
client.send(ms) # 必须和connect配合使用,什么都不解决
print(2, client)
data, info = client.recvfrom(1024) # 它需要本地address
print(data, info)
# client.connect(address) # 加了这一句,send就可以用了
# while True:
# cmd = input(">>>").strip()
# ms = cmd.encode()
# # client.sendto(ms, address)
# client.send(ms) # 此句必须和connect配合使用
# client.sendto(ms + b'~~~~~~~', ('127.0.0.1', 10000))
# client.sendto(ms + b'+++++++', ('127.0.0.1', 10001))
# data, info = client.recvfrom(1024) # 比recv安全,可以知道是谁发给你的
# print(data)
# msg = "{} from {}".format(data.decode(), *info).encode()
#
# print('~' * 30)
client.close()
注意:UDP是无连接协议,所有可以只有任何一端,例如客户端数据发往服务端,服务端存在与否无所谓
1.2 UDP编程中常用的方法
对udp编程常用方法的几点说明:send方法必须和connect方法配合使用,否则直接报错;recvfrom比recv要安全,recvfrom知道是谁发送信息给你的,它需要知道本地的address;sendto会抢一个本地的地址和端口(端口是临时的),它可以自己玩,因为它有本地的地址,它不会记录远端地址;connect会解决本地地址和远端地址。
1.3 UDP编程实现群聊
1.3.1 UDP版群聊服务端代码
import socket
import datetime
import logging
import threading
FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatServerUdp:
# UDP群聊用的都是同一个socket,所以用字典浪费了,所有的value值都是一样的,列表可以,但是移除的话,效率低,所以考虑用集合
# 但是添加了过期删除了client的话,集合就不合适了,此时还是要用字典
def __init__(self, ip='127.0.0.1', port=9999, interval=10): # 服务端的时间间隔一般是客户端的时间间隔的2到3倍
self.sock = socket.socket(type=socket.SOCK_DGRAM) # 数据报文协议
self.address = ip, port
self.event = threading.Event()
# self.clients = set()
self.clients = {}
self.interval = interval
def start(self):
self.sock.bind(self.address)
threading.Thread(target=self.rec, name='rec').start()
def rec(self):
while not self.event.is_set():
data, client_info = self.sock.recvfrom(1024)
current = datetime.datetime.now().timestamp() # float
# self.clients.add(client_info)
if data.strip() == b'^hb^': # b'^hb^'为自己设计的
self.clients[client_info] = current
logging.info('{} hb^^^^^'.format(client_info))
continue
if data.strip() == b'quit':
# self.clients.remove(client_info) # 注意remove相当于是按照key查找的,因为集合的值可以看做字典的key,所以比列表高效
self.clients.pop(client_info) # 客户端主动断开连接,就把该客户的ip从字典中删除
logging.info("{} leaving".format(client_info))
continue # 不能用break,因为总共只有一个线程,break了,while循环结束了
self.clients[client_info] = current
# 在该位子遍历字典,删除过期的clients,比较耗时,因为如果一个都没有删除,每次都要遍历字典,会很耗时,可以考虑在发送信息时,
# 遍历字典判断是否超时
logging.info(data)
msg = "{} [{}:{}] {}".format(datetime.datetime.now(), *client_info, data.decode())
keys = set()
for c, stamp in self.clients.items(): # 有线程安全问题,解决方法是加锁
if current - stamp < 0 or current - stamp > self.interval: # 小于0应该是时间出问题了
keys.add(c) # 不能直接self.clients.pop(c),因为字典在遍历的过程中,其长度不能改变
else:
self.sock.sendto(msg.encode(), c)
for c in keys:
self.clients.pop(c)
def stop(self):
self.event.set()
self.clients.clear()
self.sock.close()
csu = ChatServerUdp()
csu.start()
while True:
cmd = input('>>>').strip()
if cmd == 'quit':
csu.stop()
break
logging.info(threading.enumerate())
1.3.2 UDP版群聊客户端代码
import socket
import logging
import threading
FORMAT = "%(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
class ChatClientUdp:
def __init__(self, ip='127.0.0.1', port=9999, interval=5):
self.sock = socket.socket(type=socket.SOCK_DGRAM)
self.r_address = ip, port
self.event = threading.Event()
self.interval = interval
def start(self):
self.sock.connect(self.r_address)
self.send('{} hello'.format(self.sock.getsockname()))
threading.Thread(target=self.heart_beat, name='heartbeat', daemon=True).start()
threading.Thread(target=self.rec, name='rec').start()
def heart_beat(self):
while not self.event.wait(self.interval):
# self.sock.send(b'^hb^') # 发送心跳包,记录最后一次发送的时间,此句比较浪费时间,换成下面的语句
self.send('^hb^')
def rec(self):
while not self.event.is_set():
data = self.sock.recv(1024)
logging.info(data)
def send(self, msg: str):
self.sock.sendto(msg.encode(), self.r_address)
def stop(self):
self.event.set()
self.send('quit')
self.sock.close()
ccu = ChatClientUdp()
ccu.start()
while True:
line = input('>>>').strip()
if line == 'quit':
ccu.stop()
break
ccu.send(line)
logging.info(threading.enumerate())
心跳机制:
- 一般来说是客户端定时发往服务端的,服务端并不需要ack回复客户端,只要记录该客户端还活着就可以了
- 如果是服务端定时发往客户端的,一般需要客户端ack响应来表示活着,如果没有收到ack的客户端,服务端移除其信息。这种实现较为复杂,用的较少。
- 也可以双向都发心跳的,用的更少
1.4 UDP协议应用
UDP协议是无连接协议,它基于以下假设:(User Datagram Protocol)
- 网路足够好
- 消息不会丢包
- 包不会乱序
但是,即使是局域网,也不能保证不丢包,而且包的到达不一定有序。
应用场景:视频、音频传输,一般来说,丢些包,问题不大,最多丢些图像,听不清话语,可以重新发话语来 解决。海量采集数据,例如传感器发来的数据,丢几十、几百条数据也没有关系。DNS协议,数据内容小,一个包就能查询到结果,不存在乱序,丢包,重新请求解析。一般来说,UDP性能优于TCP,但是可靠性要求高的场合还是要选择TCP协议。