Python網路編程中的UDP協議以及簡單會話模型。

一轉眼就到了一年結束的時候了。

所以今天在最後兩個小時里寫下這最後一篇文章。

祝大家元旦快樂!謝謝大家一直關注。

先簡單介紹一下UDP協議:

UDP 是User Datagram Protocol的簡稱, 中文名是用戶數據報協議,是OSI(Open System Interconnection,開放式系統互聯) 參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務。

在IP協議的上層。

IP協議只負責嘗試將每個數據包傳送至正確的機器。如果有兩個獨立的應用程序想維護一個會話,這時候還需要兩個其他的特性。

  1. 在兩台主機間傳送的大量數據包打上標籤。即所說的多路復用。
  2. 可以修復兩台獨立主機獨立傳輸的數據包流發生的任何錯誤。即所說的可靠傳輸。

而UDP只是實現了第一個特性。而TCP協議解決了這兩個問題。但是TCP協議會在下一篇文章裡面詳細講解。

一般來說UDP協議機制好似很簡單的。僅使用IP+埠號來進行標識,然後讓數據包發送到目標地址。

具體客戶端如何知道需要連接的埠號。一般來說有三個常規方法:

  1. 慣例:互聯網號碼分配機構(IANA)分配了官方知名埠,比如DNS默認是53埠。
  2. 自動配置:計算機首次連接網路是,會使用DHCP來獲取一些重要服務的地址。一般都是一些基礎服務。
  3. 手動配置:除了上述的兩種情況,管理員和用戶手動輸入。

IANA分配埠的時候分為了三大類,可以讓UDP和TCP使用:

  1. 知名埠(0-1023):分配給最重要的最常用的服務。一般來說程序無法監聽這些埠。
  2. 註冊埠(1024-49151):一般來說使用特定服務的時候才註冊。
  3. 其餘的埠號(49152-65535):可以隨意使用。

下面我們接著說套接字:

網路操作系統背後都是圍繞著socket來進行的。套接字是一個通信斷電,操作系統使用整數來標識套接字。

在Python中可以使用socket.socket來方便地表示套接字。這個對象內部維護了操作系統標識套接字的整數。當然你可以調用她的fileno()方法來查看。

下面我們來用Python實現一個簡單的UDP服務和UDP客戶端。

# 使用自環埠的UDP伺服器和客戶端nimport argparse, socketnfrom datetime import datetimennMAX_BYTES = 65535nndef server(port):n sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)n sock.bind((127.0.0.1, port))n print(Listening at {}.format(sock.getsockname()))n while True:n data, address = sock.recvfrom(MAX_BYTES)n text = data.decode(ascii)n print(The client at {} says {!r}.format(address, text))n text = Your data was {} bytes long.format(len(data))n data = text.encode(ascii)n sock.sendto(data, address)nndef client(port):n sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)n text = The time is {}.format(datetime.now())n data = text.encode(ascii)n sock.sendto(data, (127.0.0.1, port))n print(The OS assigned me the address {}.format(sock.getsockname()))n data, address = sock.recvfrom(MAX_BYTES) # Danger! See Chapter 2n text = data.decode(ascii)n print(The server {} replied {!r}.format(address, text))nnif __name__ == __main__:n choices = {client: client, server: server}n parser = argparse.ArgumentParser(description=Send and receive UDP locally)n parser.add_argument(role, choices=choices, help=which role to play)n parser.add_argument(-p, metavar=PORT, type=int, default=1060,n help=UDP port (default 1060))n args = parser.parse_args()n function = choices[args.role]n function(args.p)n

這個腳本只使用了本地的IP地址。沒有連接到網路也可以使用。

下面我們說下一個問題:

上面的代碼都是在同一個機器上,如果真正的情況我們要考慮到不可靠性、退避、阻塞和超時。

下面看一下代碼

# 運行在不同機器上的UDP伺服器和客戶端nimport argparse, random, socket, sysnnMAX_BYTES = 65535nndef server(interface, port):n sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)n sock.bind((interface, port))n print(Listening at, sock.getsockname())n while True:n data, address = sock.recvfrom(MAX_BYTES)n if random.random() < 0.5:n print(Pretending to drop packet from {}.format(address))n continuen text = data.decode(ascii)n print(The client at {} says {!r}.format(address, text))n message = Your data was {} bytes long.format(len(data))n sock.sendto(message.encode(ascii), address)nndef client(hostname, port):n sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)n sock.connect((hostname, port))n print(Client socket name is {}.format(sock.getsockname()))nn delay = 0.1 # secondsn text = This is another messagen data = text.encode(ascii)n while True:n sock.send(data)n print(Waiting up to {} seconds for a reply.format(delay))n sock.settimeout(delay)n try:n data = sock.recv(MAX_BYTES)n except socket.timeout as exc:n delay *= 2 # wait even longer for the next requestn if delay > 2.0:n raise RuntimeError(I think the server is down) from excn else:n break # we are done, and can stop loopingnn print(The server says {!r}.format(data.decode(ascii)))nnif __name__ == __main__:n choices = {client: client, server: server}n parser = argparse.ArgumentParser(description=Send and receive UDP,n pretending packets are often dropped)n parser.add_argument(role, choices=choices, help=which role to take)n parser.add_argument(host, help=interface the server listens at;n host the client sends to)n parser.add_argument(-p, metavar=PORT, type=int, default=1060,n help=UDP port (default 1060))n args = parser.parse_args()n function = choices[args.role]n function(args.host, args.p)n

然後我們再說UDP分組的問題:

一般的乙太網卡只支持1500B的數據包。所以我們需要把大的數據包分為幾個比較小的數據包。

下面是在Linux運行的,網路編程的腳本。

# 發送大型UDP數據包nimport argparse, socket, sysnn# Inlined constants, because Python 3.6 has dropped the IN module.nnclass IN:n IP_MTU = 14n IP_MTU_DISCOVER = 10n IP_PMTUDISC_DO = 2nnif sys.platform != linux:n print(Unsupported: Can only perform MTU discovery on Linux,n file=sys.stderr)n sys.exit(1)nndef send_big_datagram(host, port):n sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)n sock.setsockopt(socket.IPPROTO_IP, IN.IP_MTU_DISCOVER, IN.IP_PMTUDISC_DO)n sock.connect((host, port))n try:n sock.send(b# * 999999)n except socket.error:n print(Alas, the datagram did not make it)n max_mtu = sock.getsockopt(socket.IPPROTO_IP, IN.IP_MTU)n print(Actual MTU: {}.format(max_mtu))n else:n print(The big datagram was sent!)nnif __name__ == __main__:n parser = argparse.ArgumentParser(description=Send UDP packet to get MTU)n parser.add_argument(host, help=the host to which to target the packet)n parser.add_argument(-p, metavar=PORT, type=int, default=1060,n help=UDP port (default 1060))n args = parser.parse_args()n send_big_datagram(args.host, args.p)n

下面說UDP的另外一個功能,那就是廣播:

通過廣播可以將數據包的目標地址設置為本機連接的整個子網,然後使用物理網卡就可以將數據包廣播,這樣就無需複製該數據包並單獨將其發送給所有連接至該子網的主機。

當然,現在廣播已經過時了,都使用的多播技術。

不過還是可以參考一下代碼:

# UDP廣播nimport argparse, socket, sysnn# Inlined constants, because Python 3.6 has dropped the IN module.nnclass IN:n IP_MTU = 14n IP_MTU_DISCOVER = 10n IP_PMTUDISC_DO = 2nnif sys.platform != linux:n print(Unsupported: Can only perform MTU discovery on Linux,n file=sys.stderr)n sys.exit(1)nndef send_big_datagram(host, port):n sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)n sock.setsockopt(socket.IPPROTO_IP, IN.IP_MTU_DISCOVER, IN.IP_PMTUDISC_DO)n sock.connect((host, port))n try:n sock.send(b# * 999999)n except socket.error:n print(Alas, the datagram did not make it)n max_mtu = sock.getsockopt(socket.IPPROTO_IP, IN.IP_MTU)n print(Actual MTU: {}.format(max_mtu))n else:n print(The big datagram was sent!)nnif __name__ == __main__:n parser = argparse.ArgumentParser(description=Send UDP packet to get MTU)n parser.add_argument(host, help=the host to which to target the packet)n parser.add_argument(-p, metavar=PORT, type=int, default=1060,n help=UDP port (default 1060))n args = parser.parse_args()n send_big_datagram(args.host, args.p)n

到這裡,內容大概就說完了。

至於什麼時候使用UDP:

  1. 實現一個已經使用了的UDP協議
  2. 要設計對時間很嚴苛的媒體流
  3. 設計LAN子網多播的應用程序

UDP建立在網路數據包的基礎之上,所以它是不可靠的。

可能會遇到重複相應的問題。這時候請求ID是解決重複響應問題的利器。

具體的會在TCP文章裡面說到。

到這裡,2017年的最後一篇文章就結束了。

謝謝大家關注。

大家多注意身體。

元旦快樂!

2018,祝所有人新的一年都會經歷更好的事。


推薦閱讀:

TAG:Python | 网络编程 | 计算机网络 |