您的位置:首页 > 运维架构 > Nginx

nginx服务器的事件驱动模型

2017-06-07 20:33 302 查看
事件驱动模型是Nginx服务器保障完整功能和具有良好性能的重要机制之一。

事件驱动模型一般是由事件收集器、事件发送器、事件处理器三部分基本单元组成。



在nginx里面有三个库,就是大名鼎鼎的select库,poll库,epoll库。

select库

      select库,是linux和windows都支持的基本事件驱动模型。并且在接口上的定义基本相同,只是部分的参数可能有略微的差异。select库的步骤一般是:

      首先,创建所关注事件的描述符集合。对于一个描述符,可以关注其上面的读(Read)事件,写(Write)事件以及异常发生(Exception)事件。所以要创建三类事件描述符集合。分别用来收集读事件,写事件和异常事件的描述符的集合。
      其次,调用底层提供的select函数。等待事件发生。
      然后,轮询所有事件描述符集合中的每一个事件描述符。检查是否有相应的事件发生,如果有,就进行处理。

poll库

    poll库,作为Linux上的基本事件驱动模型,windows是不支持的。poll库和select库的工作方式基本相同,都是创建一个关注事件的描述符集合。再去等待这些事件的发生。然后再轮询描述符集合。检查是否有相应的事件发生,有就进行处理。
    但是poll和select库的区别是,select需要为读事件,写事件,异常事件分别创建一个描述符,因此在最后轮询的时候,需要分别轮询三个集合。而poll库只需要创建一个集合,在每个集合描述符上分别设置读事件,写事件或者异常事件。在最后的轮询过程中,可以同时检查三种事件是否发生。可以说,poll库是select的优化升级版本。

epoll库

    epoll库是Nginx服务器支持的高性能事件驱动库之一。它是公认的最好的事件驱动模型。和poll库及select库有很大的区别。
    poll和select都是创建一个待处理事件列表,然后把这个列表发给内核,返回的时候,再去轮询检查这个列表。以判断这个事件是否发生。在描述符太多的情况下,就会明显效率低下了。
    epoll是这么做的,它把事件描述符列表的管理交给内核复制。一旦有摸个事件发生,内核将发生事件的事件描述符交给Nginx的进程,而不是将整个事件描述符列表交给进程,让进程去轮询具体是哪个描述符。epoll()避免了轮询整个事件描述符列表。所以显得更高效。
    epoll库的基本步骤:
    首先:epoll库通过相关调用通知内核创建一个有N个描述符的事件列表。饭后给这个事件列表设置自己关心的事件。并把它添加到内核中。在具体的代码中还可以实现对相关调用的事件描述符列表进行修改和删除。
    之后,一旦设置完成就一直等待内核通知事件发生了,某一事件发生后,内核就将发生事件的描述符给epoll库,epoll库去处理事件。

   epoll库在Linux平台上是十分高效的,它支持一个进程打开大数目的事件描述符。上限时可以打开的文件最大数。可以由ulimit -n 和 max_file去设置打开的最大描述符。

另外select和poll是是水平触发的(Level Triggered)。就是此次描述符的响应未处理的话,下次还可以再次提醒处理
而epoll是水平触发(Level Triggered)和边沿(Edge Triggered)都支持的。理论上边沿触发的性能更高点

python实现的select。
import socket
import Queue
from select import select

SERVER_IP = ('127.0.0.1', 9999)

# 保存客户端发送过来的消息,将消息放入队列中
message_queue = {}
input_list = []
output_list = []

if __name__ == "__main__":
server = socket.socket()
server.bind(SERVER_IP)
server.listen(10)
# 设置为非阻塞
server.setblocking(False)

# 初始化将服务端加入监听列表
input_list.append(server)

while True:
# 开始 select 监听,对input_list中的服务端server进行监听
stdinput, stdoutput, stderr = select(input_list, output_list, input_list)

# 循环判断是否有客户端连接进来,当有客户端连接进来时select将触发
for obj in stdinput:
# 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时,说明有新客户端连接进来了
if obj == server:
# 接收客户端的连接, 获取客户端对象和客户端地址信息
conn, addr = server.accept()
print("Client {0} connected! ".format(addr))
# 将客户端对象也加入到监听的列表中, 当客户端发送消息时 select 将触发
input_list.append(conn)
# 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
message_queue[conn] = queue.Queue()

else:
# 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list),客户端发送消息将触发
# 所以判断是否是客户端对象触发
try:
recv_data = obj.recv(1024)
# 客户端未断开
if recv_data:
print("received {0} from client {1}".format(recv_data.decode(), addr))
# 将收到的消息放入到各客户端的消息队列中
message_queue[obj].put(recv_data)

