代码改变世界

Learning by doing 系列文章(之一)如何在 Python 中使用 epoll ?

2012-01-11 17:27  Haippy  阅读(2054)  评论(0编辑  收藏  举报

epoll 简介

参见本博前一文《epoll使用详解》

Epoll Within Python

Python 在 2.6 版中引入了用于处理Linux epoll系统调用的API,本文简单地介绍 python 中与 epoll 有关的 API,欢迎大家提出问题。

阻塞套接字编程实例

例1 是一个简单的服务器程序,该程序监听8080号端口,等待请求信息,并将其打印在控制台,同时向客户端发送反馈信息。

  • Line 9: 创建服务器端套接字。
  • Line 10: 保证 11 行的bind() 能够成功,如果不使用 socket.SO_REUSEADDRP,那么监听其他程序近期使用过的同一个端口可能会失败,此时需要等待操作系统回收该端口之后才能绑定成功, 通常会等待1-2分钟。
  • Line 11: 在本机上绑定可用的 IPv4 地址(通常是127.0.0.1)至8080号端口。
  • Line 12: 监听套接字,告诉此套接字可以开始接受客户端连接。
  • Line 14: 该程序直到有连接到来后才会停止,当有连接到来时,服务器端套接字会在本机创建一个新的套接字来处理客户端连接, 新的套接字由 accept() 产生,对象 clientconnection 表示该套接字,对象 address 表示该连接另一端的 IP 地址和端口号。
  • Lines 15-17: 组装将要发送至客户端的数据,该数据以 HTTP 协议发送,HTTP协议参见 HTTP Made Easy.
  • Line 18: 打印数据至控制台
  • Line 19: 发送反馈数据。
  • Lines 20-22: 关闭客户端连接和服务器端套接字。

Python 官方的 HOWTO 给出了更为详细的套接字编程手册。

例 1

 1 import socket
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13
14 connectiontoclient, address = serversocket.accept()
15 request = b''
16 while EOL1 not in request and EOL2 not in request:
17 request += connectiontoclient.recv(1024)
18 print(request.decode())
19 connectiontoclient.send(response)
20 connectiontoclient.close()
21
22 serversocket.close()

 

例 2 在15行添加了一个循环来不停的接受客户端连接,直到被用户打断(如,键盘输入 Ctrl + C), 该方式更加清晰的说明了服务器端的套接字并不用来和客户端交换数据,而是接受客户端连接,然后在服务器上创建一个新的套接字来和客户端通信。

最后23-24行的 finally 语句块保证监听的服务器套接字总是可以关闭的,即使发生了意外情况。

例 2

 1 import socket
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13
14 try:
15 while True:
16 connectiontoclient, address = serversocket.accept()
17 request = b''
18 while EOL1 not in request and EOL2 not in request:
19 request += connectiontoclient.recv(1024)
20 print('-'*40 + '\n' + request.decode()[:-2])
21 connectiontoclient.send(response)
22 connectiontoclient.close()
23 finally:
24 serversocket.close()

异步套接字和 Linux epoll

例2 使用的套接字被称为 阻塞套接字,因为 Python 程序在事件产生时停止执行,调用 accept() 将会导致程序阻塞,直到新的连接到来。 调用 recv() 也会导致程序阻塞,直到从客户端接收到数据(或没有更多的数据接收),调用 send() 也会阻塞程序,直到所有的将要发送给客户端的数据缓存的发送队列中。

当程序使用阻塞套接字时,我们通常只用一个线程(或专用的进程)来处理这些套接字上的通信,主程序线程包含了监听的服务器端套接字,我们在该套接字上接受客户端连接,通常一次接受一个新的连接,并将新创建的套接字传递给另外的线程,然后又该线程处理客户端连接,由于这些线程都只与一个客户端进行连接, 所以在某个线程处理连接时即使被阻塞了也无碍,该线程阻塞并不会影响其他的线程继续执行它们各自的任务。

使用多线程和阻塞的套接字的代码是比较直观的,但是也会产生一些问题, 首先难以保证多个线程协同共享某些资源,并且在只有一个 CPU 的机器中,该方式的处理效率并不高。

The C10K Problem 讨论了一些处理并发请求时的可选方案,其中之一便是使用异步套接字,所谓异步套接字,就是这些套接字在某些事件发生时并不会被阻塞,而是程序在该异步套接字上采取某些动作, 并立即得到所采取的动作是否成功的信息,这些信息决定了程序如何进一步的处理事件。由于异步套接字是非阻塞的,没有必要进行多线程处理,所有的工作可以在单线成中处理 。单线成程序固然尤其缺点,但是对于大部分程序而言都是一个很好的选择,它也可以和多线程方法结合起来:在服务器程序中,使用单线程处理网络模块中的异步套接字,而多线程处理其他的阻塞资源,如数据库。

Linux 2.6 提供了很多管理异步套接字的机制,其中 Python API 是 select, pollepoll,epollpollselect 好,因为此时 Python 程序不必为了自己感兴趣的事件而去检查每个套接字,而是依赖操作系统获知哪些套接字有自己感兴趣的事件。另外,epoll 又比 poll 好,因为 Python 程序不要求操作系统检测每个套接字来获取自己感兴趣的事件,而是在某些事件发生时 Linux 跟踪这些事件。然后返回事件列表,所以 epoll 在具有大量(数千的并发连接)并发连接时更高效,可扩展性更强,如果您有疑问,可以参看 这里

epoll 异步套接字编程实例

使用 epoll 机制的程序通常按照如下流程执行。

  1. 创建 epoll 对象。
  2. 告知该 epoll 对象需要在特定套接字上监听的某些事件。(即,注册事件)
  3. 询问 epoll 对象哪些套接字在最近一次查询后又有新的已注册的事件到来。
  4. 在第3步中有事件到来的那些套接字上进行操作。
  5. 告知 epoll 对象修改监听的套接字列表或事件类型。
  6. 重复 3-5 步,直到程序结束。
  7. 销毁 epoll 对象。

例 3 重复实现了例 2 的功能,只是例 3 使用了异步的套接字。 该程序更复杂,因为服务器端只有一个线程和多个客户端进行通信。

  • Line 1: The select module contains the epoll functionality.
  • Line 13: Since sockets are blocking by default, this is necessary to use non-blocking (asynchronous) mode.
  • Line 15: Create an epoll object.
  • Line 16: Register interest in read events on the server socket. A read event will occur any time the server socket accepts a socket connection.
  • Line 19: The connection dictionary maps file descriptors (integers) to their corresponding network connection objects.
  • Line 21: Query the epoll object to find out if any events of interest may have occurred. The parameter "1" signifies that we are willing to wait up to one second for such an event to occur. If any events of interest occurred prior to this query, the query will return immediately with a list of those events.
  • Line 22: Events are returned as a sequence of (fileno, event code) tuples. fileno is a synonym for file descriptor and is always an integer.
  • Line 23: If a read event occurred on the socket server, then a new socket connection may have been created.
  • Line 25: Set new socket to non-blocking mode.
  • Line 26: Register interest in read (EPOLLIN) events for the new socket.
  • Line 31: If a read event occurred then read new data sent from the client.
  • Line 33: Once the complete request has been received, then unregister interest in read events and register interest in write (EPOLLOUT) events. Write events will occur when it is possible to send response data back to the client.
  • Line 34: Print the complete request, demonstrating that although communication with clients is interleaved this data can be assembled and processed as a whole message.
  • Line 35: If a write event occurred on a client socket, it's able to accept new data to send to the client.
  • Lines 36-38: Send the response data a bit at a time until the complete response has been delivered to the operating system for transmission.
  • Line 39: Once the complete response has been sent, disable interest in further read or write events.
  • Line 40: A socket shutdown is optional if a connection is closed explicitly. This example program uses it in order to cause the client to shutdown first. The shutdown call informs the client socket that no more data should be sent or received and will cause a well-behaved client to close the socket connection from it's end.
  • Line 41: The HUP (hang-up) event indicates that the client socket has been disconnected (i.e. closed), so this end is closed as well. There is no need to register interest in HUP events. They are always indicated on sockets that are registered with the epoll object.
  • Line 42: Unregister interest in this socket connection.
  • Line 43: Close the socket connection.
  • Lines 18-45: The try-catch block is included because the example program will most likely be interrupted by a KeyboardInterrupt exception
  • Lines 46-48: Open socket connections don't need to be closed since Python will close them when the program terminates. They're included as a matter of good form.

例 3

 1 import socket, select
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13 serversocket.setblocking(0)
14
15 epoll = select.epoll()
16 epoll.register(serversocket.fileno(), select.EPOLLIN)
17
18 try:
19 connections = {}; requests = {}; responses = {}
20 while True:
21 events = epoll.poll(1)
22 for fileno, event in events:
23 if fileno == serversocket.fileno():
24 connection, address = serversocket.accept()
25 connection.setblocking(0)
26 epoll.register(connection.fileno(), select.EPOLLIN)
27 connections[connection.fileno()] = connection
28 requests[connection.fileno()] = b''
29 responses[connection.fileno()] = response
30 elif event & select.EPOLLIN:
31 requests[fileno] += connections[fileno].recv(1024)
32 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
33 epoll.modify(fileno, select.EPOLLOUT)
34 print('-'*40 + '\n' + requests[fileno].decode()[:-2])
35 elif event & select.EPOLLOUT:
36 byteswritten = connections[fileno].send(responses[fileno])
37 responses[fileno] = responses[fileno][byteswritten:]
38 if len(responses[fileno]) == 0:
39 epoll.modify(fileno, 0)
40 connections[fileno].shutdown(socket.SHUT_RDWR)
41 elif event & select.EPOLLHUP:
42 epoll.unregister(fileno)
43 connections[fileno].close()
44 del connections[fileno]
45 finally:
46 epoll.unregister(serversocket.fileno())
47 epoll.close()
48 serversocket.close()

epoll 有两种操作方式,分别被称为边缘触发( edge-triggered ) 和 水平触发( level-triggered )。在边缘触发操作模式下,调用 epoll.poll() 时事件只会在特定的有读写请求到来的套接字上返回一次,此后操作系统不再发出事件通知,程序必须处理所有的与该事件相关的数据,当数据从特定的事件消耗完毕后(即数据处理完成之后),再对该套接字进行操作将会产生异常。而相反地,水平触发方式( level-triggered mode),只要数据没有被处理完,多次调用 epoll.poll() 将会得到重复的与程序员感兴趣事件相关的通知,直到所有的与该事件相关的数据被处理完毕,在水平触发方式下,程序不会产生异常。

例如,假定服务器套接字已经注册了一个读事件的 epoll 对象, 在边缘触发模式下,程序需要调用 accept() 产生新的套接字,直到 socket.error 异常发生,然而,在水平触发模式下,调用一次  accept(),然后 epoll 对象可以再次被查询,以便该套接字上新的事件可以在调用 accept() 后被获取。

例 3 使用了水平触发,这是 epoll 默认的操作模式, 例 4 说明了如何使用边缘触发, 第 25, 36 和 45 引入了循环,直到发生异常(或所有的数据已被处理). 第 32, 38 和 48 捕获套接字异常,最后,第 16, 28, 41 和 51 将 EPOLLET 选项加入到了模式中。

例 4

 1 import socket, select
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13 serversocket.setblocking(0)
14
15 epoll = select.epoll()
16 epoll.register(serversocket.fileno(), select.EPOLLIN | select.EPOLLET)
17
18 try:
19 connections = {}; requests = {}; responses = {}
20 while True:
21 events = epoll.poll(1)
22 for fileno, event in events:
23 if fileno == serversocket.fileno():
24 try:
25 while True:
26 connection, address = serversocket.accept()
27 connection.setblocking(0)
28 epoll.register(connection.fileno(), select.EPOLLIN | select.EPOLLET)
29 connections[connection.fileno()] = connection
30 requests[connection.fileno()] = b''
31 responses[connection.fileno()] = response
32 except socket.error:
33 pass
34 elif event & select.EPOLLIN:
35 try:
36 while True:
37 requests[fileno] += connections[fileno].recv(1024)
38 except socket.error:
39 pass
40 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
41 epoll.modify(fileno, select.EPOLLOUT | select.EPOLLET)
42 print('-'*40 + '\n' + requests[fileno].decode()[:-2])
43 elif event & select.EPOLLOUT:
44 try:
45 while len(responses[fileno]) > 0:
46 byteswritten = connections[fileno].send(responses[fileno])
47 responses[fileno] = responses[fileno][byteswritten:]
48 except socket.error:
49 pass
50 if len(responses[fileno]) == 0:
51 epoll.modify(fileno, select.EPOLLET)
52 connections[fileno].shutdown(socket.SHUT_RDWR)
53 elif event & select.EPOLLHUP:
54 epoll.unregister(fileno)
55 connections[fileno].close()
56 del connections[fileno]
57 finally:
58 epoll.unregister(serversocket.fileno())
59 epoll.close()
60 serversocket.close()

由于例 3 和例 4 的相似性,在移植使用了 select 或 poll 机制的程序时,通常选择水平触发模式 ,但是边缘触发模式在程序员不需要或不关心太多的操作系统管理的网络事件时变得尤为重要。

除了使用 以上两种触发方式,我们还可以选择 EPOLLONESHOT 事件掩码,该使用该选项时,注册的事件在一次调用 epoll.poll() 后只会触发一次,此后该类型的事件自动地从被监听的套接字所注册的事件列表中移除。

Performance Considerations

监听队列大小

在例 1 - 4中, 12 行都调用了 serversocket.listen()。该方法的阐述是被监听的套接字的个数,它告知操作系统最大能够接受多少个 TCP/IP 连接(TCP/IP 积压队列),每次 Python 程序调用在服务器端套接字上调用 accept() 时,其中的一个连接就会从队列中移除此时该连接槽可以被其他连接使用,当连接队列已满时,新的连接请求将会被忽略,导致客户端网络连接不必要的延迟,成熟的服务器通常可以并行处理上万的并发连接,因此仅仅设置为 1 肯定是不够的,例如,在利用 ab 对上面的例子模拟负载测试时,当有100个 HTTP 1.0 并发客户端进行请求时,小于 50 的套接字监听队列值通常会导致一定的性能损失。

TCP 选项

TCP_CORK 选项可以用来暂存信息,直到消息被发送出去。该选项在例 5 的 34 - 40 行处展示如何使用,可以作为 HTTP/1.1服务器的一个编程选项。

例 5

 1 import socket, select
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13 serversocket.setblocking(0)
14
15 epoll = select.epoll()
16 epoll.register(serversocket.fileno(), select.EPOLLIN)
17
18 try:
19 connections = {}; requests = {}; responses = {}
20 while True:
21 events = epoll.poll(1)
22 for fileno, event in events:
23 if fileno == serversocket.fileno():
24 connection, address = serversocket.accept()
25 connection.setblocking(0)
26 epoll.register(connection.fileno(), select.EPOLLIN)
27 connections[connection.fileno()] = connection
28 requests[connection.fileno()] = b''
29 responses[connection.fileno()] = response
30 elif event & select.EPOLLIN:
31 requests[fileno] += connections[fileno].recv(1024)
32 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
33 epoll.modify(fileno, select.EPOLLOUT)
34 connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 1)
35 print('-'*40 + '\n' + requests[fileno].decode()[:-2])
36 elif event & select.EPOLLOUT:
37 byteswritten = connections[fileno].send(responses[fileno])
38 responses[fileno] = responses[fileno][byteswritten:]
39 if len(responses[fileno]) == 0:
40 connections[fileno].setsockopt(socket.IPPROTO_TCP, socket.TCP_CORK, 0)
41 epoll.modify(fileno, 0)
42 connections[fileno].shutdown(socket.SHUT_RDWR)
43 elif event & select.EPOLLHUP:
44 epoll.unregister(fileno)
45 connections[fileno].close()
46 del connections[fileno]
47 finally:
48 epoll.unregister(serversocket.fileno())
49 epoll.close()
50 serversocket.close()

另一方面, TCP_NODELAY 选项用来告知操作系统 socket.send() 发送的数据应该立即发送出去,而不是缓存起来,该选项在例 6 的 14行代码处展示如何使用,可以作为编写SSH客户端或其他实时性较高的网络程序时的选项。

例 6

 1 import socket, select
2
3 EOL1 = b'\n\n'
4 EOL2 = b'\n\r\n'
5 response = b'HTTP/1.0 200 OK\r\nDate: Mon, 1 Jan 1996 01:01:01 GMT\r\n'
6 response += b'Content-Type: text/plain\r\nContent-Length: 13\r\n\r\n'
7 response += b'Hello, world!'
8
9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
11 serversocket.bind(('0.0.0.0', 8080))
12 serversocket.listen(1)
13 serversocket.setblocking(0)
14 serversocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
15
16 epoll = select.epoll()
17 epoll.register(serversocket.fileno(), select.EPOLLIN)
18
19 try:
20 connections = {}; requests = {}; responses = {}
21 while True:
22 events = epoll.poll(1)
23 for fileno, event in events:
24 if fileno == serversocket.fileno():
25 connection, address = serversocket.accept()
26 connection.setblocking(0)
27 epoll.register(connection.fileno(), select.EPOLLIN)
28 connections[connection.fileno()] = connection
29 requests[connection.fileno()] = b''
30 responses[connection.fileno()] = response
31 elif event & select.EPOLLIN:
32 requests[fileno] += connections[fileno].recv(1024)
33 if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
34 epoll.modify(fileno, select.EPOLLOUT)
35 print('-'*40 + '\n' + requests[fileno].decode()[:-2])
36 elif event & select.EPOLLOUT:
37 byteswritten = connections[fileno].send(responses[fileno])
38 responses[fileno] = responses[fileno][byteswritten:]
39 if len(responses[fileno]) == 0:
40 epoll.modify(fileno, 0)
41 connections[fileno].shutdown(socket.SHUT_RDWR)
42 elif event & select.EPOLLHUP:
43 epoll.unregister(fileno)
44 connections[fileno].close()
45 del connections[fileno]
46 finally:
47 epoll.unregister(serversocket.fileno())
48 epoll.close()
49 serversocket.close()

(全文完)