Part 11.網絡編程--解決併發服務器的幾種方法

(一)單進程服務器數組

 1 from socket import *
 2 
 3 serSocket = socket(AF_INET, SOCK_STREAM)
 4 
 5 # 重複使用綁定的信息,當咱們服務器先掛掉時,不會影響客戶端的操做
 6 serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
 7 
 8 localAddr = ('', 7788)
 9 
10 serSocket.bind(localAddr)
11 
12 serSocket.listen(5)
13 
14 while True:
15 
16     print('-----主進程,,等待新客戶端的到來------')
17 
18     newSocket,destAddr = serSocket.accept()
19 
20     print('-----主進程,,接下來負責數據處理[%s]-----'%str(destAddr))
21 
22     try:
23         while True:
24             recvData = newSocket.recv(1024)
25             if len(recvData)>0:
26                 print('recv[%s]:%s'%(str(destAddr), recvData))
27             else:
28                 print('[%s]客戶端已經關閉'%str(destAddr))
29                 break
30     finally:
31         newSocket.close()
32 
33 serSocket.close()

總結

  • 同一時刻只能爲一個客戶進行服務,不能同時爲多個客戶服務
  • 相似於找一個「明星」簽字同樣,客戶須要耐心等待才能夠獲取到服務
  • 當服務器爲一個客戶端服務時,而另外的客戶端發起了connect,只要服務器listen的隊列有空閒的位置,就會爲這個新客戶端進行鏈接,而且客戶端能夠發送數據,但當服務器爲這個新客戶端服務時,可能一次性把全部數據接收完畢
  • 當recv接收數據時,返回值爲空,即沒有返回數據,那麼意味着客戶端已經調用了close關閉了;所以服務器經過判斷recv接收數據是否爲空來判斷客戶端是否已經下線。

 

(二)多進程服務器服務器

 1 from socket import *
 2 from multiprocessing import *
 3 from time import sleep
 4 
 5 # 處理客戶端的請求併爲其服務
 6 def dealWithClient(newSocket,destAddr):
 7     while True:
 8         recvData = newSocket.recv(1024)
 9         if len(recvData)>0:
10             print('recv[%s]:%s'%(str(destAddr), recvData))
11         else:
12             print('[%s]客戶端已經關閉'%str(destAddr))
13             break
14 
15     newSocket.close()
16 
17 
18 def main():
19 
20     serSocket = socket(AF_INET, SOCK_STREAM)
21     serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
22     localAddr = ('', 7788)
23     serSocket.bind(localAddr)
24     serSocket.listen(5)
25 
26     try:
27         while True:
28             print('-----主進程,,等待新客戶端的到來------')
29             newSocket,destAddr = serSocket.accept()
30 
31             print('-----主進程,,接下來建立一個新的進程負責數據處理[%s]-----'%str(destAddr))
32             client = Process(target=dealWithClient, args=(newSocket,destAddr))
33             client.start()
34 
35             #由於已經向子進程中copy了一份(引用),而且父進程中這個套接字也沒有用處了
36             #因此關閉
37             newSocket.close()
38     finally:
39         #當爲全部的客戶端服務完以後再進行關閉,表示再也不接收新的客戶端的連接
40         serSocket.close()
41 
42 if __name__ == '__main__':
43     main()
View Code

總結

  • 經過爲每一個客戶端建立一個進程的方式,可以同時爲多個客戶端進行服務
  • 當客戶端不是特別多的時候,這種方式還行,若是有幾百上千個,就不可取了,由於每次建立進程等過程須要好較大的資源

 

(三)多線程服務器網絡

 1 #coding=utf-8
 2 from socket import *
 3 from threading import Thread
 4 from time import sleep
 5 
 6 # 處理客戶端的請求並執行事情
 7 def dealWithClient(newSocket,destAddr):
 8     while True:
 9         recvData = newSocket.recv(1024)
10         if len(recvData)>0:
11             print('recv[%s]:%s'%(str(destAddr), recvData))
12         else:
13             print('[%s]客戶端已經關閉'%str(destAddr))
14             break
15 
16     newSocket.close()
17 
18 
19 def main():
20 
21     serSocket = socket(AF_INET, SOCK_STREAM)
22     serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
23     localAddr = ('', 7788)
24     serSocket.bind(localAddr)
25     serSocket.listen(5)
26 
27     try:
28         while True:
29             print('-----主進程,,等待新客戶端的到來------')
30             newSocket,destAddr = serSocket.accept()
31 
32             print('-----主進程,,接下來建立一個新的進程負責數據處理[%s]-----'%str(destAddr))
33             client = Thread(target=dealWithClient, args=(newSocket,destAddr))
34             client.start()
35 
36             #由於線程中共享這個套接字,若是關閉了會致使這個套接字不可用,
37             #可是此時在線程中這個套接字可能還在收數據,所以不能關閉
38             #newSocket.close() 
39     finally:
40         serSocket.close()
41 
42 if __name__ == '__main__':
43     main()
View Code

總結

  • 經過爲每一個客戶端建立一個線程的方式,可以同時爲多個客戶端進行服務
  • 而且這種方式比進程佔用的資源少

 

(四)單進程服務器---非堵塞模式多線程

 1 from socket import *
 2 import time
 3 
 4 # 用來存儲全部的新連接的socket
 5 g_socketList = []
 6 
 7 def main():
 8     serSocket = socket(AF_INET, SOCK_STREAM)
 9     serSocket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
10     localAddr = ('', 7788)
11     serSocket.bind(localAddr)
12     #能夠適當修改listen中的值來看看不一樣的現象
13     serSocket.listen(1000)
14     #將套接字設置爲非堵塞
15     #設置爲非堵塞後,若是accept時,恰巧沒有客戶端connect,那麼accept會
16     #產生一個異常,因此須要try來進行處理
17     serSocket.setblocking(False)
18 
19     while True:
20 
21         #用來測試
22         #time.sleep(0.5)
23 
24         try:
25             newClientInfo = serSocket.accept()
26         except Exception as result:
27             pass
28         else:
29             print("一個新的客戶端到來:%s"%str(newClientInfo))
30             newClientInfo[0].setblocking(False)
31             g_socketList.append(newClientInfo)
32 
33         # 用來存儲須要刪除的客戶端信息
34         needDelClientInfoList = []
35 
36         for clientSocket,clientAddr in g_socketList:
37             try:
38                 recvData = clientSocket.recv(1024)
39                 if len(recvData)>0:
40                     print('recv[%s]:%s'%(str(clientAddr), recvData))
41                 else:
42                     print('[%s]客戶端已經關閉'%str(clientAddr))
43                     clientSocket.close()
44                     g_needDelClientInfoList.append((clientSocket,clientAddr))
45             except Exception as result:
46                 pass
47 
48         for needDelClientInfo in needDelClientInfoList:
49             g_socketList.remove(needDelClientInfo)
50 
51 if __name__ == '__main__':
52     main()
View Code

 

(五)單進程服務器---select版併發

1. select 原理

在多路複用的模型中,比較經常使用的有select模型和epoll模型。這兩個都是系統接口,由操做系統提供。固然,Python的select模塊進行了更高級的封裝。app

網絡通訊被Unix系統抽象爲文件的讀寫,一般是一個設備,由設備驅動程序提供,驅動能夠知道自身的數據是否可用。支持阻塞操做的設備驅動一般會實現一組自身的等待隊列,如讀/寫等待隊列用於支持上層(用戶層)所需的block或non-block操做。設備的文件的資源若是可用(可讀或者可寫)則會通知進程,反之則會讓進程睡眠,等到數據到來可用的時候,再喚醒進程。socket

這些設備的文件描述符被放在一個數組中,而後select調用的時候遍歷這個數組,若是對應的文件描述符可讀則會返回改文件描述符。當遍歷結束以後,若是仍然沒有一個可用設備文件描述符,select讓用戶進程則會睡眠,直到等待資源可用的時候在喚醒,遍歷以前那個監視的數組。每次遍歷都是依次進行判斷的。ide

 1 import select
 2 import socket
 3 import sys
 4 
 5 
 6 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 7 server.bind(('', 7788))
 8 server.listen(5)
 9 
