Reference: http://blog.csdn.net/hehe123456ZXC/article/details/52526670html
由于最近想学习如何用epoll写服务器, 因而找到了一篇介绍的文章. 由于我最近一直看不进技术文章, 因而打算经过翻译来强迫本身学习. 原文在这里:python
文章里面的代码下载地址:linux
介绍
从2.6版本开始, Python 提供了使用Linux epoll 的功能. 这篇文章经过3个例子来大体介绍如何使用它. 欢迎提问和反馈.sql
第一个例子是一个简单的python3.0版本的服务器代码, 监听8080端口的http请求, 打印结果到命令行, 回应http response给客户端.数据库
行 9: 创建服务器的socket
行 10: 容许11行的bind()操做, 即便其余程序也在监听一样的端口. 否则的话, 这个程序只能在其余程序中止使用这个端口以后的1到2分钟后才能执行.
行 11: 绑定socket到这台机器上全部IPv4地址上的8080端口.
行 12: 告诉服务器开始响应从客户端过来的链接请求.
行 14: 程序会一直停在这里, 直到创建了一个链接. 这个时候, 服务器socket会创建一个新的socket, 用来和客户端通信. 这个新的socket是accept()的返回值, address对象标示了客户端的IP地址和端口.
行 15-17: 接收数据, 直到一个完整的http请求被接收完毕. 这是一个简单的http服务器实现.
行 18: 为了方便验证, 打印客户端过来的请求到命令行.
行 19: 发送回应.
行 20-22: 关闭链接, 以及服务器的监听socket.编程
Example1:缓存
import socket 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) connectiontoclient, address = serversocket.accept() request = b'' while EOL1 not in request and EOL2 not in request: request += connectiontoclient.recv(1024) print(request.decode()) connectiontoclient.send(response) connectiontoclient.close() serversocket.close()
第2个例子, 咱们在15行加上了一个循环, 用来循环处理客户端请求, 直到咱们中断这个过程(在命令行下面输入键盘中断, 好比Ctrl-C). 这个例子更明显地表示出来了, 服务器socket并无用来作数据处理, 而是接受服务器过来的链接, 而后创建一个新的socket, 用来和客户端通信.服务器
最后的23-24行确保服务器的监听socket最后老是close掉, 即便出现了异常.
Example 2:
import socket 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) try: while True: connectiontoclient, address = serversocket.accept() request = b'' while EOL1 not in request and EOL2 not in request: request += connectiontoclient.recv(1024) print('-'*40 + '\n' + request.decode()[:-2]) connectiontoclient.send(response) connectiontoclient.close() finally: serversocket.close()
异步socket和linux epoll的优点
第2个例子里面的socket采用的是阻塞方式, 由于python解释器在出现事件以前都处在中止状态. 16行的accept()一直阻塞, 直到新的链接进来. 19行的recv()也是一直阻塞, 直到从客户端收到数据(或者直到没有数据能够接收). 21行的send()也一直阻塞, 直到全部须要发送给客户端的数据都交给了linux内核的发送队列.
当一个程序采用阻塞socket的时候, 它常常采用一个线程(甚至一个进程)一个socket通信的模式. 主线程保留服务器监听socket, 接受进来的链接, 一次接受一个链接, 而后把生成的socket交给一个分离的线程去作交互. 由于一个线程只和一个客户端通信, 在任何位置的阻塞都不会形成问题. 阻塞自己不会影响其余线程的工做.
多线程阻塞socket模式代码清晰, 可是有几个缺陷, 可能很难确保线程间资源共享工做正常, 可能在只有一个CPU的机器上效率低下.
C10K(单机1万链接问题!) 探讨了其余处理并行socket通信的模式. 一种是采用异步socket. socket不会阻塞, 直到特定事件发生. 程序在异步socket上面进行一个特定操做, 而且当即获得一个结果, 无论执行成功或者失败. 而后让程序决定下一步怎么作. 由于异步socket是非阻塞的, 咱们能够不采用多线程. 全部的事情均可以在一个线程里面完成. 虽然这种模式有它须要面对的问题, 它对于特定程序来讲仍是不错的选择. 也能够和多线程合起来使用: 单线程的异步socket能够看成服务器上面处理网络的一个模块, 而线程能够用来访问阻塞式的资源, 好比数据库.
Linux 2.6有一些方式来管理异步socket, python API可以用的有3种: select, poll和epoll. epoll和poll比select性能更好, 由于python程序不须要为了特定的事件去查询单独的socket, 而是依赖操做系统来告诉你什么socket产生了什么事件. epoll比poll性能更好, 由于它不须要每次python程序查询的时候, 操做系统都去检查全部的socket, 在事件产生的时候, linux跟踪他们, 而后在python程序调用的时候, 返回具体的列表. 因此epoll在大量(上千)并行链接下, 是一种更有效率, 伸缩性更强的机制.
采用epoll的程序通常这样操做:
创建一个epoll对象
告诉epoll对象, 对于一些socket监控一些事件.
问epoll, 从上次查询以来什么socket产生了什么事件.
针对这些socket作特定操做.
告诉epoll, 修改监控socket和/或监控事件.
重复第3步到第5步, 直到结束.
销毁epoll对象.
采用异步socket的时候第3步重复了第2步的事情. 这里的程序更复杂, 由于一个线程须要和多个客户端交互.
行 1: select模块带有epoll功能
行 13: 由于socket默认是阻塞的, 咱们须要设置成非阻塞(异步)模式.
行 15: 创建一个epoll对象.
行 16: 注册服务器socket, 监听读取事件. 服务器socket接收一个链接的时候, 产生一个读取事件.
行 19: connections表映射文件描述符(file descriptors, 整型)到对应的网络链接对象上面.
行 21: epoll对象查询一下是否有感兴趣的事件发生, 参数1说明咱们最多等待1秒的时间. 若是有对应事件发生, 马上会返回一个事件列表.
行 22: 返回的events是一个(fileno, event code)tuple列表. fileno是文件描述符, 是一个整型数.
行 23: 若是是服务器socket的事件, 那么须要针对新的链接创建一个socket.
行 25: 设置socket为非阻塞模式.
行 26: 注册socket的read(EPOLLIN)事件.
行 31: 若是读取事件发生, 从客户端读取新数据.
行 33: 一旦完整的http请求接收到, 取消注册读取事件, 注册写入事件(EPOLLOUT), 写入事件在可以发送数据回客户端的时候产生.
行 34: 打印完整的http请求, 展现即便通信是交错的, 数据自己是做为一个完整的信息组合和处理的.
行 35: 若是写入事件发生在一个客户端socket上面, 咱们就能够发送新数据到客户端了.
行s 36-38: 一次发送一部分返回数据, 直到全部数据都交给操做系统的发送队列.
行 39: 一旦全部的返回数据都发送完, 取消监听读取和写入事件.
行 40: 若是链接被明确关闭掉, 这一步是可选的. 这个例子采用这个方法是为了让客户端首先断开, 告诉客户端没有数据须要发送和接收了, 而后让客户端断开链接.
行 41: HUP(hang-up)事件表示客户端断开了链接(好比 closed), 因此服务器这端也会断开. 不须要注册HUP事件, 由于它们都会标示到注册在epoll的socket.
行 42: 取消注册.
行 43: 断开链接.
行s 18-45: 在这里的异常捕捉的做用是, 咱们的例子老是采用键盘中断来中止程序执行.
行s 46-48: 虽然开启的socket不须要手动关闭, 程序退出的时候会自动关闭, 明确写出来这样的代码, 是更好的编码风格.
Example 3:
import socket, 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 = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN) try: connections = {}; requests = {}; responses = {} while True: events = epoll.poll(1) for fileno, event in events: 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有2种模式, 边沿触发(edge-triggered)和状态触发(level-triggered). 边沿触发模式下, epoll.poll()在读取/写入事件发生的时候只返回一次, 程序必须在后续调用epoll.poll()以前处理完对应事件的全部的数据. 当从一个事件中获取的数据被用完了, 更多在socket上的处理会产生异常. 相反, 在状态触发模式下面, 重复调用epoll.poll()只会返回重复的事件, 直到全部对应的数据都处理完成. 通常状况下不产生异常.
好比, 一个服务器socket注册了读取事件, 边沿触发程序须要调用accept创建新的socket链接直到一个socket.error错误产生, 而后状态触发下只须要处理一个单独的accept(), 而后继续epoll查询新的事件来判断是否有新的accept须要操做.
例子3采用默认的状态触发模式, 例子4展现如何用边沿触发模式. 例子4中的25, 36和45行引入了循环, 直到错误产生(或者全部的数据都处理完了), 32, 38 和48行捕捉socket异常. 最后16, 28, 41 和51行添加EPOLLET mask用来设置边沿触发.
Example 4:
import socket, 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 = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET) try: connections = {}; requests = {}; responses = {} while True: events = epoll.poll(1) for fileno, event in events: if fileno == serversocket.fileno(): try: while True: connection, address = serversocket.accept() connection.setblocking(0) epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET) connections[connection.fileno()] = connection requests[connection.fileno()] = b'' responses[connection.fileno()] = response except socket.error: pass elif event & select.EPOLLIN: try: while True: requests[fileno] += connections[fileno].recv(1024) except socket.error: pass if EOL1 in requests[fileno] or EOL2 in requests[fileno]: epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET) print('-'*40 + '\n' + requests[fileno].decode()[:-2]) elif event & select.EPOLLOUT: try: while len(responses[fileno]) > 0: byteswritten = connections[fileno].send(responses[fileno]) responses[fileno] = responses[fileno][byteswritten:] except socket.error: pass if len(responses[fileno]) == 0: epoll.modify(fileno, select.EPOLLET) 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()
由于比较相似, 状态触发常常用在转换采用select/poll模式的程序上面, 边沿触发用在程序员不须要或者不但愿操做系统来管理事件状态的场合上面.
除了这两种模式之外, socket常常注册为EPOLLONESHOT event mask, 当用到这个选项的时候, 事件只有效一次, 以后会自动从监控的注册列表中移除.
Listen Backlog Queue Size
在1-4的例子中, 12行显示了调用serversocket.listen()方法. 参数是监听等待队列的大小. 它告诉了操做系统, 在python代码accept前, 缓存多少TCP/IP链接在队列中. 每次python代码调用accept()的时候, 一个链接从队列中移除, 为新的链接进来空出一个位置. 若是队列满了, 新的链接自动放弃, 给客户端带来没必要要的网络延迟. 一个生产环境下的服务器常常处理几十或者几百的同时链接数, 因此参数不该该设置为1. 好比, 当采用 ab 来对这些测试程序进行并发100个http1.0客户端请求时, 少于50的参数容易形成性能降低.
TCP Options
TCP_CORK 参数能够设置缓存消息直到一块儿被发送, 这个选项, 在例子5的34和40行, 适合给一个实现 http/1.1 pipelining 的服务器来使用.
Example 5:
import socket, 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 = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN) try: connections = {}; requests = {}; responses = {} while True: events = epoll.poll(1) for fileno, event in events: 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) connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1) 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: connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 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()
另外一方面, TCP_NODELAY 能够用来告诉操做系统, 任何发给socket.send()的数据必须不通过操做系统的缓存, 马上发送给客户端.
这个选项, 在第6个例子的14行, 能够给SSH客户端或者其余实时性要求比较高的应用来使用.
Example 6:
import socket, 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) serversocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) epoll = select.epoll() epoll.register(serversocket.fileno(), select.EPOLLIN) try: connections = {}; requests = {}; responses = {} while True: events = epoll.poll(1) for fileno, event in events: 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()
源码在这里: