構建一個 Python 聊天服務器python
如今您已經瞭解了 Python 中基本的網絡 API;接下來能夠在一個簡單的應用程序中應用這些知識了。在本節中,將構建一個簡單的聊天服務器。使用 Telnet,客戶機能夠鏈接到 Python 聊天服務器上,並在全球範圍內相互進行通訊。提交到聊天服務器的消息能夠由其餘人進行查看(以及一些管理信息,例如客戶機加入或離開聊天服務器)。這個模型如圖 1 所示。windows
圖 1. 聊天服務器使用 select 方法來支持任意多個客戶機服務器
聊天服務器的一個重要需求是必須能夠伸縮。服務器必須可以支持任意個流(TCP)客戶機。網絡
要支持任意個客戶機,可使用 select
方法來異步地管理客戶機的列表。不過也可使用服務器 socket 的 select
特性。select
的讀事件決定了一個客戶機什麼時候有可讀數據,並且它也能夠用來判斷什麼時候有一個新客戶機要鏈接服務器 socket 了。能夠利用這種行爲來簡化服務器的開發。app
接下來,咱們將展現聊天服務器的 Python 源代碼,並說明 Python 怎樣幫助簡化這種實現。dom
讓咱們首先了解一下 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 上打印一條消息,說明這個服務器已經被啓動了。
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。
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 ) |
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, |
如今您已經看到了 Python 聊天服務器(這隻使用了不到 50 行的代碼),如今讓咱們看一下如何在 Python 中實例化一個新的聊天服務器。
咱們經過建立一個新的 ChatServer
對象來啓動一個服務器(傳遞要使用的端口號),而後調用 run
方法來啓動服務器並容許接收全部到達的鏈接:
myServer = ChatServer( 2626 ) myServer.run() |
如今,這個服務器已經在運行了,您能夠從一個或多個客戶機鏈接到這個服務器上。也能夠將幾個方法串接在一塊兒來簡化這個過程(若是須要簡化的話):
myServer = ChatServer( 2626 ).run() |
這能夠實現相同的結果。下面咱們將展現 ChatServer
類的用法。
下面就是 ChatServer
的用法。咱們將展現 ChatServer
的輸出結果(請參閱清單 22 )以及兩個客戶機之間的對話(請參閱清單 23 和 清單 24)。用戶輸入的文本以黑體形式表示。
[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 |
[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]$ |
[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有關?這個待驗證
此文章爲轉載!