[Python之路] 使用epoll实现高并发HTTP服务器

摘要:
什么是epoll?在以各种方式在Python中实现并发WebServer的最后,我们使用单进程+单线程+无阻塞+长连接来实现一个可以并发处理客户端连接的服务器。在epoll中,套接字列表位于一个特殊的内存空间中,由应用程序和内核共享。

什么是epoll

我们在  Python多种方式实现并发的Web Server 的最后使用单进程+单线程+非阻塞+长连接实现了一个可并发处理客户端连接的服务器。他的原理可以用以下的图来描述:

[Python之路] 使用epoll实现高并发HTTP服务器第1张

解释:

1.HTTP服务器是我们使用 单进程+单线程+非阻塞+长连接实现 的web服务器。

2.在实现的时候,我们创建了一个存放已接受Socket连接的列表,该列表是在应用程序的内存空间中的。如图中深蓝色部分

3.当有3个客户端接入的时候,列表中一共存在3个对应的socket句柄,分别对应三个小黄框。

4.灰色小框代表服务器接收请求的socket。

5.我们在进行无限循环的时候,首先是检查是否有新的客户端接入,相当于检查灰色小框是否有数据到达。然后轮询3个小黄框对应socket是否有数据到达。轮询的效率是很低的。

6.服务器在使用accept和recv时,实际上是委托操作系统帮他检查是否有数据到达,由于这个列表的socket都处于用户内存空间,所以需要将其复制到内核空间。操作系统检查完毕后,如果有数据就返回数据给应用程序,如果没有数据就以异常的方式通知应用程序。而且不光这样,操作系统可能还同时在运行其他的应用程序,这样效率会非常低。

我们再来看epoll的图:

[Python之路] 使用epoll实现高并发HTTP服务器第2张

解释:

1.我们可以看到,在结构上,最大的区别在于,存放socket的列表不处于应用程序内部。在epoll中,这个存放socket的列表处于一个特殊的内存空间,这个内存空间是应用程序与内核共享的空间。也就是说,当应用程序委托操作系统检查是否有数据到达时,无需将复制数据给内核空间,操作系统可以直接进行检查。

2.操作系统检查到某个socket有数据到达,使用事件通知的形式,直接告诉应用程序,而不是以轮询的方式。打个比方,一个厨师挨个问50个人饿了没,如果饿了就给他东西吃,这是轮询。而50个人中,谁饿了谁举手,厨师就给吃的,这叫事件通知。很明显,事件通知的效率会特别高。

实现代码:

import socket

import re
import select


def handle_request(new_socket, recv_msg):
    # 从请求中解析出URI
    recv_lines = recv_msg.splitlines()

    # 使用正则表达式提取出URI
    ret = re.match(r"[^/]+(/[^ ]*)", recv_lines[0])

    if ret:
        # 获取URI字符串
        file_name = ret.group(1)
        # 如果URI是/,则默认返回index.html的内容
        if file_name == "/":
            file_name = "/index.html"

    try:
        # 根据请求的URI,读取相应的文件
        fp = open("." + file_name, "rb")
    except:
        # 找不到文件,响应404
        response_msg = "HTTP/1.1 404 NOT FOUND
"
        response_msg += "
"
        response_msg += "<h1>----file not found----</h1>"
        new_socket.send(response_msg.encode("utf-8"))
    else:
        html_content = fp.read()
        fp.close()

        response_body = html_content

        # 响应正确 200 OK
        response_header = "HTTP/1.1 200 OK
"
        response_header += "Content-Length:%d
" % len(response_body)
        response_header += "
"

        response = response_header.encode("utf-8") + response_body

        # 返回响应数据
        new_socket.send(response)


def main():
    # 创建TCP SOCKET实例
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # # 设置重用地址
    # tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定地址(默认本机IP)和端口
    tcp_server_socket.bind(("", 7890))
    # 监听
    tcp_server_socket.listen(128)

    # 将accept设置为非阻塞,这里设置一次,后面不管调多少次accept都是非阻塞的
    tcp_server_socket.setblocking(False)

    # 创建一个epoll对象
    epl = select.epoll()
    # 将监听套接字对应的fd注册到epoll中,并让其监听有没有数据进来,所以使用EPOLLIN
    epl.register(tcp_server_socket.fileno(), select.EPOLLIN)

    # 定义一个字典,用于存放fd和套接字的对应关系,因为操作系统在事件通知的时候,使用的是fd,而不是套接字,我们需要使用fd来找到对应
    # 的套接字,从而可以调用accept和recv
    fd_event_dict = dict()

    # 循环接收客户端连接
    while True:
        # 使用一个列表来接受操作系统的事件通知,poll()是阻塞的,当有数据到达时,poll才会解开阻塞
        fd_event_list = epl.poll()
        # 操作系统的事件通知返回一个列表(可能同时有多个套接字有数据进入),这个列表中的元素都是元组(fd,event)
        for fd, event in fd_event_list:
            # 首先判断事件通知中的fd是否对应监听套接字(监听套接字调用accept)
            if fd == tcp_server_socket.fileno():
                new_socket, client_addr = tcp_server_socket.accept()
                # 监听到一个新的客户端连接,将new_socket也注册到epoll中
                epl.register(new_socket.fileno(), select.EPOLLIN)
                # 并且将这个socket加入fd_event_dict字段,方便以后通过fd来获取套接字
                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:
                    # 处理数据
                    handle_request(fd_event_dict[fd], recv_data)
                else:  # 如果没有数据,则表示客户端断开连接
                    # 关闭fd对应的socket
                    fd_event_dict[fd].close()
                    # 从epoll中踢出已经断开的fd
                    epl.unregister(fd)
                    # 从字典中删除fd对应的记录
                    del fd_event_dict[fd]

    # 关闭整个SOCKET
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

解释:

1.首先创建epoll对象

2.将监听套接字对应fd注册到epoll,并设置监听数据的IN。

3.调用poll()函数,如果没有数据到达,则处于阻塞状态,如果有数据到达,则操作系统会返回一个事件通知列表。

4.遍历列表,如果发现fd是监听套接字对应fd,则使用监听套接字调用accept,并将接收到的新的客户端连接对应socket也注册到epoll中,并将其存放到字典fd_event_dict中(方便后续使用fd获取socket)。

5.如果不是监听套接字,则直接从fd_event_dict中通过fd获取对应的socket,然后调用recv来接收数据。

6.如果接收到的数据有内容,则调用请求处理逻辑。

7.如果接收到的数据为空,则表示客户端主动调用了close,想要断开连接。此时从fd_event_dict中通过fd获取对应socket,然后调用socker.close()来关闭连接。

8.关闭连接后,将该socket从epoll中剔除,并且从fd_event_dict中删除。

注意:该代码无法在windows上运行,因为epoll是Linux2.6内核增加的新功能,windows并不支持。

免责声明:文章转载自《[Python之路] 使用epoll实现高并发HTTP服务器》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇学习bash——数据流重定向把Gitlab迁移到Docker容器里下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

PyH : python生成html

参考:Python PyH模块中文文档 样例 下面是官网的一个例子: from pyh import * page = PyH('My wonderful PyH page') page.addCSS('myStylesheet1.css', 'myStylesheet2.css') page.addJS('myJavascript1.js', 'm...

boost asio 异步实现tcp通讯

---恢复内容开始--- asioboost 目录(?)[-] 一前言 二实现思路 通讯包数据结构 连接对象 连接管理器 服务器端的实现 对象串行化 一、前言 boost asio可算是一个简单易用,功能又强大可跨平台的C++通讯库,效率也表现的不错,linux环境是epoll实现的,而windows环境是iocp实现的。而tcp通讯是项...

C#服务器全面讲解与制作

C#服务器全面讲解与制作一             环境配置与基础架构 环境配置 基础的服务器架构 这里我会讲解高级的C#服务器的全面制作流程 会对大家有很大的帮助 不过在这个教程中主要是讲解服务器的制作,所以不会讲解客户端的制作,不过会提供相关客户端的代码。 1 环境配置 1.1 VS code环境配置   如果你觉得用Visual Studio来写...

python 把数据 json格式输出

有个要求需要在python的标准输出时候显示json格式数据,如果缩进显示查看数据效果会很好,这里使用json的包会有很多操作 import json date = {u'versions': [{u'status': u'CURRENT', u'id': u'v2.3', u'links': [{u'href': u'http://controll...

thrift入门(1) 安装配置

thrift 是一个跨语言的通讯框架,支持c++, java, .net, python,php等。你可以用一种语言写一个服务器,然后另外一种语言写一个客户端,快速搭建一个rpc调用服务。而且它很轻量级,只要引入特定库便可以运行服务和客户端,不需要再安装配置其它复杂的环境和容器。 下面介绍如何在ubuntu11.10上安装thrift0.8.0。 1....

Maya Max python PySide集成 shiboken版本对应关系

  Maya_Max _python_PySide集成_shiboken版本对应关系1.如何查看 Maya Max 集成的 Python版本:Maya:在 Maya 的安装目录下的 bin 文件夹中找到 mayapy.exe,双击运行就可以看到 Python 版本。 Max(2017及其以上)  :在 Max 的安装目录下找到 3dsmaxpy.exe,双...