# Socket Programming HOWTO(翻譯) ###### tags: `翻譯` [官方文件](https://docs.python.org/3/howto/sockets.html) Author: Gordon McMillan :::info **Abstract** `Sockets`的應用無所不在,但卻是最被誤會的技術之一。這只是關於`sockets`的概述。它並不是一個真正的教程,你仍然需要下點工夫。它並沒有涵蓋的很精確(絕大部份如此),但我希望可以給你足夠的背景來正確的使用它。 ::: ## Sockets 我只想談談INET(i.e. IPv4)sockets,但它們佔了將近99%使用中的sockets。並且我將只談STREAM(i.e. TCP)sockets,除了你真的知道你在做什麼(這種情況下這個HOWTO並不適合你),你將從STREAM socket得到比其它模型更好的做法及效能。 理解這些事的部份麻煩在於,`socket`可以代表許多不同細微的東西,這取決於應用上下文。首先,我們先區分client端的socket-一個會話的端點,與server端的socket,它更像是一個交換機的操作。client應用程式(如,瀏灠器)單純的使用client sockets;而與它談的web server則同時使用著server sockets與client sockets。 ### History 各種形式的IPC中,sockets是目前為止最受歡迎的。在任何給定平台上,可能其它形式的IPC是更快的,但對於跨平台通信,sockets是唯一的選擇。 它們在Berkeley發明,做為Unix BSD的一部份。他們在網路上迅速傳播。有充份的理由,sockets與INET的結合使得與世界各地的任何機器交談變的異常的簡單(至少與其它方案相比) ## Creating a Socket 大致而言,當你點擊連結,帶你到這個網頁,你的瀏灠器做了下面事情: ```python # create an INET, STREAMing socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # now connect to the web server on port 80 - the normal http port s.connect(("www.python.org", 80)) ``` 當`connect`完成的時候,socket-`s`可以用來發送對頁面文字的請求。相同的socket將會讀取回覆,然後被銷毀。沒錯,被銷毀了。Client sockets通常只用於一次交換(或一小組序列交換) 在web server上發生的事有點複雜。首先,web server建立一個`server socket` ```python # 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()`,這樣外部就可以看到`socket`。如果我們使用`s.bind(('localhost', 80))`或`s.bind(('127.0.0.1', 80))`,我們依然會有一個server socket,但那只會在相同機器上可以看的見。`s.bind(('', 80))`指定socket是可以被機器碰巧有的位址訪問。 第二件要注意的事:數值較小的port通常保留給"已知"的服務(HTTP、SNMP等)。如果你只是玩玩,請記得用四位數以上的port號。 最後,`listen`的參數告訴socket library,我們希望它在拒絕外部連線之前,隊列內應該要有五個連線(正常最大值)。如果其餘的程式碼正確的話,那應該是足夠的。 現在,我們擁有server socket,監聽80port,我們可以進入web server的主要迴圈: ```python 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() ``` 實際上,有三種通用方法是這個迴圈內能做的事-調度一個線程處理`clientsocket`,建立一個進程處理`clientsocket`,或重構這個應用程式,使用非阻塞式socket,以及使用`select`在我們的server socket及任一活動的`clientstockets`進行多工。稍後會詳細介紹。現在需要理解的一個重點是:這就是server socket做的全部的事了。它沒有發送任何的資料。它沒有接收任何的資料。它只生成client sockets。每個`clientsocket`都是為了回應某些其它client socket執行`connect()`到我們所綁定的主機與port而建立。一但我們建立`clientsocket`,我們就會回頭監聽更多的連線。兩個clients可以自由交握-它們使用一些動態分配的port,當對話結束的時候,這些port會被回收。 ### IPC 如果你需要在一台機器上的兩個進程之間快速的IPC,你應該調查`pipes`或共享記憶體。如果你決定使用`AF_INET`sockets,那就綁定server socket到`localhost`。在多數平台上,這將圍繞兩層網絡程式碼走一條捷徑,而且速度要快得多。 :::info See also [multiprocessing](https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing)整合跨平台IPC到高階API中。 ::: ## Using a Socket 首先要注意的是web browser的client socket與web server的client socket是完全相同的。意思是,這是點對點的通訊。或者換種方式,做為設計人員,你必須決定通訊的禮儀規則。通常,連接socket通過發送一個需求或登錄來啟動通訊。但它是設計決定-它並不是sockets的規則。 現在有兩組動詞用於通訊。你可以使用`send`與`recv`,或首可以轉換你的client socket變為`file-like`,然後使用`read`與`write`。後者是Java呈現socket的方式。除了警告你需要在socket上使用`flush`之外,我並不會在這邊討論它。這些是緩衝檔案,常見錯誤是寫入一些東西,然後讀取回覆。如果沒有`flush`,你也許會一直等待回覆,因為request依然在你的輸出緩衝區中。 現在我們回到socket的主要癥結-`send`與`recv`操作在網路緩衝區中。它們並不需要處理所有你傳送給它們的位元組,因為它們主要關注的是處理網路緩衝區。通常,它們在相關的網路緩衝被填滿(`send`)或清空(`recv`)的時候回傳。然後它們會告訴你它們處理多少位元組。在你的訊息完全的被處理之前,你有責任再次調用它們。 當`recv`回傳0位元組的時候,這代表另一端已經關閉(或者正在關閉)連線。你將不會在這連線上再接收到任何資料。曾經,你也許能夠成功的傳送資料,稍後我會再詳細討論這點。 像HTTP這樣的協定使用socket只能進行一次傳輸。client送出一個request,然後讀取回覆。就這樣。然後socket就被丟掉。這意味著client可以透過接收到0位元組來偵測回覆的結束。 但是,如果你計劃重新使用你的socket做進一步的傳輸,你必需意識到,socket沒有EOT<sub>(End-of-Transmission)</sub>。再說一遍,如果socket在處理0位元組之後`send`或`recv`回傳,那這個連線已經斷了。如果連線沒有斷,你也許會一直等著`recv`,因為socket不會告訴你(現在)沒東西可讀了。現在,如果你稍微思考一下,你就會瞭解sockets的基本原理:訊息必需固定長度、或是被分割、或者指定它們的長度、或通過關閉連線結束。這都取決於你的決定(但有些方法比其它方法更正確)。 假設你沒有要斷開連線,最簡單的解決方式就是固定訊息長度: ```python 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) ``` 這裡的發送程式碼幾乎可以用於任何消息傳遞方式-在Python你發送字串,你可以使用`len()`來確認它的長(即使它包含0個字元)。主要是接收程式碼變得更複雜了(在C語言中,它並不會更糟,除非你不能使用`strlen`)。 最簡單的增強方式就是讓訊息的第一個字元做為訊息的指示器,並且確定長度。現在你有兩個`recv`s-第一個取得(最少)第一個字元,你可以查詢長度,第二個在迴圈中取得其餘的字元。如果你決定使用分離路由,你會接收到一些任意`chunk size`,(4096或8192通常很適合網路緩衝區大小),並掃描你接收到的分隔符號。 需要注意的一個複雜因素是:如果你的通訊協定允許多個訊息被回送<sub>(back to back)</sub>(不需某種回覆),然後你傳遞`recv`任意`chunk size`,你可能最終會讀取到後面的訊息的起始字元。你需要把它放一邊並保持住,直到需要它為止。 訊息前綴長度(假設是五個字元)變的更複雜,因為(信不信由你)你可能無法一次`recv`得到五個字元。在遊戲中你可以擺脫它,但在高網路負載中,除非你可以使用兩個`recv`迴圈,否則你的程式碼會很快中斷-第一個迴圈用於確定長度,第二個用於取得訊息的資料部份。雅打。這也是當你發現`send`並不總是可以在一次傳輸處理所有事情的時候。儘管已經讀過這篇文章了,你最終還是會被它反咬一口。 ### Binary Data 通過socket發送二進制資料是完全有可能的。主要的問題是並不是所有的機器都可以使用相同格式的二進制資料。舉例來說,一個Motorola芯片將代表一個十六位元整數,值為1,即兩個十六進制位元組`00 01`,Intel與DEC,然而,位元組是倒轉的,相同的1,其十六進制為`01 00`。socket套件要求轉換十六與三十二位元整數-`ntohl, htonl, ntohs, htons`,`n`代表`network`,`h`代表`host`,`s`代表`short`,`l`代表`long`。如果網路順序是host順序,那它們什麼也不會做,但如果是位元組反轉的話,那它們會適當的交換位元組。 在現在32bit的機器中,二進制資料的ascii表示通常小於二進制表示。這是因為驚人的時間量,所有這些長整數型的值都是0,或者可能是1。字串0是2bytes,而二進制是4bytes。當然,這不是適固定長度的訊息。 ### Disconnecting 嚴格來說,你應該在`close`socket之前先執行`shutdown`。`shutdown`是給另一端socket的提示。取決於你傳送的參數,它可是"我沒有要傳送資料了,但我依然監聽中",或"我沒監聽了,清除!"。然而,多數的socket libraries都是習慣程式設計師忽略這規則,通常`close`與`shutdown()`相同。`close()`。因此,多數情況下不需要直接執行`shutdown`。 一種有效使用`shutdown`的方法是在HTTP-like exchange。client端發送一個request,然後執行`shutdown(1)`。這告訴server端"這個client完成發送,但依然可以接收"。server可以透過接收0位元組來偵測"EOF"。它可以假設它已經完成request。server發送一個回覆。如果`send`成功完成,那實際上client依然在接收。 Python將自動關閉更進一步,並表明當socket被垃圾回收時,如果需要,它將自動執行`close`。但依賴這個是一個壞習慣。如果你的socket沒有`close`情況下消失,那另一端的socket也許會無限期的掛著,你還會以為只是變慢了。當你完成的時候請記得一定要`close`你的sockets。 ### When Sockets Die 使用`blocking sockets`最糟糕的事情,就是另一邊掛掉的時候會發生什麼事(沒有執行`close`)。你的socket會掛著。TCP是一種可靠的協議,在放棄連線之前它會等待非常長的一段時間。如果你使用thread,那基本上整個線程已經掛了。對此,你無能為力。只要你沒有做一些很愚蠢的事情,像是在`blocking read`的時候拿著鎖,那thread並不會消耗太多資源。不要試著去終止thread-threads比processes更有效率部份原因在於它們避免資源自動回收的開銷。換句話說,如果你試著去終止thread,你的整個過程很可能被搞砸。 ## Non-blocking Sockets 如果你有瞭解上述內容,那你已經瞭解使用sockets機制的大致內容。你仍然會使用大致相同的方式調用。就是這樣,如果你做對了,那你的應用程式幾乎是內而外。 在Python,你使用`socket.setblocking(0)`來使它non-blocking。在C,它會更複雜,(首先,你需要在BSD風格`O_NONBLOCK `與幾乎無法區分的Posix風格`O_NDELAY`之間做個選擇,這與`TCP_NODELAY`完全不同)這是完全相同的概念。在建立socket之後執行這個操作, 但在使用它之前。(事實上,如果你瘋了,你可以來回切換。) 主要的機械式差異在於`send`,`recv`,`connect`與`accept`可以在不做任何事情況下回傳。你有(當然)多種選擇。你可以檢查回傳的程式碼與異常程式碼,這通常讓你抓狂。如果你不相信我,試一下。你的應用程式會變的愈來愈大、愈來愈多蟲,吸光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`三個lists:第一個包含了所有你想試著讀取的sockets;第二個是你想試著寫入的sockets,最後一個(通常空的)是你想檢查的錯誤。你應該注意到,socket可以進入多個lists。`select`的調用是blocking,但你可以設置timeout,這通常是一個明智的作法-給它一個很長的timeout(假設一分鐘),除非你有一個很好的理由不這麼做。 回傳的部份也將得到三個lists。他們包含了實際可讀、可寫以及錯誤的sockets。這些清單中的每一個都是你傳入的相對應清單的子集(可能是空的)。 如果socket在可讀的list中,你可以盡可能的像我們在這業務中所得的那樣,以便在該socket上的`recv`將回傳某些東西。對可寫list是相同的想法,你可以發送一些東西。這也許不是你想要的,但有一些東西總比沒東西好。(事實上,任何正常的socket都將以可寫模式回傳-這意味著輸出網路緩衝空間是可用的)。 如果你有一個server socket,把它放進`potential_readers`。如果它出現在可讀的list中,你的`accept`(幾乎確定)會有作用。如果你已經建立新的socket,並且`connect`到其它人身上了,把它放到`potential_writers` list。如果它出現在可寫的list中,那就有機會它是已經連接的了。 事實上,即使使用blocking sockets,`select`依然是非常方便的。這是一個確認你是否阻塞的方法-當有些東西在緩衝區的時候,socket回傳為可讀。然而,這對判斷另一端是否完成是沒有幫助的,或者它只是忙著其它事情。 跨平台警示: 在Unix上,`select`可以同時適用於sockets與files。不要在Windows上這麼做。在Windows上,`select`僅支援sockets。還有C語言,socket很多進階選項在Windows上是不同的。事實上,在Windows我通常使用執行緒(執行狀況非常、非常好)。