学科分类
目录

epoll并发服务器

单进程非阻塞服务器和select服务器都以轮询的方式获取就绪套接字,当服务器程序中的连接数较多时,一趟轮询便会耗费大量的时间,服务器的效率也因此而逐渐降低。

在信息科技飞速发展的今天,一个小型网站的并发量已远远超过1024,显然单进程非阻塞服务器和select服务器都很难满足服务器搭建的实际需求。为了克服select服务器连接数量的限制,人们研发了一种名为poll的服务器,然而,这种服务器除了能解决连接上限的问题外,其它方面与select基本没有区别,它的工作效率同样会随着连接数量的增高而降低。

那么是否有兼顾连接数量与效率的服务器呢?答案是肯定的。

epoll服务器是Linux系统中常用的一种高效服务器,这种服务器采用事件通知机制,事先为要建立连接的socket注册事件,一旦该socket就绪,注册事件将被触发,socket将被加入epoll的就绪套接字列表,而服务器无需主动监测所有套接字状态,只需直接获取就绪套接字列表,对其中的套接字进行处理即可。

Python中的epoll模式定义在select模块中,select中包含一个名为epoll的类,用户可先在程序中创建epoll对象,再通过epoll对象的方法实现epoll模式。

在程序中定义epoll对象的方法如下:

import select
epoll = select.epoll()

epoll模式中包含了两个重要操作,一是事件注册,二是就绪套接字获取,这两个重要操作分别通过epoll对象的register()方法和poll()方法实现。

1. register()方法

register()方法的功能是为其参数fd创建注册事件,该方法的语法格式如下:

register(fd[, eventmask])

register()方法中的第一个参数fd是一个文件描述符,文件描述符是Linux系统中定义的,用于在进程或主机中标识一个唯一确定的已打开文件的符号,程序打开文件时,内核会向进程返回一个文件描述符。

Linux系统将套接字视为一种特殊文件,使用套接字方法fileno()可获取套接字的文件描述符,文件描述符本质上是一个整数。调用register()方法可为文件描述符fd指向的套接字注册事件。

register()方法的第二个参数eventmask是可选参数,该参数用于设置epoll要监控的事件和epoll的工作模式,事件是由epoll常量组成的位,其默认值为EPOLLIN | EPOLLOUT | EPOLLPRI,表示同时监控fd的读事件、写事件和紧急可读事件。该参数可用的epoll常量及其表示的含义分别如下:

  • EPOLLERR表示监控fd的错误事件;

  • EPOLLHUP表示监控fd的挂断事件;

  • EPOLLET表示将epoll设置为边缘触发(Edge Triggered)模式;

  • EPOLLONESHOT表示只监听一次事件,当此次事件监听完成后,若要再次监听该fd,需将其再次添加到epoll队列中。

register()方法没有返回值。使用epoll中的unregister()方法可以将套接字从监听列表中移除。

2. poll()方法

poll()方法的功能是查询epoll对象,判断是否有epoll关注的事件被触发。poll()方法的语法格式如下:

poll([timeout=-1[, maxevents=-1]]) 

poll()中的参数都是可缺省参数,其中timeout用于设置等待时长,其默认值-1表示无限等待。若在调用poll()方法前已有epoll监测的事件发生,此次查询会立刻返回一个列表,该列表中的元素为形如(fd,event code)的元组。

以大小写转换为例,搭建基于epoll模式的TCP服务器,服务器的具体代码如下。

 1  import socket
 2  import select
 3  def main():
 4    # 创建套接字
 5    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 6    # 绑定本机信息
 7    server_socket.bind(("", 8080)) 
 8    # 重复使用绑定的信息
 9    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 10   # 变为被动
 11   server_socket.listen(10)
 12   # 设置套接字为非阻塞模式
 13   server_socket.setblocking(False)
 14   # 创建一个epoll对象
 15   epoll = select.epoll()
 16   # 为服务器端套接字server_socket的文件描述符注册事件
 17   epoll.register(server_socket.fileno(), 
 18            select.EPOLLIN | select.EPOLLET)
 19   new_socket_list = {}       # 第19行
 20   client_address_list = {}     # 第20行
 21   # 循环等待数据到达
 22   while True:
 23     # 检测并获取epoll监控的已触发事件
 24     epoll_list = epoll.poll()
 25     # 对事件进行处理
 26     for fd, events in epoll_list:
 27       # 如果有新的连接请求递达
 28       if fd == server_socket.fileno():
 29         new_socket, client_address = server_socket.accept()
 30         print('有新的客户端到来%s'%str(client_address))
 31         # 保存新客户端的套接字信息和地址信息。第31行
 32         new_socket_list[new_socket.fileno()] = new_socket
 33         client_address_list[new_socket.fileno()] = client_address
 34         # 为新套接字的文件描述符注册读事件
 35         epoll.register(new_socket.fileno(),
 36                  select.EPOLLIN | select.EPOLLET)
 37       elif events == select.EPOLLIN:
 38         # 从new_socket触发的事件
 39         recv_data = new_socket_list[fd].recv(1024)
 40         # 若数据长度大于0,处理数据
 41         if len(recv_data) > 0:
 42           print('待处理数据%s'%recv_data.decode('gb2312'))
 43           recv_data = recv_data.upper()
 44           new_socket.send(recv_data)
 45         # 若数据长度为0,关闭连接
 46         else:
 47           # 从epoll中移除fd
 48           epoll.unregister(fd)
 49           # 关闭服务器端为该连接创建的套接字
 50           new_socket_list[fd].close()
 51           print("%s---offline---" % str(client_address_list[fd]))
 52 if __name__ == '__main__':
 53   main()

以上程序的第19、20两行代码创建了字典new_socket_list和client_address_list,分别用于存储与客户端交互的套接字信息和客户端地址;第32、33两行代码以套接字的文件描述符作为key值,将新客户端的套接字和地址信息存储到了new_socket_list和client_address_list中。

为了测试该程序的功能,下面实现一个可向服务器发送数据,并能接收服务器反馈信息的客户端程序,客户端程序代码如下。

from socket import *
def main():
  client_socket = socket(AF_INET, SOCK_STREAM)
  client_socket.connect(('192.168.255.144', 8080))
  while True:
     data = input('------待处理数据------\n')
     client_socket.send(data.encode('gb2312'))
     recv_info = client_socket.recv(1024).decode('gb2312')
     print('------处理结果------\n%s'%recv_info)
  server_socket.close()
if __name__ == '__main__':
  main()

启动服务器之后再启动客户端,服务器中将打印客户端的地址信息,本次测试共启动了4个客户端,客户端都启动后,服务器中打印的信息如下:

有新的客户端到来:('192.168.255.144', 10268)
有新的客户端到来:('192.168.255.144', 10269)
有新的客户端到来:('192.168.255.144', 10275)
有新的客户端到来:('192.168.255.144', 10281)

由此可知,本小节搭建的基于epoll模型的TCP服务器可同时与多个客户端建立连接。

使用客户端向服务器中发送数据,客户端中打印的信息如下所示:

-----待处理数据------
hello itheima
------处理结果-------
HELLO ITHEIMA
-----待处理数据------

由此可知,本小节搭建的基于epoll模型的TCP服务器可成功接收客户端发送的数据,并将数据的处理结果返回给客户端。

综上所述,可知基于epoll模型的TCP并发服务器实现成功。

Windows系统中利用IOCP(完成端口)可实现与Linux下epoll相同的功能,有兴趣的读者可自行查阅资料学习。

点击此处
隐藏目录