1. 程式人生 > >Python 絕技 —— UDP 服務器與客戶端

Python 絕技 —— UDP 服務器與客戶端

建模 又一 https lib python3 -a 數據 Coding doc

i春秋作家:wasrehpic

0x00 前言

在上一篇文章「Python 絕技 —— TCP 服務器與客戶端」中,介紹了傳輸層的核心協議 TCP ,並運用 Python 腳本的 socket 模塊演示了 TCP 服務器與客戶端的通信過程。

本篇將按照同樣的套路,先介紹傳輸層的另一個核心協議 UDP,再比較 TCP 與 UDP 的特點,最後借助 Python 腳本演示 UDP 服務器與客戶端的通信過程。

0x01 UDP 協議

UDP(User Datagram Protocol,用戶數據報協議)是一種無連接、不可靠、基於數據報的傳輸層通信協議。

  • UDP 的通信過程與 TCP 相比較為簡單,不需要復雜的三次握手與四次揮手,體現了無連接;
  • UDP 傳輸速度比 TCP 快,但容易丟包、數據到達順序無保證、缺乏擁塞控制、秉承盡最大努力交付的原則,體現了不可靠;
  • UDP 的無連接與不可靠特性註定無法采用字節流的通信模式,由協議名中的「Datagram」與 socket 類型中的「SOCK_DGRAM」即可體現它基於數據報的通信模式。

為了更直觀地比較 TCP 與 UDP 的異同,筆者將其整理成以下表格:

TCPUDP
連接模式 面向連接(單點通信) 無連接(多點通信)
傳輸可靠性 可靠 不可靠
通信模式 基於字節流 基於數據報
報頭結構 復雜(至少20字節) 簡單(8字節)
傳輸速度
資源需求
到達順序 保證 不保證
流量控制
擁塞控制
應用場合 大量數據傳輸 少量數據傳輸
支持的應用層協議 Telnet、FTP、SMTP、HTTP DNS、DHCP、TFTP、SNMP

0x02 Network Socket

Network Socket(網絡套接字)是計算機網絡中進程間通信的數據流端點,廣義上也代表操作系統提供的一種進程間通信機制。

進程間通信(Inter-Process Communication,IPC)的根本前提是能夠唯一標示每個進程。在本地主機的進程間通信中,可以用 PID(進程 ID)唯一標示每個進程,但 PID 只在本地唯一,在網絡中不同主機的 PID 則可能發生沖突,因此采用「IP 地址 + 傳輸層協議 + 端口號」的方式唯一標示網絡中的一個進程。

小貼士:網絡層的 IP 地址可以唯一標示主機,傳輸層的 TCP/UDP 協議和端口號可以唯一標示該主機的一個進程。註意,同一主機中 TCP 協議與 UDP 協議的可以使用相同的端口號。

所有支持網絡通信的編程語言都各自提供了一套 socket API,下面以 Python 3 為例,講解服務器與客戶端建立 UDP 通信連接的交互過程:

技術分享圖片

可見,UDP 的通信過程比 TCP 簡單許多,服務器少了監聽與接受連接的過程,而客戶端也少了請求連接的過程。客戶端只需要知道服務器的地址,直接向其發送數據即可,而服務器也敞開大門,接收任何發往自家地址的數據。

小貼士:由於 UDP 采用無連接模式,可知 UDP 服務器在接收到客戶端發來的數據之前,是不知道客戶端的地址的,因此必須是客戶端先發送數據,服務器後響應數據。而 TCP 則不同,TCP 服務器接受了客戶端的連接後,既可以先向客戶端發送數據,也可以等待客戶端發送數據後再響應。

0x03 UDP 服務器

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 6000))
print("UDP bound on port 6000...")

while True:
    data, addr = s.recvfrom(1024)
    print("Receive from %s:%s" % addr)
    if data == b"exit":
        s.sendto(b"Good bye!\n", addr)
        continue
    s.sendto(b"Hello %s!\n" % data, addr)
  • Line 5:創建 socket 對象,第一個參數為 socket.AF_INET,代表采用 IPv4 協議用於網絡通信,第二個參數為 socket.SOCK_DGRAM,代表采用 UDP 協議用於無連接的網絡通信。
  • Line 6:向 socket 對象綁定服務器主機地址 ("127.0.0.1", 6000),即本地主機的 UDP 6000 端口。
  • Line 9:進入與客戶端交互數據的循環階段。
  • Line 10:接收客戶端發來的數據,包括 bytes 對象 data,以及客戶端的 IP 地址和端口號 addr,其中 addr 為二元組 (host, port)。
  • Line 11:打印接收信息,表示從地址為 addr 的客戶端接收到數據。
  • Line 12:若 bytes 對象為 b"exit",則向地址為 addr 的客戶端發送結束響應信息 b"Good bye!\n"。發送完畢後,繼續等待其他 UDP 客戶端發來數據。
  • Line 15:若 bytes 對象不為 b"exit",則向地址為 addr 的客戶端發送問候響應信息 b"Hello %s!\n",其中 %s是客戶端發來的 bytes 對象。發送完畢後,繼續等待任意 UDP 客戶端發來數據。

與 TCP 服務器相比,UDP 服務器不必使用多線程,因為它無需為每個通信過程創建獨立連接,而是采用「即收即發」的模式,又一次體現了 UDP 的無連接特性。

0x04 UDP 客戶端

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
addr = ("127.0.0.1", 6000)

while True:
    data = input("Please input your name: ")
    if not data:
        continue
    s.sendto(data.encode(), addr)
    response, addr = s.recvfrom(1024)
    print(response.decode())
    if data == "exit":
        print("Session is over from the server %s:%s\n" % addr)
        break

s.close()
  • Line 5:創建 socket 對象,第一個參數為 socket.AF_INET,代表采用 IPv4 協議用於網絡通信,第二個參數為 socket.SOCK_DGRAM,代表采用 UDP 協議用於無連接的網絡通信。
  • Line 6:初始化 UDP 服務器的地址 ("127.0.0.1", 6000),即本地主機的 UDP 6000 端口。
  • Line 8:進入與服務器交互數據的循環階段。
  • Line 9:要求用戶輸入名字。
  • Line 10:當用戶的輸入為空時,則重新開始循環,要求用戶重新輸入。
  • Line 12:當用戶的輸入非空時,則將字符串轉換為 bytes 對象後,發送至地址為 ("127.0.0.1", 6000) 的 UDP 服務器。
  • Line 13:接收服務器的響應數據,包括 bytes 對象 response,以及服務器的 IP 地址和端口號 addr,其中 addr 為二元組 (host, port)。
  • Line 14:將響應的 bytes 對象 response 轉換為字符串後打印輸出。
  • Line 15:當用戶的輸入為 "exit" 時,則打印會話結束信息,終止與服務器交互數據的循環階段,即將關閉套接字。
  • Line 19:關閉套接字,不再向服務器發送數據。

0x05 UDP 進程間通信

將 UDP 服務器與客戶端的腳本分別命名為 udp_server.pyudp_client.py,然後存至桌面,筆者將在 Windows 10 系統下用 PowerShell 進行演示。

小貼士:讀者進行復現時,要確保本機已安裝 Python 3,註意筆者已將默認的啟動路徑名 python 改為了 python3

單服務器 VS 多客戶端

技術分享圖片

  1. 在其中一個 PowerShell 中運行命令 python3 ./udp_server.py,服務器綁定本地主機的 UDP 6000 端口,並打印信息 UDP bound on port 6000...,等待客戶端發來數據;
  2. 在另兩個 PowerShell 中分別運行命令 python3 ./udp_client.py,並向服務器發送字符串 Client1Client2
  3. 服務器打印接收信息,表示分別從 UDP 63643、63644端口接收到數據,並分別向客戶端發送問候響應信息;
  4. 客戶端 Client1 發送空字符串,則被要求重新輸入;
  5. 客戶端 Client2 先發送字符串 Alice,得到服務器的問候響應信息,再發送字符串 exit,得到服務器的結束響應信息,最後打印會話結束信息,終止與服務器的數據交互;
  6. 客戶端 Client1 發送字符串 exit,得到服務器的結束響應信息,並打印會話結束信息,終止與服務器的數據交互;
  7. 服務器按照以上客戶端的數據發送順序打印接收信息,並繼續等待任意 UDP 客戶端發來數據。

0x06 Python API Reference

socket 模塊

本節介紹上述代碼中用到的內建模塊 socket,是 Python 網絡編程的核心模塊。

socket() 函數

socket() 函數用於創建網絡通信中的套接字對象。函數原型如下:

socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
  • family 參數代表地址族(Address Family),默認值為 AF_INET,用於 IPv4 網絡通信,常用的還有 AF_INET6,用於 IPv6 網絡通信。family 參數的可選值取決於本機操作系統。
  • type 參數代表套接字的類型,默認值為 SOCK_STREAM,用於 TCP 協議(面向連接)的網絡通信,常用的還有 SOCK_DGRAM,用於 UDP 協議(無連接)的網絡通信。
  • proto 參數代表套接字的協議,默認值為 0,一般忽略該參數,除非 family 參數為 AF_CAN,則 proto 參數需設置為 CAN_RAWCAN_BCM
  • fileno 參數代表套接字的文件描述符,默認值為 None,若設置了該參數,則其他三個參數將會被忽略。

創建完套接字對象後,需使用對象的內置函數完成網絡通信過程。註意,以下函數原型中的「socket」是指 socket 對象,而不是上述的 socket 模塊。

bind() 函數

bind() 函數用於向套接字對象綁定 IP 地址與端口號。註意,套接字對象必須未被綁定,並且端口號未被占用,否則會報錯。函數原型如下:

socket.bind(address)
  • address 參數代表套接字要綁定的地址,其格式取決於套接字的 family 參數。若 family 參數為 AF_INET,則 address 參數表示為二元組 (host, port),其中 host 是用字符串表示的主機地址,port 是用整型表示的端口號。

sendto() 函數

sendto() 函數用於向遠程套接字對象發送數據。註意,該函數用於 UDP 進程間的無連接通信,遠程套接字的地址在參數中指定,因此使用前不需要先與遠程套接字連接。相對地,TCP 進程間面向連接的通信過程需要用 send() 函數。函數原型如下:

socket.sendto(bytes[, flags], address)
  • bytes 參數代表即將發送的 bytes 對象數據。例如,對於字符串 "hello world!" 而言,需要用 encode() 函數轉換為 bytes 對象 b"hello world!" 才能進行網絡傳輸。
  • flags 可選參數用於設置 sendto() 函數的特殊功能,默認值為 0,也可由一個或多個預定義值組成,用位或操作符 | 隔開。詳情可參考 Unix 函數手冊中的 sendto(2),flags 參數的常見取值有 MSG_OOB、MSG_EOR、MSG_DONTROUTE 等。
  • address 參數代表遠程套接字的地址,其格式取決於套接字的 family 參數。若 family 參數為 AF_INET,則 address 參數表示為二元組 (host, port),其中 host 是用字符串表示的主機地址,port 是用整型表示的端口號。

sendto() 函數的返回值是發送數據的字節數。

recvfrom() 函數

recvfrom() 函數用於從遠程套接字對象接收數據。註意,與 sendto() 函數不同,recvfrom() 函數既可用於 UDP 進程間通信,也能用於 TCP 進程間通信。函數原型如下:

socket.recvfrom(bufsize[, flags])
  • bufsize 參數代表套接字可接收數據的最大字節數。註意,為了使硬件設備與網絡傳輸更好地匹配,bufsize 參數的值最好設置為 2 的冪次方,例如 4096
  • flags 可選參數用於設置 recv() 函數的特殊功能,默認值為 0,也可由一個或多個預定義值組成,用位或操作符 |隔開。詳情可參考 Unix 函數手冊中的 recvfrom(2),flags 參數的常見取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。

recvfrom() 函數的返回值是二元組 (bytes, address),其中 bytes 是接收到的 bytes 對象數據,address 是發送方的 IP 地址與端口號,用二元組 (host, port) 表示。註意,recv() 函數的返回值只有 bytes 對象數據。

close() 函數

close() 函數用於關閉本地套接字對象,釋放與該套接字連接的所有資源。

socket.close()

0x07 總結

本文介紹了 UDP 協議的基礎知識,並與 TCP 協議進行對比,再用 Python 3 實現並演示了 UDP 服務器與客戶端的通信過程,最後將腳本中涉及到的 Python API 做成了的參考索引,有助於讀者理解實現過程。

感謝各位的閱讀,筆者水平有限,若有不足或錯誤之處請諒解並告知,希望自己對 TCP 和 UDP 的淺薄理解,能幫助讀者更好地理解傳輸層協議。

本文的相關參考請移步至:

TCP和UDP的最完整的區別
TCP和UDP之間的區別
UDP編程 - 廖雪峰的官方網站

Python 絕技 —— UDP 服務器與客戶端