Socket Programming HOWTO(翻譯)
Author: Gordon McMillan
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
大致而言,當你點擊連結,帶你到這個網頁,你的瀏灠器做了下面事情:
# 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
# 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的主要迴圈:
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
。在多數平台上,這將圍繞兩層網絡程式碼走一條捷徑,而且速度要快得多。
See also 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(End-of-Transmission)。再說一遍,如果socket在處理0位元組之後send
或recv
回傳,那這個連線已經斷了。如果連線沒有斷,你也許會一直等著recv
,因為socket不會告訴你(現在)沒東西可讀了。現在,如果你稍微思考一下,你就會瞭解sockets的基本原理:訊息必需固定長度、或是被分割、或者指定它們的長度、或通過關閉連線結束。這都取決於你的決定(但有些方法比其它方法更正確)。
假設你沒有要斷開連線,最簡單的解決方式就是固定訊息長度:
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通常很適合網路緩衝區大小),並掃描你接收到的分隔符號。
需要注意的一個複雜因素是:如果你的通訊協定允許多個訊息被回送(back to back)(不需某種回覆),然後你傳遞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我通常使用執行緒(執行狀況非常、非常好)。
沒有留言:
張貼留言