WEB静态服务器
epoll版的http服务器
什么是epoll ?
epoll
是什么?按照Linux中 "man"
手册的说法:是为处理大批量句柄而作了改进的poll
。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
。
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法
。
epoll的原理过程
epoll
同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术
,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式
。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
IO 多路复用
就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
代码示例
import socket
import re
import select
def service_client(new_socket, request):
"""为这个客户端返回数据"""
# # 1、接收浏览器发送过来的请求,即http请求
# # GET / HTTP/1.1
# # ......
# global response
# request = new_socket.recv(1024).decode("utf-8")
# # print(">>" * 50)
# # print(request)
request_lines = request.splitlines()
print("")
print(">"*50)
print(request_lines)
# GET /index.html HTTP/1.1
# get post put del 正则1:r"[^/]+(/[^ ]*)" 正则2:r".*(/.*?\..*?) HTTP.*"
file_name = ""
req_str = re.search(r"[?/html](/.*?\..*?) HTTP.*", request_lines[0]) # 匹配第一个元素的 ‘HTTP’ 之前的文件名
print(req_str)
if req_str:
file_name = req_str.group(1)
print("*"*50, file_name)
if file_name == "/":
file_name = "/html/index.html"
# 2、返回 http格式的数据,给浏览器
try:
file = open("./html" + file_name, "rb")
except IOError:
# 404表示没有这个页面
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "------FILE NOT FOUND------"
new_socket.send(response.encode("utf-8"))
else:
html_content = file.read()
file.close()
response_body = html_content
# 2.1、准备发送给浏览器的数据---header
response_header = "HTTP/1.1 200 OK\r\n"
response_header += "Content-Length:%d\r\n" % len(html_content) # 长连接
response_header += "\r\n"
response = response_header.encode("utf-8") + response_body # 二进制
# 2.2、准备发送给浏览器的数据---body
# 将response header 发送给浏览器
new_socket.send(response)
# 关闭套接字
# new_socket.close()
def main():
"""作为程序的主控制入口"""
# 1、创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2、绑定
tcp_server_socket.bind(("", 12708))
# 3、变为监听套接字
tcp_server_socket.listen(128)
tcp_server_socket.setblocking(False) # 将套接字变为非堵塞
# 创建一个 epoll 对象
epl = select.epoll()
# 将监听套接字对应的fd注册到epoll中进行监听
epl.register(tcp_server_socket.fileno(), select.EPOLLIN) # 套接字.fileno() : 得到套接字的文件描述符
fd_event_dict = dict()
while True:
fd_event_list = epl.poll() # 默认会堵塞,直到 os 监测到数据的到来,通过事件通知方式 告诉这个程序,此时才会解堵塞
# [(fd, event),(套接字对应的文件描述符,这个文件描述符到底是什么事件 例如 可以调用 recv 接收等)]
for fd, event in fd_event_list:
# 等待新客户端 的链接
if fd == tcp_server_socket.fileno():
new_socket, client_addr = tcp_server_socket.accept()
epl.register(new_socket.fileno(), select.EPOLLIN)
fd_event_dict[new_socket.fileno()] = new_socket
elif event == select.EPOLLIN:
# 判断已经链接的客户端十个缶有数据发送过来
recv_data = fd_event_dict[fd].recv(1024).decode("utf-8")
if recv_data:
service_client(fd_event_dict[fd], recv_data)
else:
fd_event_dict[fd].close()
epl.unregister(fd)
del fd_event_dict[fd]
# 关闭套接字
tcp_server_socket.close()
if __name__ == "__main__":
main()
小总结
I/O 多路复用的特点:
通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,epoll()函数就可以返回。 所以, IO多路复用,本质上不会有并发的功能,因为任何时候还是只有一个进程或线程进行工作,它之所以能提高效率是因为select\epoll 把进来的socket放到他们的 ‘监视’ 列表里面,当任何socket有可读可写数据立马处理,那如果select\epoll 手里同时检测着很多socket, 一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理高效率。
当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换页会耗掉更多的系统资源。 所以我们可以结合IO多路复用和多进程/多线程 来高性能并发,IO复用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑。
参考资料
如果想了解下epoll在Linux中的实现过程可以参考:http://blog.csdn.net/xiajun07061225/article/details/9250579
关于module ‘select’ has no attribute ‘epoll’,详见下一篇博文。
链接:https://blog.csdn.net/weixin_42250835/article/details/89573354