Skip to content

Instantly share code, notes, and snippets.

@limboinf
Created May 31, 2016 10:16
Show Gist options
  • Save limboinf/e850776b4e0f497074ee4cdfd94d1b46 to your computer and use it in GitHub Desktop.
Save limboinf/e850776b4e0f497074ee4cdfd94d1b46 to your computer and use it in GitHub Desktop.
Python implements a http server by epoll(version 2.1, epoll, edge-triggered)
# coding=utf-8
"""
:copyright: (c) 2016 by fangpeng(@beginman.cn).
:license: MIT, see LICENSE for more details.
"""
import socket
import select
EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
response = b'HTTP/1.1 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!'
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 因为socket默认是阻塞的, 我们需要设置成非阻塞(异步)模式.
sock.setblocking(0)
sock.bind(('0.0.0.0', 8000))
sock.listen(1)
# 建立一个epoll对象
epoll = select.epoll()
# 注册服务器socket, 监听读取事件. 服务器socket接收一个连接的时候, 产生一个读取事件.
# select.EPOLLET use edge-triggered
epoll.register(sock.fileno(), select.EPOLLIN|select.EPOLLET)
try:
# connections表映射文件描述符(file descriptors, 整型)到对应的网络连接对象上面.
connections = {}
requests = {}
responses = {}
while 1:
# epoll对象查询一下是否有感兴趣的事件发生,
# 参数1说明我们最多等待1秒的时间. 如果有对应事件发生, 立刻会返回一个事件列表.
events = epoll.poll(1)
for fileno, event in events:
# 如果是服务器socket的事件, 那么需要针对新的连接建立一个socket.
if fileno == sock.fileno():
try:
while 1:
conn, addr = sock.accept()
conn.setblocking(0) # 设置socket为非阻塞模式.
# 注册socket的read(EPOLLIN)事件.并设置为edge-triggered模式
epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
connections[conn.fileno()] = conn
requests[conn.fileno()] = b''
responses[conn.fileno()] = response
except socket.error:
pass
# 如果读取事件发生, 从客户端读取新数据.
elif event & select.EPOLLIN:
try:
while 1:
requests[fileno] += connections[fileno].recv(1024)
except socket.error:
pass
if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
# 一旦完整的http请求接收到, 取消注册读取事件, 注册写入事件(EPOLLOUT)
# 写入事件在 发送数据回客户端 的时候产生.
epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)
# 打印完整的http请求, 展示即使通讯是交错的, 数据本身是作为一个完整的信息组合和处理的.
print('-'*40 + '\n' + requests[fileno].decode()[:-2])
# 如果写入事件发生在一个客户端socket上面, 我们就可以发送新数据到客户端了.
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
# 一旦所有的返回数据都发送完, 取消监听读取和写入事件.只保留EPOLLET
if len(responses[fileno]) == 0:
epoll.modify(fileno, select.EPOLLET)
# 如果连接被明确关闭掉, 这一步是可选的.
# 这个例子采用这个方法是为了让客户端首先断开, 告诉客户端没有数据需要发送和接收了, 然后让客户端断开连接.
connections[fileno].shutdown(socket.SHUT_RDWR)
# HUP(hang-up)事件表示客户端断开了连接(比如 closed), 所以服务器这端也会断开.
# 不需要注册HUP事件, 因为它们都会标示到注册在epoll的socket.
elif event & select.EPOLLHUG:
epoll.unregister(fileno) # 取消注册.
connections[fileno].close() # 断开连接
del connections[fileno]
finally:
# try...finally
# 在这里的异常捕捉的作用是, 我们的例子总是采用键盘中断来停止程序执行.
# 虽然开启的socket不需要手动关闭, 程序退出的时候会自动关闭, 明确写出来这样的代码, 是更好的编码风格.
epoll.unregister(sock.fileno())
epoll.close()
sock.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment