2013-03-05 84 views
1

對不起,打擾所有人,但我已經被困了一段時間了。如何通過套接字和選擇模塊管理聊天服務器(Python)的套接字連接

問題是我決定重新配置這個聊天程序,我使用套接字,而不是客戶端和服務器/客戶端,它會有一個服務器,然後是兩個單獨的客戶端。

我早前問過如何讓我的服務器'管理'這些​​客戶端的連接,以便它可以重定向它們之間的數據。我得到了一個很好的答案,它爲我提供了準確的代碼,我顯然需要這樣做。

問題是我不明白它是如何工作的,我在評論中詢問過,但除了一些文檔鏈接外,我沒有得到太多答覆。

這裏是我得到了什麼:

connections = [] 

while True: 
    rlist,wlist,xlist = select.select(connections + [s],[],[]) 
    for i in rlist: 
     if i == s: 
      conn,addr = s.accept() 
      connections.append(conn) 
      continue 
     data = i.recv(1024) 
     for q in connections: 
      if q != i and q != s: 
       q.send(data) 

據我瞭解,選擇模塊爲使在select.select的情況下,可等待對象的能力。

我已經得到了rlist,待讀取列表,wlist,等待寫入列表,然後是xlist,即將發生的異常情況。

他將掛起寫入列表分配給在我的聊天服務器部分中的「s」,它是在指定端口上監聽的套接字。

這就像我覺得我理解得很清楚一樣。但我真的很喜歡一些解釋。

如果您不覺得我問了一個合適的問題,請在評論中告訴我,我會將其刪除。我不想違反任何規則,而且我敢肯定我沒有重複線索,因爲我在做一些研究之前會訴諸問題。

謝謝!

+0

你看過Twisted Python嗎?它是現成的 - http://twistedmatrix.com/documents/current/core/examples/#auto1 – 2013-03-06 00:30:25

回答

7

注意:我在這裏的解釋假設你在談論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來提高效率,但它使事情變得更加複雜,所以讓我們現在就讓事情變得簡單。

這是一個合理的說明,但不幸的是它的一些問題遭受:

  1. 正如我所說,該代碼假定您可以隨時撥打send()安全的,但如果你有一個連接在另一端的ISN」 t正確接收(也許該機器已超載),那麼您的代碼可能會填滿發送緩衝區,然後在嘗試呼叫send()時掛起。
  2. 該代碼無法應對連接關閉,這通常會導致從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++版本)。

希望我已經在這裏充分解釋了一些事情 - 如果您還有問題,請在評論中告訴我,我可以在我的答案中添加更多詳細信息。