10 inputs = [server, sys.stdin]
11 
12 running = True
13 
14 while True:
15 
16     # 調用 select 函數,阻塞等待
17     readable, writeable, exceptional = select.select(inputs, [], [])
18 
19     # 數據抵達,循環
20     for sock in readable:
21 
22         # 監聽到有新的鏈接
23         if sock == server:
24             conn, addr = server.accept()
25             # select 監聽的socket
26             inputs.append(conn)
27 
28         # 監聽到鍵盤有輸入
29         elif sock == sys.stdin:
30             cmd = sys.stdin.readline()
31             running = False
32             break
33 
34         # 有數據到達
35         else:
36             # 讀取客戶端鏈接發送的數據
37             data = sock.recv(1024)
38             if data:
39                 sock.send(data)
40             else:
41                 # 移除select監聽的socket
42                 inputs.remove(sock)
43                 sock.close()
44 
45     # 若是檢測到用戶輸入敲擊鍵盤,那麼就退出
46     if not running:
47         break
48 
49 server.close()
View Code

總結:

優勢

select目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢。函數

缺點

select的一個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制,可是這樣也會形成效率的下降。測試

通常來講這個數目和系統內存關係很大,具體數目能夠cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.

對socket進行掃描時是依次掃描的,即採用輪詢的方法,效率較低。

當套接字比較多的時候,每次select()都要經過遍歷FD_SETSIZE個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間。

 

(六)單進程服務器---epoll版

1. epoll的優勢:

  1. 沒有最大併發鏈接的限制,能打開的FD(指的是文件描述符,通俗的理解就是套接字對應的數字編號)的上限遠大於1024
  2. 效率提高,不是輪詢的方式,不會隨着FD數目的增長效率降低。只有活躍可用的FD纔會調用callback函數;即epoll最大的優勢就在於它只管你「活躍」的鏈接,而跟鏈接總數無關,所以在實際的網絡環境中,epoll的效率就會遠遠高於select和poll。

2.說明:

  • EPOLLIN (可讀)
  • EPOLLOUT (可寫)
  • EPOLLET (ET模式)

epoll對文件描述符的操做有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別以下:

LT模式:當epoll檢測到描述符事件發生並將此事件通知應用程序,應用程序能夠不當即處理該事件。下次調用epoll時,會再次響應應用程序並通知此事件。 ET模式:當epoll檢測到描述符事件發生並將此事件通知應用程序,應用程序必須當即處理該事件。若是不處理,下次調用epoll時,不會再次響應應用程序並通知此事件。
 1 import socket
 2 import select
 3 
 4 # 建立套接字
 5 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 6 
 7 # 設置能夠重複使用綁定的信息
 8 s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
 9 
10 # 綁定本機信息
11 s.bind(("",7788))
12 
13 # 變爲被動
14 s.listen(10)
15 
16 # 建立一個epoll對象
17 epoll=select.epoll()
18 
19 # 測試,用來打印套接字對應的文件描述符
20 # print s.fileno()
21 # print select.EPOLLIN|select.EPOLLET
22 
23 # 註冊事件到epoll中
24 # epoll.register(fd[, eventmask])
25 # 注意,若是fd已經註冊過,則會發生異常
26 # 將建立的套接字添加到epoll的事件監聽中
27 epoll.register(s.fileno(),select.EPOLLIN|select.EPOLLET)
28 
29 
30 connections = {}
31 addresses = {}
32 
33 # 循環等待客戶端的到來或者對方發送數據
34 while True:
35 
36     # epoll 進行 fd 掃描的地方 -- 未指定超時時間則爲阻塞等待
37     epoll_list=epoll.poll()
38 
39     # 對事件進行判斷
40     for fd,events in epoll_list:
41 
42         # print fd
43         # print events
44 
45         # 若是是socket建立的套接字被激活
46         if fd == s.fileno():
47             conn,addr=s.accept()
48 
49             print('有新的客戶端到來%s'%str(addr))
50 
51             # 將 conn 和 addr 信息分別保存起來
52             connections[conn.fileno()] = conn
53             addresses[conn.fileno()] = addr
54 
55             # 向 epoll 中註冊 鏈接 socket 的 可讀 事件
56             epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
57 
58 
59         elif events == select.EPOLLIN:
60             # 從激活 fd 上接收
61             recvData = connections[fd].recv(1024)
62 
63             if len(recvData)>0:
64                 print('recv:%s'%recvData)
65             else:
66                 # 從 epoll 中移除該 鏈接 fd
67                 epoll.unregister(fd)
68 
69                 # server 側主動關閉該 鏈接 fd
70                 connections[fd].close()
71 
72                 print("%s---offline---"%str(addresses[fd]))
View Code
相關文章
相關標籤/搜索