1. 程式人生 > >使用 Python 進行 socket 編程

使用 Python 進行 socket 編程

ini block 設置 機器 public 責任 som 所有 list

本文主要參考 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 之間的交流規則。

sendrecv 操作網絡 buffers,它們不一定會處理所有你傳遞給它們的 bytes,因為它們集中於處理網絡 buffers。當網絡 buffers 被 sendrecv 時,它們會返回它們處理的 bytes 數目。調用它們以確保所有數據已被處理是你的責任

recv 返回 b"" 或者 send 返回 0 意味著另一邊已經關閉了(或正在關閉)連接。如果是 recv ,那麽你將不會從這個連接再收到任何數據,但你可能可以成功的發送數據,在下文會談到。如果是 send,那你不能再向這個 socket 發送任何數據。

類似 HTTP 協議在一次交談中只使用一個 socket。客戶端 socket 發送請求,讀取回復,然後客戶端 socket 被遺棄。所以客戶端可以通過接受到 0 bytes 的回復來發現交談結束了。

如果你打算為了將來的傳輸復用你的 socket,你需要知道 socket 中沒有傳輸結束(EOT)這個標識。

總結一下:如果 sendrecv 0 bytes,那麽這個連接已經被關閉了。如果一個連接沒有被關閉,你可能永遠在等 recv,因為 socket 不會告訴你現在並沒有更多消息了。

所以信息

  • 必須是固定長度的
  • 或者被劃定了界限
  • 或者指出信息有多長
  • 或者以關閉連接來結束

完全由你來選擇使用何種方法。

信息長度指 sendrecv 的信息的長度。比如 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 之後做這個。

機制的主要區別是 sendrecvconnectaccpet 沒有做任何事就會返回。你有很多選擇。比如檢查返回碼和錯誤碼,但這會使你的應用變大、容易出 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/ 。

參考

  1. https://gist.github.com/owainlewis/3217710
  2. https://docs.python.org/3/library/struct.html
  3. https://pymotw.com/2/select/

使用 Python 進行 socket 編程