標籤:格式 模式 tcp ocs The 指令碼 資源 單點 hello
i春秋作家:wasrehpic0x00 前言
在上一篇文章「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 的異同,筆者將其整理成以下表格:
|
TCP |
UDP |
串連模式 |
連線導向(單點通訊) |
無串連(多點通訊) |
傳輸可靠性 |
可靠 |
不可靠 |
通訊模式 |
基於位元組流 |
基於資料報 |
前序結構 |
複雜(至少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 sockets = 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 sockets = 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) breaks.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.py
與 udp_client.py
,然後存至案頭,筆者將在 Windows 10 系統下用 PowerShell 進行示範。
小貼士:讀者進行複現時,要確保本機已安裝 Python 3,注意筆者已將預設的啟動路徑名 python
改為了 python3
。
單伺服器 VS 多用戶端
- 在其中一個 PowerShell 中運行命令
python3 ./udp_server.py
,伺服器綁定本地主機的 UDP 6000 連接埠,並列印資訊 UDP bound on port 6000...
,等待用戶端發來資料;
- 在另兩個 PowerShell 中分別運行命令
python3 ./udp_client.py
,並向伺服器發送字串 Client1
、Client2
;
- 伺服器列印接收資訊,表示分別從 UDP 63643、63644連接埠接收到資料,並分別向用戶端發送問候響應資訊;
- 用戶端
Client1
發送Null 字元串,則被要求重新輸入;
- 用戶端
Client2
先發送字串 Alice
,得到伺服器的問候響應資訊,再發送字串 exit
,得到伺服器的結束響應資訊,最後列印會話結束資訊,終止與伺服器的資料互動;
- 用戶端
Client1
發送字串 exit
,得到伺服器的結束響應資訊,並列印會話結束資訊,終止與伺服器的資料互動;
- 伺服器按照以上用戶端的資料發送順序列印接收資訊,並繼續等待任意 UDP 用戶端發來資料。
0x06 Python API Referencesocket 模組
本節介紹上述代碼中用到的內建模組 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_RAW 或 CAN_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 伺服器與用戶端