在Python中如何使用Linux的epoll


阻塞socket编程示例

示例1用python3.0搭建了一个简单的服务:在8080端口监听HTTP请求,把它打印到控制台,并返回一个HTTP响应消息给客户端。

  1. 第9行:创建服务器socket。
  2. 第10行:允许在11行使用bind()来监听指定端口,即使这个端口最近被其他程序监听。没有这个设置的话,服务不能运行,直到一两分钟后,这个端口不再被之前的程序使用。
  3. 第11行:监听这台机器所有可用的IPv4地址上面的8080端口。
  4. 第12行:通知服务端socket开始接受来自客户端的连接。
  5. 第 14行:这行代码直到接收到一个客户端连接才会完成。这时,服务端socket会在服务端机器上面创建一个新的socket,用来和客户端通信。这个新的 socket在代码里面就是accept()调用返回的clientconnection 对象。返回的address对象代表着客户端的IP和端口。
  6. 第15-17行:组装从客户端传输过来的数据,直到HTTP请求完成。HTTP协议可以参考这里
  7. 第18行:把请求打印到控制台,验证操作是否正确。
  8. 第19行:发送响应回客户端。
  9. 第20-22行:关闭和客户端的连接以及服务端监听socket。

(All examples use Python 3)

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行增加了一个循环来不断的处理来自客户端的连接,直到用户中断(比如键盘中断)。这个例子更清楚的说明服务端socket从不和客户端交换数据。相反的,它接收客户端的连接,然后在这台服务器上面创建一个新的socket用来和客户端通信。

在23-24行的finally语句,可以确保服务端负责监听的socket会关闭,即使有异常发生。

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叫做阻塞socket,因为python程序会停止运行,直到一个event发生。16行的accept()调用会阻塞,直到接收到 一个客户端连接。19行的recv()调用会阻塞,直到这次接收客户端数据完成(或者没有更多的数据要接收)。21行的send()调用也会阻塞,直到将 这次需要返回给客户端的数据都放到Linux的发送缓冲队列中。

当 一个程序使用阻塞socket时,常常使用一个线程(甚至是一个专门的程序)来进行各个socket之间的通信。主程序线程会包含接收客户端连接的服务端 监听socket。这个socket一次接收一个客户端连接,把连接传给另外一个线程新建的socket去处理。因为这些线程每个只和一个客户端通信,所 以处理时即便在某几个点阻塞也没有关系。这种阻塞并不会对其他线程的处理造成任何影响。

使用多线程、阻塞socket来处理的话,代码会很直观,但是也会有不少缺陷。它很难确保线程共享资源没有问题。而且这种编程风格的程序在只有一个CPU的电脑上面效率更低。

C10K问题探讨了一些替代选择,其一是使用示例3重复了示例2的功能,同时使用异步socket。这个程序更为复杂,因为一个线程要交错与多个客户端通信。

  1. 第1行:select模块包含epoll功能。
  2. 第13行:因为socket默认是阻塞的,所以需要使用非阻塞(异步)模式。
  3. 第15行:创建一个epoll对象。
  4. 第16行:在服务端socket上面注册对读event的关注。一个读event随时会触发服务端socket去接收一个socket连接。
  5. 第19行:字典connections映射文件描述符(整数)到其相应的网络连接对象。
  6. 第21行:查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,我们会等待1秒来看是否有event发生。如果有任何我们感兴趣的event发生在这次查询之前,这个查询就会带着这些event的列表立即返回。
  7. 第22行:event作为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。
  8. 第23行:如果一个读event在服务端sockt发生,就会有一个新的socket连接可能被创建。
  9. 第25行:设置新的socket为非阻塞模式。
  10. 第26行:为新的socket注册对读(EPOLLIN)event的关注。
  11. 第31行:如果发生一个读event,就读取从客户端发送过来的新数据。
  12. 第33行:一旦完成请求已收到,就注销对读event的关注,注册对写(EPOLLOUT)event的关注。写event发生的时候,会回复数据给客户端。
  13. 第34行:打印完整的请求,证明虽然与客户端的通信是交错进行的,但数据可以作为一个整体来组装和处理。
  14. 第35行:如果一个写event在一个客户端socket上面发生,它会接受新的数据以便发送到客户端。
  15. 第36-38行:每次发送一部分响应数据,直到完整的响应数据都已经发送给操作系统等待传输给客户端。
  16. 第39行:一旦完整的响应数据发送完成,就不再关注读或者写event。
  17. 第40行:如果一个连接显式关闭,那么socket shutdown是可选的。本示例程序这样使用,是为了让客户端首先关闭。shutdown调用会通知客户端socket没有更多的数据应该被发送或接收,并会让功能正常的客户端关闭自己的socket连接。
  18. 第41行:HUP(挂起)event表明客户端socket已经断开(即关闭),所以服务端也需要关闭。没有必要注册对HUP event的关注。在socket上面,它们总是会被epoll对象注册。
  19. 第42行:注销对此socket连接的关注。
  20. 第43行:关闭socket连接。
  21. 第18-45行:使用try-catch,因为该示例程序最有可能被KeyboardInterrupt异常中断。
  22. 第46-48行:打开的socket连接不需要关闭,因为Python会在程序结束的时候关闭。这里显式关闭是一个好的代码习惯。

 

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有两种操作模式,称为水平触发 。在边沿触发模式中,epoll.poll()在读或者写event在socket上面发生后,将只会返回一次event。调用epoll.poll() 的程序必须处理所有和这个event相关的数据,随后的epoll.poll()调用不会再有这个event的通知。当一个特定event的数据耗尽时, 进一步尝试操作socket将导致一个异常。相反,在水平触发模式下,重复调用epoll.poll()会重复通知关注的event,直到与该event 有关的所有数据都已被处理。在水平模式下通常没有异常。

例如, 假设一个服务端socket已经为一个epoll对象注册了读event。在边沿触发模式下,程序需要一直accept()新的socket连接,直到一 个socket.error的异常发生。而在水平触发模式下,一个accept()调用后,epoll对象会被服务端socket再次询问是否有新的 event,以确定下一个accept()是否应该被调用。

示 例3使用水平触发模式,这是操作的默认模式。示例4演示了如何使用边沿触发模式。在示例4中,第25,36和45行引入循环,直到出现异常才退出(或者所 有其他已知的数据都被处理)。第32,38和48行捕获预期的socket异常。最后,第16,28,41和51行添加EPOLLET掩码,用来设置为边 沿触发模式。

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机制的应用程序时,而边沿触发模式可以用在当程序员不需要或不想要操作系统协助管理event状态时。

除了这两种操作模式,epoll对象也可以注册socket使用EPOLLONESHOTevent掩码。当使用这个选项时,注册的event只适用于一个epoll.poll()调用,调用之后它会自动从被监视的socket注册列表中移除。

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()


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值