网络编程之UDP编程

知识共享许可协议 版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons

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编程常用方法

对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())

心跳机制:

  1. 一般来说是客户端定时发往服务端的,服务端并不需要ack回复客户端,只要记录该客户端还活着就可以了
  2. 如果是服务端定时发往客户端的,一般需要客户端ack响应来表示活着,如果没有收到ack的客户端,服务端移除其信息。这种实现较为复杂,用的较少。
  3. 也可以双向都发心跳的,用的更少

1.4 UDP协议应用

UDP协议是无连接协议,它基于以下假设:(User Datagram Protocol)

  • 网路足够好
  • 消息不会丢包
  • 包不会乱序

但是,即使是局域网,也不能保证不丢包,而且包的到达不一定有序。

应用场景:视频、音频传输,一般来说,丢些包,问题不大,最多丢些图像,听不清话语,可以重新发话语来 解决。海量采集数据,例如传感器发来的数据,丢几十、几百条数据也没有关系。DNS协议,数据内容小,一个包就能查询到结果,不存在乱序,丢包,重新请求解析。一般来说,UDP性能优于TCP,但是可靠性要求高的场合还是要选择TCP协议。

猜你喜欢

转载自blog.csdn.net/sqsltr/article/details/92553138