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

構建一個 Python 聊天服務器python

一個簡單的聊天服務器linux

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


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

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

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

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

 

ChatServer 類異步

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

這個類由 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有關?這個待驗證

此文章爲轉載!

相關文章
相關標籤/搜索