# 将回复操作放到output列表中,让select监听
if obj not in output_list:
output_list.append(obj)

except ConnectionResetError:
# 客户端断开连接了,将客户端的监听从input列表中移除
input_list.remove(obj)
# 移除客户端对象的消息队列
del message_queue[obj]
print("\n[input] Client  {0} disconnected".format(addr))

# 如果现在没有客户端请求,也没有客户端发送消息时,开始对发送消息列表进行处理,是否需要发送消息
for sendobj in output_list:
try:
# 如果消息队列中有消息,从消息队列中获取要发送的消息
if not message_queue[sendobj].empty():
# 从该客户端对象的消息队列中获取要发送的消息
send_data = message_queue[sendobj].get()
sendobj.sendall(send_data)
else:
# 将监听移除等待下一次客户端发送消息
output_list.remove(sendobj)

except ConnectionResetError:
# 客户端连接断开了
del message_queue[sendobj]
output_list.remove(sendobj)
print("\n[output] Client  {0} disconnected".format(addr))


python实现的epoll
#!/usr/bin/env python
import select
import socket

response = b''

serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
# 因为socket默认是阻塞的,所以需要使用非阻塞(异步)模式。
serversocket.setblocking(0)

# 创建一个epoll对象
epoll = select.epoll()
# 在服务端socket上面注册对读event的关注。一个读event随时会触发服务端socket去接收一个socket连接
epoll.register(serversocket.fileno(), select.EPOLLIN)

try:
# 字典connections映射文件描述符(整数)到其相应的网络连接对象
connections = {}
requests = {}
responses = {}
while True:
# 查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,我们会等待1秒来看是否有event发生。
# 如果有任何我们感兴趣的event发生在这次查询之前,这个查询就会带着这些event的列表立即返回
events = epoll.poll(1)
# event作为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。
for fileno, event in events:
# 如果是服务端产生event,表示有一个新的连接进来
if fileno == serversocket.fileno():
connection, address = serversocket.accept()
print('client connected:', address)
# 设置新的socket为非阻塞模式
connection.setblocking(0)
# 为新的socket注册对读(EPOLLIN)event的关注
epoll.register(connection.fileno(), select.EPOLLIN)
connections[connection.fileno()] = connection
# 初始化接收的数据
requests[connection.fileno()] = b''

# 如果发生一个读event,就读取从客户端发送过来的新数据
elif event & select.EPOLLIN:
print("------recvdata---------")
# 接收客户端发送过来的数据
requests[fileno] += connections[fileno].recv(1024)
# 如果客户端退出,关闭客户端连接,取消所有的读和写监听
if not requests[fileno]:
connections[fileno].close()
# 删除connections字典中的监听对象
del connections[fileno]
# 删除接收数据字典对应的句柄对象
del requests[connections[fileno]]
print(connections, requests)
epoll.modify(fileno, 0)
else:
# 一旦完成请求已收到,就注销对读event的关注,注册对写(EPOLLOUT)event的关注。写event发生的时候,会回复数据给客户端
epoll.modify(fileno, select.EPOLLOUT)
# 打印完整的请求,证明虽然与客户端的通信是交错进行的,但数据可以作为一个整体来组装和处理
print('-' * 40 + '\n' + requests[fileno].decode())

# 如果一个写event在一个客户端socket上面发生,它会接受新的数据以便发送到客户端
elif event & select.EPOLLOUT:
print("-------send data---------")
# 每次发送一部分响应数据,直到完整的响应数据都已经发送给操作系统等待传输给客户端
byteswritten = connections[fileno].send(requests[fileno])
requests[fileno] = requests[fileno][byteswritten:]
if len(requests[fileno]) == 0:
# 一旦完整的响应数据发送完成,就不再关注写event
epoll.modify(fileno, select.EPOLLIN)

# HUP(挂起)event表明客户端socket已经断开(即关闭),所以服务端也需要关闭。
# 没有必要注册对HUP event的关注。在socket上面,它们总是会被epoll对象注册
elif event & select.EPOLLHUP:
print("end hup------")
# 注销对此socket连接的关注
epoll.unregister(fileno)
# 关闭socket连接
connections[fileno].close()
del connections[fileno]
finally:
# 打开的socket连接不需要关闭,因为Python会在程序结束的时候关闭。这里显式关闭是一个好的代码习惯
epoll.unregister(serversocket.fileno())
epoll.close()
serversocket.close()
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: