第十八篇:简易版web服务器开发
在上篇有实现了一个静态的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()
多进程-简易版web服务器
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()
多线程-简易版web服务器
从上面的程序中,我们每接收到一个客户端的连接便创建一个线程或者进程来执行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、随后检测通信套接字是否有反应,接收请求消息;