Epoll 模型简介

作者: 人世间 | 来源:发表于2015-07-12 16:07 被阅读3328次

    网络IO的模型中,之前介绍了select模型。select 确实是一个简明好用的模型。可是现在的服务器却越来越少采取这样的模型,原因之一就是它的性能让人担忧。虽然后来升级了poll模型,本质上还是和select模型类似。当然,当一个技术逐渐被人放弃的时候,很大程度上是有了更好的替代方案。没错,还有select/poll模型更好的网络IO模型,就是今天介绍的主角---Epoll。在很多地方,epoll都是高性能代名词,准确的说epoll是Linux内核升级的多路复用IO模型,在Unix和MacOS上类似的则是 Kqueue。

    epoll优点

    select的缺点之一就是在网络IO流到来的时候,线程会轮询监控文件数组,并且是线性扫描,还有最大值的限制。相比select,epoll则无需如此。服务器主线程创建了epoll对象,并且注册socket和文件事件即可。当数据抵达的时候,也就是对于事件发生,则会调用此前注册的那个io文件。

    先看一个python的epoll例子,采用了网络上一段著名的code:

    import socket
    import select
    
    EOL1 = b'\n\n'
    EOL2 = b'\n\r\n'
    response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
    response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
    response += b'Hello, world!'
    
    # 创建套接字对象并绑定监听端口
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serversocket.bind(('0.0.0.0', 8080))
    serversocket.listen(1)
    serversocket.setblocking(0)
    
    # 创建epoll对象,并注册socket对象的 epoll可读事件
    epoll = select.epoll()
    epoll.register(serversocket.fileno(), select.EPOLLIN)
    
    try:
        connections = {}
        requests = {}
        responses = {}
        while True:
            # 主循环,epoll的系统调用,一旦有网络IO事件发生,poll调用返回。这是和select系统调用的关键区别
            events = epoll.poll(1)
            # 通过事件通知获得监听的文件描述符,进而处理
            for fileno, event in events:
                # 注册监听的socket对象可读,获取连接,并注册连接的可读事件
                if fileno == serversocket.fileno():
                    connection, address = serversocket.accept()
                    connection.setblocking(0)
                    epoll.register(connection.fileno(), select.EPOLLIN)
                    connections[connection.fileno()] = connection
                    requests[connection.fileno()] = b''
                    responses[connection.fileno()] = response
                elif event & select.EPOLLIN:
                    # 连接对象可读,处理客户端发生的信息,并注册连接对象可写
                    requests[fileno] += connections[fileno].recv(1024)
                    if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
                        epoll.modify(fileno, select.EPOLLOUT)
                        print('-' * 40 + '\n' + requests[fileno].decode()[:-2])
                elif event & select.EPOLLOUT:
                    # 连接对象可写事件发生,发送数据到客户端
                    byteswritten = connections[fileno].send(responses[fileno])
                    responses[fileno] = responses[fileno][byteswritten:]
                    if len(responses[fileno]) == 0:
                        epoll.modify(fileno, 0)
                        connections[fileno].shutdown(socket.SHUT_RDWR)
                elif event & select.EPOLLHUP:
                    epoll.unregister(fileno)
                    connections[fileno].close()
                    del connections[fileno]
    finally:
        epoll.unregister(serversocket.fileno())
        epoll.close()
        serversocket.close()
    

    可见epoll使用也很简单,并没有过多复杂的逻辑,当然主要是在系统层面封装的好。至于Epoll的原理,也不是三言两语可以解释清楚,作为开发者,先学会如何使用API。

    epoll与tornado

    既然epoll是一种高性能的网络io模型,很多web框架也采取epoll模型。大名鼎鼎tornado是python框架中一个高性能的异步框架,其底层也是来者epoll的IO模型。
    当然,tornado是跨平台的,因此他的网络io,在linux下是epoll,unix下则是kqueue。幸好tornado都做了封装,对于开发者及其友好,下面看一个tornado写的回显例子。

    import errno
    import functools
    import tornado.ioloop
    import socket
    
    
    def handle_connection(connection, address):
        """ 处理请求,返回数据给客户端 """
        data = connection.recv(2014)
        print data
        connection.send(data)
    
    
    def connection_ready(sock, fd, events):
        """ 事件回调函数,主要用于socket可读事件,用于获取socket的链接 """
        while True:
            try:
                connection, address = sock.accept()
            except socket.error as e:
                if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
                    raise
                return
            connection.setblocking(0)
            handle_connection(connection, address)
    
    
    if __name__ == '__main__':
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.setblocking(0)
        sock.bind(("", 5000))
        sock.listen(128)
        # 使用tornado封装好的epoll接口,即IOLoop对象
        io_loop = tornado.ioloop.IOLoop.current()
        callback = functools.partial(connection_ready, sock)
        # io_loop对象注册网络io文件描述符和回调函数与io事件的绑定
        io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
        io_loop.start()
    

    上面的代码来者tornado的模块IOLoop源码的文档,很简明的介绍了在tornado中如何使用网络IO。当然具体的封装实现,可以参考tornado源码获知,在此不做介绍了。

    相关文章

      网友评论

      本文标题:Epoll 模型简介

      本文链接:https://www.haomeiwen.com/subject/sysuqttx.html