通過python 構建一個簡單的聊天服務器


構建一個 Python 聊天服務器

一個簡單的聊天服務器

現在您已經了解了 Python 中基本的網絡 API;接下來可以在一個簡單的應用程序中應用這些知識了。在本節中,將構建一個簡單的聊天服務器。使用 Telnet,客戶機可以連接到 Python 聊天服務器上,並在全球范圍內相互進行通信。提交到聊天服務器的消息可以由其他人進行查看(以及一些管理信息,例如客戶機加入或離開聊天服務器)。這個模型如圖 1 所示。


圖 1. 聊天服務器使用 select 方法來支持任意多個客戶機
聊天服務器使用 select 方法來支持任意多個客戶機

聊天服務器的一個重要需求是必須可以伸縮。服務器必須能夠支持任意個流(TCP)客戶機。

要支持任意個客戶機,可以使用 select 方法來異步地管理客戶機的列表。不過也可以使用服務器 socket 的 select 特性。select 的讀事件決定了一個客戶機何時有可讀數據,而且它也可以用來判斷何時有一個新客戶機要連接服務器 socket 了。可以利用這種行為來簡化服務器的開發。

接下來,我們將展示聊天服務器的 Python 源代碼,並說明 Python 怎樣幫助簡化這種實現。

 

ChatServer 類

讓我們首先了解一下 Python 聊天服務器類和 __init__ 方法 —— 這是在創建新實例時需要調用的構造函數。

這個類由 4 個方法組成。run 方法用來啟動服務器,並且允許客戶機的連接。broadcast_string 和 accept_new_connection 方法在類內部使用,我們稍后就會討論。

__init__ 方法是一個特殊的方法,它們會在創建一個類的新實例時調用。注意所有的方法都使用一個 self 參數,這是對這個類實例本身的引用(與 C++ 中的 this 參數非常類似)。這個 self 參數是所有實例方法的一部分,此處用來訪問實例變量。

__init__ 方法創建了 3 個實例變量。port 是服務器的端口號(傳遞給構造函數)。srvsock 是這個實例的 socket 對象,descriptors 是一個列表,包含了這個類中的每個 socket 對象。可以在 select 方法中使用這個列表來確定讀事件的列表。

最后,清單 16 給出了 __init__ 方法的代碼。在創建一個流 socket 之后,就可以啟用 SO_REUSEADDR socket 選項了;這樣如果需要,服務器就可以快速重新啟動了。通配符地址被綁定到預先定義好的端口號上。然后調用 listen 方法,從而允許到達的連接接入。服務器 socket 被加入到descriptors 列表中(現在只有一個元素),但是所有的客戶機 socket 都可以在到達時被加入到這個列表中(請參閱 accept_new_connection)。此時會在 stdout 上打印一條消息,說明這個服務器已經被啟動了。


清單 16. ChatServer 類的 init 方法


					
import socket
import select

class ChatServer:

  def __init__( self, port ):
    self.port = port;

    self.srvsock = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) self.srvsock.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) self.srvsock.bind( ("", port) ) self.srvsock.listen( 5 ) self.descriptors = [self.srvsock] print 'ChatServer started on port %s' % port def run( self ): ... def broadcast_string( self, str, omit_sock ): ... def accept_new_connection( self ): ... 

 

run 方法對於聊天服務器來說是一個循環(請參閱清單 17)。在調用時,它還會進入一個無限循環,並在連接的客戶機之間進行通信。run 方法

服務器的核心是 select 方法。我將 descriptor 列表(其中包含了所有服務器的 socket)作為讀事件的列表傳遞給 select (寫事件和異常事件列表都為空)。當檢測到讀事件時,它會作為 sread 返回。(我們忽略了 swrite 和 sexc 列表。)sread 列表包含要服務的 socket 對象。我們循環遍歷這個 sread 列表,檢查每個找到的 socket 對象。

在這個循環中首先檢查 socket 對象是否是服務器。如果是,就說明一個新的客戶機正在試圖連接,這就要調用 accept_new_connection 方法。否則,就讀取客戶機的 socket。如果 recv 返回 NULL,那就關閉 socket。

在這種情況中,我們構建了一條消息,並將其發送給所有已經連接的客戶機,然后關閉 socket,並從 descriptor 列表中刪除對應的對象。如果 recv 返回值不是 NULL,那么就說明已經有消息可用了,它被存儲在 str 中。這條消息會使用 broadcast_string 發送給其他所有的客戶機。


清單 17. 聊天服務器的 run 方法是這個聊天服務器的核心


					
def run( self ):

  while 1:

    # Await an event on a readable socket descriptor
    (sread, swrite, sexc) = select.select( self.descriptors, [], [] ) # Iterate through the tagged read descriptors for sock in sread: # Received a connect to the server (listening) socket if sock == self.srvsock: self.accept_new_connection() else: # Received something on a client socket str = sock.recv(100) # Check to see if the peer socket closed if str == '': host,port = sock.getpeername() str = 'Client left %s:%s\r\n' % (host, port) self.broadcast_string( str, sock ) sock.close self.descriptors.remove(sock) else: host,port = sock.getpeername() newstr = '[%s:%s] %s' % (host, port, str) self.broadcast_string( newstr, sock ) 



