注意:我在這裏的解釋假設你在談論TCP套接字,或者至少是某種基於連接的類型。 UDP和其他數據報(即非基於連接的)套接字在某些方面是相似的,但是你在它們上使用select
的方式略有不同。
每個套接字就像一個可以讀取和寫入數據的打開文件。您寫入的數據會進入等待在網絡上發送出去的系統內的緩衝區。從網絡到達的數據在系統內部進行緩衝,直到您讀到它爲止。很多聰明的東西都在下面,但是當你使用一個你真正需要知道的套接字時(至少在最初時)。
在下面的解釋中記住系統正在做這種緩衝通常很有用,因爲您會意識到操作系統中的TCP/IP堆棧獨立於應用程序發送和接收數據 - 這樣做是爲了讓您的應用程序可以有一個簡單的接口(這就是套接字,這是隱藏代碼中所有TCP/IP複雜性的一種方式)。
這樣做讀寫的一種方法是阻止。例如,使用該系統,當您撥打recv()
時,如果系統中有數據等待,則會立即返回。但是,如果沒有數據等待,那麼調用會阻止 - 也就是說,程序會暫停,直到有數據要讀取。有時你可以用超時來做到這一點,但是在純粹的阻塞IO中,你真的可以永遠等待,直到另一端發送一些數據或關閉連接。
對於一些簡單的情況,這不起作用,但只在與另一臺機器通話的地方 - 當您在多個套接字上通信時,不能等待來自一臺機器的數據因爲另一個人可能會向你發送東西。還有其他問題,我不會在這裏詳細介紹 - 足以說這不是一個好方法。
一個解決方案是爲每個連接使用不同的線程,所以阻塞是可以的 - 其他線程可以被阻塞而不會相互影響。在這種情況下,每個連接需要兩個線程,一個讀取,一個寫入。然而,線程可能是棘手的野獸 - 你需要仔細地同步它們之間的數據,這可能會使編碼變得複雜一些。而且,對於像這樣的簡單任務來說,它們效率不高。
的select
模塊,可以單線程解決了這個問題 - 而不是阻塞在單個連接上,它可以讓你一個函數,它說:「才睡覺,這些插座中的至少一個有一些數據,我可以讀在上面「(這是我稍後會糾正的簡化)。因此,一旦對select.select()
的調用返回,您可以確定您正在等待的其中一個連接有一些數據,並且您可以放心地讀取它(即使阻止了IO,如果您小心 - 因爲您確定那裏有數據,你永遠不會阻止等待它)。
當你第一次啓動你的應用程序時,你只有一個套接字,它是你的監聽套接字。所以,你只能通過select.select()
的通話。我之前做的簡化是,實際上這個調用接受三個用於讀,寫和錯誤的套接字列表。第一個列表中的套接字被監視讀取 - 因此,如果其中任何一個有數據要讀取,則select.select()
函數會將控制權返回給您的程序。第二個列表是用於寫入的 - 你可能認爲你總是可以寫入一個套接字,但實際上,如果連接的另一端沒有足夠快地讀取數據,那麼系統的寫入緩衝區可能會被填滿,並且你可能暫時無法寫入。看起來像給你代碼的人忽略了這種複雜性,這對於一個簡單的例子來說並不算太壞,因爲通常緩衝區足夠大,你不可能在這樣的簡單情況下遇到問題,但是這是一個問題,你應該在您的代碼的其餘部分工作後,將來的地址。最終列表會收到錯誤信息 - 這不是廣泛使用,所以我現在就跳過它。通過空列表在這裏沒問題。
在這一點上,有人連接到你的服務器 - 至於select.select()
所關心的是,這使得監聽套接字「可讀」,所以函數返回並且可讀套接字列表(第一個返回值)將包括listen插座。
下一部分運行所有有數據讀取的連接,並且您可以看到您的偵聽套接字s
的特例。該代碼在其上調用accept()
,它將從偵聽套接字中獲取下一個等待的新連接,並將其變爲該連接的全新套接字(偵聽套接字繼續偵聽,並且可能還有其他新連接正在等待,但這沒關係 - 我會在第二秒)。全新的套接字被添加到connections
列表中,這是處理偵聽套接字的結束 - continue
將移動到從select.select()
返回的下一個連接(如果有的話)。
對於其他可讀的連接,代碼會調用recv()
來恢復下一個1024
字節(或者小於1024字節時可用的任何字節)。重要注意事項 - 如果您沒有使用select.select()
來確保連接可讀,對recv()
的這個調用可能會阻止並暫停程序,直到數據到達特定連接 - 希望這可以說明爲什麼需要select.select()
。
一旦讀取了一些數據,代碼就會在所有其他連接(如果有)上運行,並使用send()
方法將數據向下複製。代碼正確跳過與剛剛到達的數據(這是關於q != i
的業務)相同的連接,並且跳過s
,但正如它發生的情況,這不是必需的,因爲據我所見,它實際上從未添加到connections
列表中。
所有可讀連接處理完畢後,代碼返回到select.select()
循環等待更多數據。請注意,如果連接仍有數據,則該調用立即返回 - 這就是爲什麼只接受來自偵聽套接字的單個連接即可。如果有更多的連接,select.select()
將立即再次返回,循環可以處理下一個可用的連接。你可以使用非阻塞IO來提高效率,但它使事情變得更加複雜,所以讓我們現在就讓事情變得簡單。
這是一個合理的說明,但不幸的是它的一些問題遭受:
- 正如我所說,該代碼假定您可以隨時撥打
send()
安全的,但如果你有一個連接在另一端的ISN」 t正確接收(也許該機器已超載),那麼您的代碼可能會填滿發送緩衝區,然後在嘗試呼叫send()
時掛起。
- 該代碼無法應對連接關閉,這通常會導致從
recv()
返回空字符串。這應該會導致連接關閉並從connections
列表中刪除,但此代碼不會執行此操作。
我已經更新了代碼稍微嘗試解決這兩個問題:
connections = []
buffered_output = {}
while True:
rlist,wlist,xlist = select.select(connections + [s],buffered_output.keys(),[])
for i in rlist:
if i == s:
conn,addr = s.accept()
connections.append(conn)
continue
try:
data = i.recv(1024)
except socket.error:
data = ""
if data:
for q in connections:
if q != i:
buffered_output[q] = buffered_output.get(q, b"") + data
else:
i.close()
connections.remove(i)
if i in buffered_output:
del buffered_output[i]
for i in wlist:
if i not in buffered_output:
continue
bytes_sent = i.send(buffered_output[i])
buffered_output[i] = buffered_output[i][bytes_sent:]
if not buffered_output[i]:
del buffered_output[i]
我應該在這裏指出的是,我認爲,如果遠端關閉連接,我們也想在這裏立即關閉。嚴格地說,這忽略了TCP half-close的潛力,遠程端已經發送了一個請求並且關閉了它的結束,但仍然期待數據恢復。我相信很久以前的HTTP版本有時會這樣做,以表明請求已結束,但實際上這很少再使用,可能與您的示例無關。
另外值得注意的是,很多人使用select
時,使他們的插座非阻塞 - 這意味着,recv()
或send()
呼叫,否則塊將代替返回一個錯誤(提高在Python條款的除外)。這部分是爲了安全起見,以確保不小心的代碼不會阻止應用程序;但它也允許一些稍微更高效的方法,例如在多個塊中讀取或寫入數據,直到沒有剩餘的數據塊爲止。使用阻塞IO這是不可能的,因爲select.select()
調用只保證有一些數據要讀取或寫入 - 它不能保證多少。因此,您只能在每次連接上安全地撥打阻止send()
或recv()
一次,然後再次調用select.select()
以查看是否可以再次這樣做。這同樣適用於監聽套接字上的accept()
。
但是,對於擁有大量繁忙連接的系統來說,效率節省通常只是一個問題,所以在您的情況下,我會保持簡單,而不必擔心現在阻塞。在你的情況下,如果你的應用程序似乎掛斷了,並且變得沒有響應,那麼你可能會在某個你不應該的地方進行阻塞呼叫。
最後,如果你想使這個代碼便攜式和/或更快,它可能是值得看的東西像libev
,基本上有幾個替代select.select()
其在不同平臺上運行良好。然而,這些原則大致相同,因此現在最好着重於select
,直到您運行代碼,然後調查將在稍後進行更改。
另外,我注意到一位評論者建議Twisted這是一個提供更高級別抽象的框架,因此您不必擔心所有細節。就我個人而言,過去我遇到過一些問題,例如很難以簡便的方式捕捉錯誤,但很多人非常成功地使用它 - 這只是他們的方法是否適合您思考問題的問題。值得至少調查一下,看看它的風格是否適合你,比對我更好。我來自使用C/C++編寫網絡代碼的背景,所以也許我只是堅持我所知(Python select
模塊非常接近它所基於的C/C++版本)。
希望我已經在這裏充分解釋了一些事情 - 如果您還有問題,請在評論中告訴我,我可以在我的答案中添加更多詳細信息。
你看過Twisted Python嗎?它是現成的 - http://twistedmatrix.com/documents/current/core/examples/#auto1 – 2013-03-06 00:30:25