37 - 网络编程-UDP编程
1 UDP协议
UDP是面向无连接的协议,使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包
。但是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
2 UDP通信流程
我们先来了解一下,python的socket的通讯流程:
服务端:
- 创建Socket对象
- 绑定IP地址Address和端口Port,使用bind方法,IPv4地址为一个二元组('IP',Port),
一个UDP端口只能被绑定一次
- 接受数据,recvfrom方法,使用缓冲区接受数据
- 发送数据,sendto方法,类型为bytes
- 关闭连接
客户端:
- 创建Socket对象
- 连接服务端。connect方法(可选)
- 发送数据,sendto/send方法,类型为bytes
- 接受数据,recvfrom/recv方法,使用缓冲区接受数据
- 关闭连接
我们可以看到UDP不需要维护一个连接,所以比较简单
3 UDP编程
使用udp编程和使用tcp编程用于相似的步骤,而因为udp的特性,它的服务端不需要监听端口,并且客户端也不需要事先连接服务端。根据上图,以及建立服务端的流程,我门来捋一下服务端的逻辑到代码的步骤:
3.1 构建服务端
- 创建服务端
socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# socke.AF_INET 指的是使用 IPv4
# socket.SOCK_STREAM 指定使用面向数据报的UDP协议
- 绑定IP地址和端口。
socket.bind(('127.0.0.1',999))
# 小于1024的端口只有管理员才可以指定
- 接受数据(阻塞)
data, client_info = sock.recvfrom(1024)
# 返回一个元组,数据和客户端的地址,因为UDP没有连接,所以只能通过提取消息的发送的源地址,才能在应答时指定对方地址
- 发送数据
sock.sendto('data'.encode(),('127.0.0.1',999)) # bytes格式
# 第二个参数为客户端地址
- 关闭连接
sock.close()
完成的代码:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 指定socket的协议,UDP使用的是SOCK_DGRAM
server.bind(('127.0.0.1', 9999)) # 绑定端口
print('UDP Server is Starting...')
data, addr = server.recvfrom(1024) # 接受(包含数据以及客户端的地址)
print('Received from {}'.format(addr))
server.sendto('hello,{}'.format(addr).encode('utf-8'), addr) # 应答,格式为(应答的数据,客户端的IP和Port元组)
为什么要使用recvfrom/sendto?
- UDP无连接的特性,当服务端收到一条消息时,不会为它维护一个socket的,那么如何应答呢?
- UDP报文中包含对方的IP和Port信息,使用recvfrom,就会返回对方发送的数据和对方的地址
- sendto由于没有socket的特性,所以应答时也需要传递client的地址和端口
3.2 构建客户端
- 创建客户端
socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# socke.AF_INET 指的是使用 IPv4
# socket.SOCK_STREAM 指定使用面向数据报的UDP协议
- 添加服务端地址信息(可选)。
socket.connect(('127.0.0.1',999))
# UDP不会创建连接,所以这里仅仅是在socket上添加了本段/对端的地址而已,并不会发起连接
- 接受数据(阻塞)
data, client_info = sock.recv(1024)
# 返回一个元组,数据和客户端的地址,因为UDP没有连接,所以只能通过提取消息的发送的源地址,才能在应答时指定对方地址
- 发送数据
sock.sendto('data'.encode(),('127.0.0.1',999)) # bytes格式
# 第二个参数为客户端地址
- 关闭连接
sock.close()
为什么connect是可选的?
- 当执行connect时,由于UDP的特性,并不会为我们创建连接,这里仅仅是在socket上添加了对端的地址而已,并不会发起连接
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print(client) # <socket.socket fd=140, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0>
client.connect(('127.0.0.1', 9999))
print(client) # <socket.socket fd=140, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 51859), raddr=('127.0.0.1', 9999)>
- 如果不执行connect,那么在使用send发生时,就无法知道对端的IP地址,那么只能使用sendto来指定了。
- 为什么接收时使用recv,因为是client,只会有server应答消息,所以就不需要来区分了。
- 如果指定了connect,sendto已久可以发给任意终端,但recv只能接受connect指定的对端,发来的消息。
完整的代码:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 指定socket的协议,UDP使用的是SOCK_DGRAM
client.sendto('hello world'.encode('utf-8'), ('127.0.0.1', 9999)) # 发送数据,格式为(发送的数据,服务端的IP和Port元组)
print(client.recv(1024).decode('utf-8')) # 同样使用recv来接受服务端的应答数据
UDP的使用与TCP类似,但是不需要建立连接。此外,服务器绑定UDP端口和TCP端口互不冲突,也就是说,UDP的9999端口与TCP的9999端口可以各自绑定。
3.3 常用方法
服务器端套接字:
函数 | 描述 |
---|---|
s.bind() |
绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。 |
客户端套接字:
函数 | 描述 |
---|---|
s.connect() |
初始化UDP连接对象的,本段/对端地址。 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数:
函数 | 描述 |
---|---|
s.recv() |
接收TCP/UDP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。 |
s.send() |
发送TCP/UDP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。 |
s.recvfrom() |
接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 |
s.sendto() |
发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。 |
s.close() |
关闭套接字 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) |
如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。 |
s.makefile() |
创建一个与该套接字相关连的文件 |
4 聊天室
下面来模仿上一篇TCP版本的聊天室的结构来创建一个UDP版本的聊天室
服务端:
import socket
import threading
import datetime
import logging
FORMAT = '%(asctime)s %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
class ChatUDPServer:
def __init__(self, ip, port):
self.ip = ip
self.port = port
self.event = threading.Event()
self.clients = {}
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def start(self):
self.sock.bind((self.ip, self.port))
threading.Thread(target=self.recv, name='start').start()
def recv(self):
while not self.event.is_set():
# 待清理的列表
clean = set()
# 远程主机关闭连接时,这里会触发异常。不知道为啥
try:
data, client_addr = self.sock.recvfrom(1024)
except ConnectionResetError:
continue
if data.upper() == 'quit' or data == b'':
self.clients.pop(client_addr)
logging.info(client_addr, 'is down')
continue
# 心跳包,内容越小越好
if data.lower() == b'@im@':
self.clients[client_addr] = datetime.datetime.now().timestamp()
continue
logging.info('{}:{} {}'.format(*client_addr, data.decode()))
self.clients[client_addr] = datetime.datetime.now().timestamp()
msg = "{}:{} {}".format(*client_addr, data.decode()).encode()
current = datetime.datetime.now().timestamp()
for client, date in self.clients.items():
# 如果10s内没有发送心跳包,则进行清理
if current - date > 10:
clean.add(client)
else:
self.sock.sendto(msg, client)
# 清理超时连接
for client in clean:
self.clients.pop(client)
def stop(self):
self.event.set()
self.sock.close()
if __name__ == '__main__':
cus = ChatUDPServer('127.0.0.1', 9999)
cus.start()
while True:
cmd = input('>>>>: ').strip()
if cmd.lower() == 'quit':
cus.stop()
break
else:
print(threading.enumerate())
客户端:
import socket
import threading
import logging
FORMAT = '%(asctime)s %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
class ChatUDPClient:
"""
self.ip: 服务端地址
self.port:服务端端口
self.socket:创建一个socket对象,用于socket通信
self.event:创建一个事件对象,用于控制链接循环
"""
def __init__(self, ip, port):
self.ip = ip
self.port = port
self.socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
self.event = threading.Event()
def connect(self):
self.socket.connect((self.ip, self.port))
threading.Thread(target=self.recv, name='recv',daemon=True).start()
threading.Thread(target=self._heart,name='heart',daemon=True).start()
def _heart(self):
while not self.event.wait(5):
data = '@im@'
self.send(data)
def recv(self):
while not self.event.is_set():
# 某些服务端强制关闭时,会出b'',这里进行判断
try:
data = self.socket.recv(1024)
if data == b'':
self.event.set()
logging.info('{}:{} is down'.format(self.ip, self.port))
break
logging.info(data.decode())
# 有些服务端在关闭时不会触发b'',这里会直接提示异常,这里进行捕捉
except (ConnectionResetError,OSError):
self.event.set()
logging.info('{}:{} is down'.format(self.ip, self.port))
def send(self, msg):
self.socket.send(msg.encode())
def stop(self):
self.send('quit')
self.socket.close()
if __name__ == '__main__':
ctc = ChatUDPClient('127.0.0.1', 9999)
ctc.connect()
while True:
info = input('>>>>:').strip()
if not info: continue
if info.lower() == 'quit':
logging.info('bye bye')
ctc.stop()
break
if not ctc.event.is_set():
ctc.send(info)
else:
logging.info('Server is down...')
break
5 UDP协议应用
UDP是无连接协议,它基于以下假设:
- 网络足够好
- 消息不会丢包
- 包不会乱序
但是,即使在局域网,也不能保证不丢包,而且包的到达不一定有序。
应用场景:
- 视频音频传输,一般来说,丢些包,问题不大,最多丢些图像,听不清话语。
- 海量采集数据,例如传感器发来的数据,丢几十、几百条数据也没有关系。
- DNS协议,数据内容小,一个包就能查到结果,不存在乱序,丢包时重新请求解析即可。
一般来说,UDP性能优于TCP,但是可靠性要求高的场合还是要选择TCP协议。
37 - 网络编程-UDP编程的更多相关文章
- Socket网络编程-UDP编程
Socket网络编程-UDP编程 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.UDP编程概述 1>.UDP服务端编程流程 创建socket对象.socket.SOCK_ ...
- 五十六、linux 编程——UDP 编程模型
56.1 UDP 编程模型 56.1.1 编程模型 UDP 协议称为用户数据报文协议,可靠性比 TCP 低,但执行效率高 56.1.2 API (1)发送数据 函数参数: sockfs:套接字文件描述 ...
- UNIX网络编程——UDP编程模型
使用UDP编写的一些常见得应用程序有:DNS(域名系统),NFS(网络文件系统)和SNMP(简单网络管理协议). 客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目 ...
- 网络编程——UDP编程
一个简单的聊天代码:运行结果: 在这个程序之中,由于recvfrom函数拥塞函数,没有数据时会一直阻塞,所以客户端和服务器端只能通过一回一答的方式进行信息传递.严格的讲UDP没有明确的客户端和服务端, ...
- 五十五 网络编程 UDP编程
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据.相对TCP,UDP则是面向无连接的协议. 使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发数据包.但是,能不 ...
- 五十八、linux 编程——UDP 编程 广播
58.1 广播介绍 58.1.1 介绍 广播实现一对多的通讯 它通过向广播地址发送数据报文实现的 58.1.2 套接字选项 套接字选项用于修饰套接字以及其底层通讯协议的各种行为.函数 setsocko ...
- 五十七、linux 编程——UDP 编程 域名解析
57.1 介绍 57.1.1 域名解析 57.1.2 域名解析函数 gethostent 可以获取多组,gethostbyname 只可以获取一组 /etc/hosts 文件设置了域名和 IP 的绑定 ...
- [C# 网络编程系列]专题六:UDP编程
转自:http://www.cnblogs.com/zhili/archive/2012/09/01/2659167.html 引用: 前一个专题简单介绍了TCP编程的一些知识,UDP与TCP地位相当 ...
- 网络编程之UDP编程
网络编程之UDP编程 UDP协议是一种不可靠的网络协议,它在通信的2端各建立一个Socket,但是这个Socket之间并没有虚拟链路,这2个Socket只是发送和接受数据的对象,Java提供了Data ...
随机推荐
- Android手机Fiddler真机抓包
Fiddler是最强大最好用的Web调试工具之一,它能记录所有客户端和服务器的http和https请求,允许用户监视,设置断点,甚至修改输入输出数据,Fiddler包含了一个强大的基于事件脚本的子系统 ...
- 第194天:js---函数对象详解(arguments、length)
一.函数即对象 function add1(a,b){ return a+b; } //Function对象的实例 -- 高级技巧 --- 写框架必须用的... //前面表示参数,后面表示函数语句 v ...
- HDU2486_A simple stone game
这个题目是这样的,一堆石子有n个,首先第一个人开始可以去1-(n-1)个,接下来两人轮流取石子,每个人可取的石子数必须是一个不超过上一次被取的石子的K倍的整数. 现在求对于一堆数量为n的石子是否为必胜 ...
- zookeeper应用实例
创建持久节点和临时节点 ZooKeeper zk=new ZooKeeper(HOST,CLIENT_SESSION_TIMEOUT,new Watcher(){ @Override public v ...
- BZOJ 1444 有趣的游戏(AC自动机+矩阵快速幂)
真的是很有趣的游戏... 对每个单词构建好AC自动机后,由于单词都是相同长度的且不同,所以不会出现互相为子串的形式. 那么我们对AC自动机上的节点构建转移矩阵.对于每个单词末尾的节点.该节点的出边仅仅 ...
- 洛谷P2894[USACO08FEB]酒店Hotel(线段树)
问题描述 奶牛们最近的旅游计划,是到苏必利尔湖畔,享受那里的湖光山色,以及明媚的阳光.作为整个旅游的策划者和负责人,贝茜选择在湖边的一家著名的旅馆住宿.这个巨大的旅馆一共有N (1 <= N & ...
- [codeforces464D]World of Darkraft - 2 概率期望
D. World of Darkraft - 2 time limit per test 2 seconds memory limit per test 256 megabytes input sta ...
- 【转】c# thread.join 理解
转自:http://blog.csdn.net/lulu_jiang/article/details/6584251 线程Join()方法:让一个线程等待另一线程终结后再继续运行. private s ...
- 【刷题】BZOJ 1468 Tree
Description 给你一棵TREE,以及这棵树上边的距离.问有多少对点它们两者间的距离小于等于K Input N(n<=40000) 接下来n-1行边描述管道,按照题目中写的输入 接下来是 ...
- Kerberos的黄金票据详解
0x01黄金票据的原理和条件 黄金票据是伪造票据授予票据(TGT),也被称为认证票据.如下图所示,与域控制器没有AS-REQ或AS-REP(步骤1和2)通信.由于黄金票据是伪造的TGT,它作为TGS- ...