輔助方法

在這個聊天服務器中有兩個輔助方法,提供了接收新客戶機連接和將消息廣播到已連接的客戶機上的功能。

當在到達連接隊列中檢測到一個新的客戶機時,就會調用 accept_new_connection 方法(請參閱清單 18)。accept 方法用來接收這個連接,它會返回一個新的 socket 對象,以及遠程地址信息。我們會立即將這個新的 socket 加入到 descriptors 列表中,然后向這個新的客戶機輸出一條消息歡迎它加入聊天。我創建了一個字符串來表示這個客戶機已經連接了,使用 broadcast_string 方法來成組地廣播這條消息(請參閱清單 19)。

注意,除了要廣播的字符串之外,還要傳遞一個 socket 對象。原因是我們希望有選擇地忽略一些 socket,從而只接收特定的消息。例如,當一個客戶機向一個組中發送一條消息時,這條消息應該發送給這個組中除了自己之外的所有人。當我們生成狀態消息來說明有一個新的客戶機正在加入該組時,這條消息也不應該發送給這個新客戶機,而是應該發送給其他所有人。這種任務是在 broadcast_string 中使用 omit_sock 參數實現的。這個方法會遍歷 descriptors 列表,並將這個字符串發送給那些不是服務器 socket 且不是 omit_sock 的 socket。


清單 18. 在聊天服務器上接收一個新客戶機連接


					
def accept_new_connection( self ):

  newsock, (remhost, remport) = self.srvsock.accept() self.descriptors.append( newsock ) newsock.send("You're connected to the Python chatserver\r\n") str = 'Client joined %s:%s\r\n' % (remhost, remport) self.broadcast_string( str, newsock ) 



清單 19. 將一條消息在聊天組中廣播


					
def broadcast_string( self, str, omit_sock ):

  for sock in self.descriptors:
    if sock != self.srvsock and sock != omit_sock:
      sock.send(str) print str, 

 

 

實例化一個新的 ChatServer

現在您已經看到了 Python 聊天服務器(這只使用了不到 50 行的代碼),現在讓我們看一下如何在 Python 中實例化一個新的聊天服務器。

我們通過創建一個新的 ChatServer 對象來啟動一個服務器(傳遞要使用的端口號),然后調用 run 方法來啟動服務器並允許接收所有到達的連接:


清單 20. 實例化一個新的聊天服務器


					
myServer = ChatServer( 2626 )
myServer.run()

 

現在,這個服務器已經在運行了,您可以從一個或多個客戶機連接到這個服務器上。也可以將幾個方法串接在一起來簡化這個過程(如果需要簡化的話):


清單 21. 串接幾個方法


					
myServer = ChatServer( 2626 ).run()

 

這可以實現相同的結果。下面我們將展示 ChatServer 類的用法。

 

 

展示 ChatServer

下面就是 ChatServer 的用法。我們將展示 ChatServer 的輸出結果(請參閱清單 22 )以及兩個客戶機之間的對話(請參閱清單 23 和 清單 24)。用戶輸入的文本以黑體形式表示。


清單 22. ChatServer 的輸出


					
[plato]$ python pchatsrvr.py ChatServer started on port 2626 Client joined 127.0.0.1:37993 Client joined 127.0.0.1:37994 [127.0.0.1:37994] Hello, is anyone there? [127.0.0.1:37993] Yes, I'm here. [127.0.0.1:37993] Client left 127.0.0.1:37993 



清單 23. 聊天客戶機 #1 的輸出


					
[plato]$ telnet localhost 2626 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. You're connected to the Python chatserver Client joined 127.0.0.1:37994 [127.0.0.1:37994] Hello, is anyone there? Yes, I'm here. ^] telnet> close Connection closed. [plato]$ 



清單 24. 聊天客戶機 #2 的輸出


					
[plato]$ telnet localhost 2626 Trying 127.0.0.1... Connected to localhost.localdomain (127.0.0.1). Escape character is '^]'. You're connected to the Python chatserver Hello, is anyone there? [127.0.0.1:37993] Yes, I'm here. [127.0.0.1:37993] Client left 127.0.0.1:37993 

 

正如您在清單 22 中看到的那樣,所有客戶機之間的對話都會顯示到 stdout 上,包括客戶機的連接和斷開消息。

總結如下:

客戶端使用linux 在發送消息的時候,在windows7上面顯示的信息是正確的,而windows 客戶端顯示的信息是在linux客戶端上面是一個字符一個字符顯示的.這個覺得跟windows7 使用的winsock有關?這個待驗證

此文章為轉載!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM