在上篇有實現了一個靜態的web服務器,可以接收web瀏覽器的請求,隨后對請求消息進行解析,獲取客戶想要文件的文件名,隨后根據文件名返回響應消息;那么這篇我們對該web服務器進行改善,通過多任務、非阻塞以及epoll(重點)的方式來對該服務器進行改善。
01-多任務-阻塞式-簡易版web服務器開發
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import socket 5 import re 6 7 def server_client(new_socket): 8 """服務客戶端""" 9 # 接收客戶端請求消息,並對接收的消息進行解析--> GET /index.html HTTP/1.1 10 request = new_socket.recv(1024).decode("utf-8") 11 request_list = request.splitlines() # 對消息進行解析,並且返回列表 12 print(request_list) 13 filename = re.match(r"[^/]+(/[^ ]*)",request_list[0]).group(1) 14 print(filename) 15 16 # 發送響應消息 17 # 指定只輸入域名時,默認響應的網頁 18 if filename == "/": 19 filename = "/index.html" 20 21 # 嘗試打開匹配的文件,若該服務器內沒有該文件,則發送狀態碼404,若有則響應的內容 22 try: 23 f = open("html"+ filename,"rb") 24 except Exception as e: 25 print(e) 26 respone_header = "HTTP/1.1 404 File No Found\r\n" 27 respone_header += "\r\n" 28 respone_body = "<h1>No Found File</h1>" 29 respone = respone_header + respone_body 30 respone = respone.encode("utf-8") 31 else: 32 respone_body = f.read() 33 f.close() 34 respone_header = "HTTP/1.1 200 OK \r\n" 35 respone_header += "\r\n" 36 respone = respone_header.encode("utf-8") + respone_body 37 finally: 38 new_socket.send(respone) 39 new_socket.close() 40 41 42 def main(): 43 server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 44 server_socket.bind(("",6969)) 45 server_socket.listen(128) 46 47 # 通過while循環,可以接收多個客戶端的連接 48 while True: 49 new_socket,client_addr = server_socket.accept() 50 server_client(new_socket) 51 52 server_socket.close() 53 54 if __name__ == "__main__": 55 main()
以上就單任務-阻塞式-簡易版的web服務器,即整個程序只有一條主進程主線程,且需要等待客戶端的接入、等待接收客戶端的發送過來的消息等,而在這個等待的過程中,整段程序均是阻塞的轉態。那么接下來我們就這兩個問題來進行優化;
02-多任務-阻塞式-簡易版web服務器開發

1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import socket 5 import re 6 import multiprocessing 7 8 def serve_client(new_socket): 9 """服務客戶端""" 10 # 接收客戶端請求消息 GET /index.html HTTP/1.1 11 request = new_socket.recv(1024).decode("utf-8") 12 request_list = request.splitlines() 13 print(request_list) 14 filename = re.match(r"[^/]+(/[^ ]*)",request_list[0]).group(1) 15 print(filename) 16 17 # 發送響應消息 18 if filename == "/": 19 filename = "/index.html" 20 try: 21 f = open("html"+ filename,"rb") 22 except Exception as e: 23 print(e) 24 respone_header = "HTTP/1.1 404 File No Found\r\n" 25 respone_header += "\r\n" 26 respone_body = "<h1>No Found File</h1>" 27 respone = respone_header + respone_body 28 respone = respone.encode("utf-8") 29 else: 30 respone_body = f.read() 31 f.close() 32 respone_header = "HTTP/1.1 200 OK \r\n" 33 respone_header += "\r\n" 34 respone = respone_header.encode("utf-8") + respone_body 35 finally: 36 new_socket.send(respone) 37 new_socket.close() 38 39 40 def main(): 41 server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 42 server_socket.bind(("",6969)) 43 server_socket.listen(128) 44 45 while True: 46 new_socket,client_addr = server_socket.accept() 47 # 允許立即使用上次綁定的port 48 self.listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 49 # 每當有一個客戶端接入,創建一個進程為其服務 50 p = multiprocessing.Process(target=serve_client,args=(new_socket,)) 51 p.start() 52 p.join() 53 54 server_socket.close() 55 56 if __name__ == "__main__": 57 main()

1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import socket 5 import re 6 import threading 7 8 def serve_client(new_socket): 9 """服務客戶端""" 10 11 # 接收客戶端請求消息 GET /index.html HTTP/1.1 12 request = new_socket.recv(1024).decode("utf-8") 13 request_list = request.splitlines() 14 print(request_list) 15 filename = re.match(r"[^/]+(/[^ ]*)",request_list[0]).group(1) 16 print(filename) 17 18 # 發送響應消息 19 if filename == "/": 20 filename = "/index.html" 21 try: 22 f = open("html"+ filename,"rb") 23 except Exception as e: 24 print(e) 25 respone_header = "HTTP/1.1 404 File No Found\r\n" 26 respone_header += "\r\n" 27 respone_body = "<h1>No Found File</h1>" 28 respone = respone_header + respone_body 29 respone = respone.encode("utf-8") 30 else: 31 respone_body = f.read() 32 f.close() 33 respone_header = "HTTP/1.1 200 OK \r\n" 34 respone_header += "\r\n" 35 respone = respone_header.encode("utf-8") + respone_body 36 finally: 37 new_socket.send(respone) 38 new_socket.close() 39 40 41 def main(): 42 """主函數""" 43 server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 44 server_socket.bind(("",6969)) 45 server_socket.listen(128) 46 47 while True: 48 new_socket,client_addr = server_socket.accept() 49 50 # 每接收到一個客戶端的連接,便創建一個線程執行該工作函數 51 t = threading.Thread(target=serve_client,args=(new_socket,)) 52 t.start() 53 t.join() 54 55 server_socket.close() 56 57 if __name__ == "__main__": 58 main()
從上面的程序中,我們每接收到一個客戶端的連接便創建一個線程或者進程來執行serve_client函數,即為該客戶端服務---接收請求消息或者發送響應消息;
創建多任務的好處就是:相當於給原本只有一個窗口的銀行再多開了窗口,不需要再等待上一個客戶結束了服務才能服務下一個 客戶,故 可以一次接受多個客戶端的連接,並且一次服務多個客戶端,每次服務客戶端的過程互不影響。
03-單任務-非阻塞式-簡易版web服務器開發
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 4 import time 5 import socket 6 import sys 7 import re 8 9 10 class WSGIServer(object): 11 """定義一個WSGI服務器的類""" 12 13 def __init__(self, documents_root): 14 15 # 1. 創建套接字 16 self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 # 2. 綁定本地信息 18 self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 19 self.server_socket.bind(("", 6868)) 20 # 3. 變為監聽套接字 21 self.server_socket.listen(128) 22 23 # 設置套接字為非阻塞式,即遇到需要等待阻塞的即拋出異常 24 self.server_socket.setblocking(False) 25 26 # 創建一個列表用來存儲,服務端沒接收到一個客戶端連接時創建的通信套接字(引用) 27 self.client_socket_list = list() 28 29 self.documents_root = documents_root 30 31 def run_forever(self): 32 """運行服務器""" 33 34 # 等待對方鏈接 35 while True: 36 37 # time.sleep(0.5) # for test 38 39 # 判斷有沒有客戶端連接 40 try: 41 new_socket, new_addr = self.server_socket.accept() 42 except Exception as ret: 43 print("-----1----", ret) # for test 44 else: 45 new_socket.setblocking(False) 46 self.client_socket_list.append(new_socket) 47 48 # 判斷已連接的客戶端 有沒有發送消息過來 49 for client_socket in self.client_socket_list: 50 try: 51 request = client_socket.recv(1024).decode('utf-8') 52 except Exception as ret: 53 print("------2----", ret) # for test 54 else: 55 if request: 56 self.deal_with_request(request, client_socket) 57 else: 58 client_socket.close() 59 self.client_socket_list.remove(client_socket) 60 61 print(self.client_socket_list) 62 63 64 def deal_with_request(self, request, client_socket): 65 """為這個瀏覽器服務器""" 66 if not request: 67 return 68 69 request_lines = request.splitlines() 70 for i, line in enumerate(request_lines): 71 print(i, line) 72 73 # 提取請求的文件(index.html) 74 # GET /a/b/c/d/e/index.html HTTP/1.1 75 ret = re.match(r"[^/]*([^ ]+)", request_lines[0]) 76 if ret: 77 print("正則提取數據:", ret.group(1)) 78 file_name = ret.group(1) 79 # 當客戶只輸入域名時,設置默認瀏覽網頁 80 if file_name == "/": 81 file_name = "/index.html" 82 83 84 # 讀取文件數據 85 try: 86 f = open(self.documents_root+file_name, "rb") 87 except: 88 response_body = "file not found, 請輸入正確的url" 89 response_header = "HTTP/1.1 404 not found\r\n" 90 response_header += "Content-Type: text/html; charset=utf-8\r\n" 91 response_header += "Content-Length: %d\r\n" % (len(response_body)) 92 response_header += "\r\n" 93 94 # 將header返回給瀏覽器 95 client_socket.send(response_header.encode('utf-8')) 96 97 # 將body返回給瀏覽器 98 client_socket.send(response_body.encode("utf-8")) 99 else: 100 content = f.read() 101 f.close() 102 103 response_body = content 104 response_header = "HTTP/1.1 200 OK\r\n" 105 response_header += "Content-Length: %d\r\n" % (len(response_body)) 106 response_header += "\r\n" 107 108 # 將header返回給瀏覽器 109 client_socket.send( response_header.encode('utf-8') + response_body) 110 111 112 # 設置服務器服務靜態資源時的路徑 113 DOCUMENTS_ROOT = "./html" 114 115 116 def main(): 117 """控制web服務器整體""" 118 119 http_server = WSGIServer(DOCUMENTS_ROOT) 120 http_server.run_forever() 121 122 123 if __name__ == "__main__": 124 main()
在上面這個demo里面,由於如果用面向過程編程進行實現則會顯得比較雜亂,這里我們用面向過程進行了封裝;該類WSGIServer,定義了一個初始化__init__函數(用來執行類中的主程序)、run_forever函數(用來運行服務器,判斷是否有客戶端接入、是否有客戶端發送消息過來)、以及deal_with_request()函數(用來接收並處理客戶端的請求消息、並且發送響應消息。)而這個demo想對於第一個的優勢在於:
這個demo使得服務器不斷的在運行,檢測是否有客戶端接入、檢測是否已連接的客戶端是否有請求消息發送過來,不會因為沒有客戶端鏈接進來或者客戶端沒有發送消息過來而使得整段程序阻塞在此處而使得其他的客戶端無法連接或者無法接收消息。
但是這種方法的缺點也特別明顯:
通過不斷循環的方式判斷是有客戶端接入和是否有請求消息發送過來,這種方式服務器處於不停的運作狀態,占用CPU的資源十分龐大。
1、列表越長遍歷所用時間越長
2、且將列表數據從用戶態拷貝到內核態需花費不少的時間,即挨個的問---輪詢
04 -epoll-簡易版web服務器
在看這個demo之前我們先對epoll進行了解。優勢:select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。epoll相對於上面demo優化之處,這是它是通過兩種機制實現的:
共享內存空間:
在創建一個epoll對象相當於在操作系統中開辟一個用戶態和內核態中間的共用特殊內存,epoll對象則相當於這個內存空間的管家,而該內存空間存儲着套接字的文件描述符fd和事件,便不需花很多的時間去遍歷,不用去copy該列表中的值,而在該內存空間中無論是用戶程序還是內核在處理數據時均可以直接調用。
事件通知:
上面那個demo采用的是輪詢的方式,即對每個套接字進行詢問,是否有客戶端連接進來、是否有請求消息進來等,這樣將會消耗大量的時間,而epoll采用的是事件通知,即若某個套接字有時間發生,則通知內核態去對該套接字進行操作。
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 4 5 import socket 6 import re 7 import select 8 9 def server_client(new_socket,request): 10 """服務客戶端""" 11 # 接收客戶端請求消息 GET /index.html HTTP/1.1 12 request_list = request.splitlines() 13 print(request_list) 14 filename = re.match(r"[^/]+(/[^ ]*)",request_list[0]).group(1) 15 print(filename) 16 17 # 發送響應消息 18 19 if filename == "/": 20 filename = "/index.html" 21 try: 22 f = open("html"+ filename,"rb") 23 except Exception as e: 24 print(e) 25 respone_header = "HTTP/1.1 404 File No Found\r\n" 26 respone_header += "\r\n" 27 respone_body = "<h1>No Found File</h1>" 28 respone = respone_header + respone_body 29 respone = respone.encode("utf-8") 30 else: 31 respone_body = f.read() 32 f.close() 33 respone_header = "HTTP/1.1 200 OK \r\n" 34 respone_header += "\r\n" 35 respone = respone_header.encode("utf-8") + respone_body 36 finally: 37 new_socket.send(respone) 38 new_socket.close() 39 40 41 def main(): 42 server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 43 server_socket.bind(("",6969)) 44 server_socket.listen(128) 45 46 # 1、創建epoll對象 47 epl = select.epoll() 48 # 2、將監聽套接字注冊到epoll對象管理的內存空間 49 epl.register(server_socket.fileno(),select.EPOLLIN) 50 # 3、由於epoll管理的內存空間處於用戶程序和操作系統之間,故需要fd和socket來回轉換,故創建一個字典 51 52 # 使用epoll來監聽是否有客戶端連接、是否客戶端有請求消息發送過來 53 # 4、創建文件描述符fd:套接字引用的字典,--》由於操作系統有用戶態(只能處理socket套接字)和內核態(只能處理文件描述符fd) 54 fd_socket ={ } 55 56 while True: 57 # 創建一個列表,存儲有事件發生的套接字 58 fd_event_list = epl.poll() 59 for fd,event in fd_event_list: 60 # 5、判斷是否為監聽套接字發生變化 61 if fd == server_socket.fileno(): 62 new_socket, client_addr = server_socket.accept() 63 epl.register(new_socket.fileno(),select.EPOLLIN) 64 fd_socket[new_socket.fileno()] = new_socket 65 # 6、否則為通信套接字有事件發送 66 else: 67 request = fd_socket[fd].recv(1024).decode("utf-8") 68 # 如果接收的請求消息不為空即客戶端沒有斷開連接 69 if request: 70 server_client(fd_socket[fd],request) 71 # 否則客戶端斷開連接: 72 else: 73 fd_socket[fd].close() 74 epl.unregister(fd_socket[fd]) 75 del fd_socket[fd] 76 77 # server_client(new_socket) 78 79 server_socket.close() 80 81 if __name__ == "__main__": 82 main()
解析:
1、創建epoll對象:相當於在操作系統中開辟一個用戶態和內核態中間的共用特殊內存,而管理員就是epoll對象;
2、將監聽套接字注冊到epoll對象:相當於將監聽套接字的文件描述符和事件添加到epoll對象管理的特殊內存空間,該內存空間無論用戶程序還是內核均可操作。
3、創建一個fd_socket字典:即由於需對套接字進行操作,而此時只有文件描述符fd,而兩者是一一映射的關系,故可以通過fd獲取對應的socket套接字;
4、通過不斷循環,查看監聽和通信套接字是否有事件發生;
5、首先判斷監聽套接字是否有反應,若有則創建新的套接字去服務客戶端,同時將新的套接字注冊到epoll對象;並且將其與文件描述符fd映射關系添加到fd_socket字典中的;
6、隨后檢測通信套接字是否有反應,接收請求消息;