37 - 網絡編程-UDP編程

1 UDP協議

UDP是面向無鏈接的協議,使用UDP協議時,不須要創建鏈接,只須要知道對方的IP地址和端口號,就能夠直接發數據包。可是,能不能到達就不知道了。雖然用UDP傳輸數據不可靠,但它的優勢是和TCP比,速度快,對於不要求可靠到達的數據,就可使用UDP協議。編程

2 UDP通訊流程

咱們先來了解一下,python的socket的通信流程:
tcp_socket服務器

服務端:網絡

  1. 建立Socket對象
  2. 綁定IP地址Address和端口Port,使用bind方法,IPv4地址爲一個二元組('IP',Port),一個UDP端口只能被綁定一次
  3. 接受數據,recvfrom方法,使用緩衝區接受數據
  4. 發送數據,sendto方法,類型爲bytes
  5. 關閉鏈接

客戶端:socket

  1. 建立Socket對象
  2. 鏈接服務端。connect方法(可選)
  3. 發送數據,sendto/send方法,類型爲bytes
  4. 接受數據,recvfrom/recv方法,使用緩衝區接受數據
  5. 關閉鏈接

咱們能夠看到UDP不須要維護一個鏈接,因此比較簡單tcp

3 UDP編程

        使用udp編程和使用tcp編程用於類似的步驟,而由於udp的特性,它的服務端不須要監聽端口,而且客戶端也不須要事先鏈接服務端。根據上圖,以及創建服務端的流程,我門來捋一下服務端的邏輯到代碼的步驟:函數

3.1 構建服務端

  1. 建立服務端
socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# socke.AF_INET 指的是使用 IPv4
# socket.SOCK_STREAM 指定使用面向數據報的UDP協議
  1. 綁定IP地址和端口。
socket.bind(('127.0.0.1',999))  
# 小於1024的端口只有管理員才能夠指定
  1. 接受數據(阻塞)
data, client_info = sock.recvfrom(1024) 
# 返回一個元組,數據和客戶端的地址,由於UDP沒有鏈接,因此只能經過提取消息的發送的源地址,才能在應答時指定對方地址
  1. 發送數據
sock.sendto('data'.encode(),('127.0.0.1',999)) # bytes格式
# 第二個參數爲客戶端地址
  1. 關閉鏈接
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?大數據

  1. UDP無鏈接的特性,當服務端收到一條消息時,不會爲它維護一個socket的,那麼如何應答呢?
  2. UDP報文中包含對方的IP和Port信息,使用recvfrom,就會返回對方發送的數據和對方的地址
  3. sendto因爲沒有socket的特性,因此應答時也須要傳遞client的地址和端口ui

    3.2 構建客戶端

  4. 建立客戶端
socket = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# socke.AF_INET 指的是使用 IPv4
# socket.SOCK_STREAM 指定使用面向數據報的UDP協議
  1. 添加服務端地址信息(可選)。
socket.connect(('127.0.0.1',999))  
# UDP不會建立鏈接,因此這裏僅僅是在socket上添加了本段/對端的地址而已,並不會發起鏈接
  1. 接受數據(阻塞)
data, client_info = sock.recv(1024) 
# 返回一個元組,數據和客戶端的地址,由於UDP沒有鏈接,因此只能經過提取消息的發送的源地址,才能在應答時指定對方地址
  1. 發送數據
sock.sendto('data'.encode(),('127.0.0.1',999)) # bytes格式
# 第二個參數爲客戶端地址
  1. 關閉鏈接
sock.close()

爲何connect是可選的?

  1. 當執行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)>
  1. 若是不執行connect,那麼在使用send發生時,就沒法知道對端的IP地址,那麼只能使用sendto來指定了。
  2. 爲何接收時使用recv,由於是client,只會有server應答消息,因此就不須要來區分了。
  3. 若是指定了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是無鏈接協議,它基於如下假設:

  • 網絡足夠好
  • 消息不會丟包
  • 包不會亂序

可是,即便在局域網,也不能保證不丟包,並且包的到達不必定有序。

應用場景:

  1. 視頻音頻傳輸,通常來講,丟些包,問題不大,最多丟些圖像,聽不清話語。
  2. 海量採集數據,例如傳感器發來的數據,丟幾10、幾百條數據也沒有關係。
  3. DNS協議,數據內容小,一個包就能查到結果,不存在亂序,丟包時從新請求解析便可。

通常來講,UDP性能優於TCP,可是可靠性要求高的場合仍是要選擇TCP協議。

相關文章
相關標籤/搜索