使用 Python 進行 socket 編程
本文主要參考 https://docs.python.org/3/howto/sockets.html 。
本文只討論 STREAME(比如 TCP) INET(比如 IPv4) socket。
在多種跨進程通信方式中,sockets 是最受歡迎的。對於任意給定的平臺,有可能存在其他更快的跨進程通信方式,但對於跨平臺交流,sockets 應該是唯一的一種。
創建 Socket
客戶端 Socket
通俗的講,當你點擊一個鏈接,你的瀏覽器會做以下事情:
# create an INET, STREAMing socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 連接到服務器,如果 URL 中沒有指明端口,那麽端口為默認的 80
s.connect(("www.python.org", 80))
建立連接後,可以用 socket s
來發送請求。然後 s
會讀取回復,然後被銷毀。在一次請求-接收過程(或者一系列連續的小的過程)中,客戶端 sockets 通常只會被使用一次。
服務端 Socket
對於 web 服務器來說:
# create an INET, STREAMing socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to a public host, and a well-known port
serversocket.bind((socket.gethostname(), 80))
# become a server socket
serversocket.listen(5)
socket.gethostname()
的地址可以被外部看到。
listen
告知 socket 庫,監聽隊列中最多能有 5 個連接請求,隊列滿了之後的請求會被拒絕。
主循環
while True:
# accept connections from outside
(clientsocket, address) = serversocket.accept()
# now do something with the clientsocket
# in this case, we'll pretend this is a threaded server
ct = client_thread(clientsocket)
ct.run()
沒有連接時 accept 會一直阻塞。
主循環通常有三種工作方式:
- 分派一個線程去處理
clientsocket
- 創建一個進程去處理
clientsocket
- 重構以使用非阻塞 sockets 並使用
select
在我們的服務器 socket 和clientsocket
多路傳輸(multiplex)
上面的代碼就是服務端 socket 所做的。它不發送、接收任何數據。它只是生產 clientsocket
。每個 clientsocket
被創建出,用來響應 connect()
來的 “client” sockets(比如瀏覽器)。
服務端 socket 在創建 clientsocket
後,又重新返回去監聽更多的連接。那兩個客戶端 sockets 在自由地交談 -- 使用動態分配的並在談話結束後會被回收的端口。
使用 Socket
作為設計者,你必須決定客戶端 sockets 之間的交流規則。
send
和 recv
操作網絡 buffers,它們不一定會處理所有你傳遞給它們的 bytes,因為它們集中於處理網絡 buffers。當網絡 buffers 被 send
或 recv
時,它們會返回它們處理的 bytes 數目。調用它們以確保所有數據已被處理是你的責任。
當 recv
返回 b""
或者 send
返回 0 意味著另一邊已經關閉了(或正在關閉)連接。如果是 recv ,那麽你將不會從這個連接再收到任何數據,但你可能可以成功的發送數據,在下文會談到。如果是 send,那你不能再向這個 socket 發送任何數據。
類似 HTTP 協議在一次交談中只使用一個 socket。客戶端 socket 發送請求,讀取回復,然後客戶端 socket 被遺棄。所以客戶端可以通過接受到 0 bytes 的回復來發現交談結束了。
如果你打算為了將來的傳輸復用你的 socket,你需要知道 socket 中沒有傳輸結束(EOT)這個標識。
總結一下:如果 send
或 recv
0 bytes,那麽這個連接已經被關閉了。如果一個連接沒有被關閉,你可能永遠在等 recv
,因為 socket 不會告訴你現在並沒有更多消息了。
所以信息
- 必須是固定長度的
- 或者被劃定了界限
- 或者指出信息有多長
- 或者以關閉連接來結束
完全由你來選擇使用何種方法。
信息長度指 send
和 recv
的信息的長度。比如 send
發送 bytes,那麽是 str 轉換為 bytes 後的信息的長度而不是 str 的表示的信息的長度。
最簡單的方法是固定長度的消息:
class MySocket:
"""demonstration class only
- coded for clarity, not efficiency
"""
def __init__(self, sock=None):
if sock is None:
self.sock = socket.socket(
socket.AF_INET, socket.SOCK_STREAM)
else:
self.sock = sock
def connect(self, host, port):
self.sock.connect((host, port))
def mysend(self, msg):
totalsent = 0
while totalsent < MSGLEN:
sent = self.sock.send(msg[totalsent:])
if sent == 0:
raise RuntimeError("socket connection broken")
totalsent = totalsent + sent
def myreceive(self):
chunks = []
bytes_recd = 0
while bytes_recd < MSGLEN:
chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
if chunk == b'':
raise RuntimeError("socket connection broken")
chunks.append(chunk)
bytes_recd = bytes_recd + len(chunk)
return b''.join(chunks)
長度的選擇是要發送的信息的最大長度,如果信息長度不足,那麽按照約定補充信息直到長度符合,約定的字符也是由你決定。
上面的代碼是確保發送、接收的代碼不小於定義的長度。
在發送時,由於發送的長度不固定,所以每次要從之前發送的信息之後開始發送。
接收時,要準確地指定需要接收的消息長度。如果指定長度小於實際長度,那麽信息就不完整;反之,會一直等待信息發送。又由於最多接收 2048 bytes,所以要 min(MSGLEN - bytes_recd, 2048)
。
Python 的 len()
可以計算含有 \0
的消息的長度,而在 C 語言中不能使用 strlen
計算含有 \0
的消息的長度。
使用信息長度作為前綴
假設使用 5 個字符作為信息前綴來表示信息長度,那麽你可能不能獲取所有的 5 個字符在一個 recv
中,可能出現在網絡負載高的情況下。所以你可以調用兩次 recv
-- 第一個決定長度,第二個獲取剩余的信息。
二進制數據
可以使用 socket 發送二進制數據。主要的問題是並不是所有的機器都是用同樣的二進制數據格式。比如摩托羅拉芯片使用兩個十六進制 bytes 00 01
表示16進制整數 1
。然而 Intel 和 DEC 是 byte-reversed -- 使用 01 00
表示 1
。
在現在的 32 位機器上, 用 ascii 表現的二進制數據通常比二進制表示的數據更小。因為在很多時候,數據中含有 0 或 1.字符串 “0” 是 2 bytes 而二進制是 4。所以,不適合用固定長度的信息。所以需要你選擇合適的傳遞信息的策略當你想要能夠使用 socket 傳遞字符串和二進制數據。
Python Struct 操縱二進制數據
>>> from struct import *
>>> pack('hhl', 1, 2, 3)
b'\x00\x01\x00\x02\x00\x00\x00\x03'
>>> unpack('hhl', b'\x00\x01\x00\x02\x00\x00\x00\x03')
(1, 2, 3)
>>> calcsize('hhl')
8
以上參考 https://docs.python.org/3/library/struct.html
斷開連接
嚴格的說,在 close
socket 之前,你應該調用 shutdown
。根據傳遞給 shutdown
的參數,可以表示 “我不會再從這個 socket 讀或向這個 socket 寫數據”。大部分 socket 庫,由於程序員老是忘記調用 shutdown
,所以 close
相當於 shutdown(); close()
。所以在大部分情況下,不需要顯示調用 shutdown
。
類似 HTTP 傳輸是可以有效地使用 shutdown
。客戶端在發送請求後調用 shutdown(1)
。這告訴服務器 “這個客戶端發送完了,但仍然會接收信息”。服務器可以通過收到 0 bytes 來知道這是 EOF(文檔的結束)。
在 Python 中,如果 socket 被垃圾回收了,它會在需要時自動執行 close()
。但依靠這個是非常糟糕的習慣。如果你的 socket 在消失前沒有執行 close
,那麽另一邊的 socket 會一直掛起。
什麽時候該清除 Sockets
使用阻塞 sockets 時最糟糕的事可能是另一邊掛了(而沒有調用 close
)。你的 socket 會一直掛起。TCP 是一個可靠的協議,所以在關閉連接之前,它會等待很久。如果你使用了線程,那麽整個線程就掛了。對此你不能做什麽。只要你不做一些愚蠢的事情,比如在做阻塞操作時加鎖了,線程不會消耗很多資源。不要嘗試取殺死這個線程 -- 線程比進程更高效的部分原因就是線程避免了自動資源回收。換句話說,如果你殺死了這個線程,那麽你的整個進程很可能會掛掉。
非阻塞 Sockets
在 Python 中,你使用 socket.setblocking(0)
令 socket 非阻塞。在 C 語言中會更加復雜,但思想是相同的。你要在創建 socket 之後做這個。
機制的主要區別是 send
、recv
、connect
、accpet
沒有做任何事就會返回。你有很多選擇。比如檢查返回碼和錯誤碼,但這會使你的應用變大、容易出 bug 並且消耗大量 CPU。
使用 select
。
在 C 語言中,使用 select
很復雜。在 Python 中,它十分容易,但它也足夠接近 C 中的概念,如果你理解了 Python 中的 select
,那麽你理解 C 中的不會有很大問題:
ready_to_read, ready_to_write, in_error = \
select.select(
potential_readers,
potential_writers,
potential_errs,
timeout)
傳遞給 select
三個參數:
- 所有你想讀的 sockets 列表
- 所有你想寫的 sockets 列表
- 所有你想檢查錯誤的 sockets 列表
你應該註意一個 socket 可以出現在多個列表中。調用 select
是阻塞的,但你可以給它一個超時時間。
返回 3 個列表。分別包含可讀的、可寫的和錯誤的 sockets。
如果一個 socket 在可讀列表中,那麽調用對其 recv
一定會返回一些東西。對可寫的也是同理。然後你可以對其使用上文阻塞操作中用到的讀寫方法。
- 創建 server socket,將其設置為非阻塞
- 將 server socket 放入 potential_readers
- 以 potential_readers 為參數調用 select
- 檢查 ready_to_read,如果是 server socket,對其調用 accept,獲取 client socket,將 client socket 設置為非阻塞
- 將 client socket 添加到 potential_writers 和 potential_readers 中
- 以 potential_writers 和 potential_readers 作為參數調用 select
- 從 potential_readers 的 client socket 中讀取信息,並儲存到 msg[client socket] 中
- 從 potential_writers 中獲取 client socket,並向其發送 msg[client socket] 如果 msg[client socket] 存在
以上參考 https://pymotw.com/2/select/ 。
可移植性警告:在 Unix 中,select
對 sockets 和 files 都有效。而在 Windows 中,select
只對 sockets 有效。並且在 C 中,很多 socket 的高級特性在 Windows 中都不同。因此推薦在 Windows 中使用 thread。
具體代碼
見 https://github.com/Jay54520/python_socket/ 。
參考
- https://gist.github.com/owainlewis/3217710
- https://docs.python.org/3/library/struct.html
- https://pymotw.com/2/select/
使用 Python 進行 socket 編程