python网络编程——IO多路复用之epoll

来源:http://www.cnblogs.com/maociping/p/5132583.html

1、内核EPOLL模型讲解

    此部分参考http://blog.csdn.net/mango_song/article/details/42643971博文并整理

   
首先我们来定义流的概念,一个流可以是文件,socket,pipe等可以进行I/O操作的内核对象。不管是文件,还是套接字(socket),还是管道(pipe),我们都可以把他们看作流。

    之后我们来讨论I/O操作,通过read,我们可以从流中读入数据;通过write,我们可以往流中写入数据。现在假定1种情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读数据,但是服务器端还没有把数据传回来),这时候该怎么办?

   
阻塞:阻塞是个什么概念呢?比如某个时候你在等快递,但是你还不知道快递什么时候过来,而且你也没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打电话(假定一定能叫醒你)。

   
非阻塞忙轮询:接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他打个电话:“你到了没?”

    很明显一般人不会用第二种做法,不仅显得无脑,浪费话费不说,还占用了快递员大量的时间。

    大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片。

   
为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。

    假设有一个管道,进程A为管道的写入方,B为管道的读出方。假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”。也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

    这四种情形涵盖了四个I/O事件,内核缓冲区满,内核缓冲区空,内核缓冲区非空,内核缓冲区非满。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

   
然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。于是再来考虑非阻塞忙轮询的I/O方式,我们发现可以同时处理多个流(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):

1 while true {
2      for i in stream[]; {
3            if i has data
4            read until unavailable
5         }
6 }  

    我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

    为了避免CPU空转,可以引进一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

1 while true {
2       select(streams[])
3       for i in streams[] {
4             if i has data
5             read until unavailable
6        }
7 }  

    于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。

    但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次说了这么多,终于能好好解释epoll了。

    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll只会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的(复杂度降低到了O(1))。

    在讨论epoll的实现细节之前,先把epoll的相关操作列出:

1 epoll_create创建一个epoll对象,一般epollfd = epoll_create()
2 epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
3  比如
4 epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入
5 epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入
6 epoll_wait(epollfd,...)等待直到注册的事件发生
7 (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。  

 一个epoll模式的代码大概的样子是:



  1. while true {  
  2.     active_stream[] = epoll_wait(epollfd)  
  3.     for i in active_stream[] {  
  4.         read or write till  
  5.     }  
  6.  } 

2 python中的epoll

   从以上可知,epoll是对select、poll模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的C/S架构中。

  1、触发方式:

     边缘触发/水平触发,只适用于Unix/Linux操作系统

   2、原理图

  3、一般步骤

  1. Create an epoll object——创建1个epoll对象
  2. Tell the epoll object to monitor specific events on specific sockets——告诉epoll对象,在指定的socket上监听指定的事件
  3. Ask the epoll object which sockets may have had the specified event since the last query——询问epoll对象,从上次查询以来,哪些socket发生了哪些指定的事件
  4. Perform some action on those sockets——在这些socket上执行一些操作
  5. Tell the epoll object to modify the list of sockets and/or events to monitor——告诉epoll对象,修改socket列表和(或)事件,并监控
  6. Repeat steps 3 through 5 until finished——重复步骤3-5,直到完成
  7. Destroy the epoll object——销毁epoll对象

  4、相关用法


import select
导入select模块

epoll = select.epoll()创建一个epoll对象

epoll.register(文件句柄,事件类型)注册要监控的文件句柄和事件

事件类型:

  select.EPOLLIN    可读事件

  select.EPOLLOUT   可写事件

  select.EPOLLERR   错误事件

  select.EPOLLHUP   客户端断开事件

epoll.unregister(文件句柄)  销毁文件句柄

epoll.poll(timeout) 当文件句柄发生变化,则会以列表的形式主动报告给用户进程,timeout

                     为超时时间,默认为-1,即一直等待直到文件句柄发生变化,如果指定为1

                     那么epoll每1秒汇报一次当前文件句柄的变化情况,如果无变化则返回空

epoll.fileno()
返回epoll的控制文件描述符(Return the epoll control file descriptor)

epoll.modfiy(fineno,event)fineno为文件描述符 event为事件类型  作用是修改文件描述符所对应的事件

epoll.fromfd(fileno)从1个指定的文件描述符创建1个epoll对象

epoll.close()  
关闭epoll对象的控制文件描述符

   5 实例:客户端发送数据 服务端将接收的数据返回给客户端

 1 #!/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3
 4 import socket
 5 import select
 6 import Queue
 7
 8 #创建socket对象
 9 serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 #设置IP地址复用
11 serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
12 #ip地址和端口号
13 server_address = ("127.0.0.1", 8888)
14 #绑定IP地址
15 serversocket.bind(server_address)
16 #监听,并设置最大连接数
17 serversocket.listen(10)
18 print  "服务器启动成功,监听IP:" , server_address
19 #服务端设置非阻塞
20 serversocket.setblocking(False)
21 #超时时间
22 timeout = 10
23 #创建epoll事件对象,后续要监控的事件添加到其中
24 epoll = select.epoll()
25 #注册服务器监听fd到等待读事件集合
26 epoll.register(serversocket.fileno(), select.EPOLLIN)
27 #保存连接客户端消息的字典,格式为{}
28 message_queues = {}
29 #文件句柄到所对应对象的字典,格式为{句柄:对象}
30 fd_to_socket = {serversocket.fileno():serversocket,}
31
32 while True:
33   print "等待活动连接......"
34   #轮询注册的事件集合,返回值为[(文件句柄,对应的事件),(...),....]
35   events = epoll.poll(timeout)
36   if not events:
37      print "epoll超时无活动连接,重新轮询......"
38      continue
39   print "有" , len(events), "个新事件,开始处理......"
40
41   for fd, event in events:
42      socket = fd_to_socket[fd]
43      #如果活动socket为当前服务器socket,表示有新连接
44      if socket == serversocket:
45             connection, address = serversocket.accept()
46             print "新连接:" , address
47             #新连接socket设置为非阻塞
48             connection.setblocking(False)
49             #注册新连接fd到待读事件集合
50             epoll.register(connection.fileno(), select.EPOLLIN)
51             #把新连接的文件句柄以及对象保存到字典
52             fd_to_socket[connection.fileno()] = connection
53             #以新连接的对象为键值,值存储在队列中,保存每个连接的信息
54             message_queues[connection]  = Queue.Queue()
55      #关闭事件
56      elif event & select.EPOLLHUP:
57         print 'client close'
58         #在epoll中注销客户端的文件句柄
59         epoll.unregister(fd)
60         #关闭客户端的文件句柄
61         fd_to_socket[fd].close()
62         #在字典中删除与已关闭客户端相关的信息
63         del fd_to_socket[fd]
64      #可读事件
65      elif event & select.EPOLLIN:
66         #接收数据
67         data = socket.recv(1024)
68         if data:
69            print "收到数据:" , data , "客户端:" , socket.getpeername()
70            #将数据放入对应客户端的字典
71            message_queues[socket].put(data)
72            #修改读取到消息的连接到等待写事件集合(即对应客户端收到消息后,再将其fd修改并加入写事件集合)
73            epoll.modify(fd, select.EPOLLOUT)
74      #可写事件
75      elif event & select.EPOLLOUT:
76         try:
77            #从字典中获取对应客户端的信息
78            msg = message_queues[socket].get_nowait()
79         except Queue.Empty:
80            print socket.getpeername() , " queue empty"
81            #修改文件句柄为读事件
82            epoll.modify(fd, select.EPOLLIN)
83         else :
84            print "发送数据:" , data , "客户端:" , socket.getpeername()
85            #发送数据
86            socket.send(msg)
87
88 #在epoll中注销服务端文件句柄
89 epoll.unregister(serversocket.fileno())
90 #关闭epoll
91 epoll.close()
92 #关闭服务器socket
93 serversocket.close()

 1 #!/usr/bin/env python
 2 #-*- coding:utf-8 -*-
 3
 4 import socket
 5
 6 #创建客户端socket对象
 7 clientsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 8 #服务端IP地址和端口号元组
 9 server_address = ('127.0.0.1',8888)
10 #客户端连接指定的IP地址和端口号
11 clientsocket.connect(server_address)
12
13 while True:
14     #输入数据
15     data = raw_input('please input:')
16     #客户端发送数据
17     clientsocket.sendall(data)
18     #客户端接收数据
19     server_data = clientsocket.recv(1024)
20     print '客户端收到的数据:'server_data
21     #关闭客户端socket
22     clientsocket.close() 

 

参考资料:

      http://blog.csdn.net/mango_song/article/details/42643971

      http://www.cnblogs.com/Alanpy/articles/5125986.html

      http://scotdoyle.com/python-epoll-howto.html

      http://www.haiyun.me/archives/1056.html

时间: 2024-07-30 13:07:00

python网络编程——IO多路复用之epoll的相关文章

python网络编程之数据传输UDP实例分析

  本文实例讲述了python网络编程之数据传输UDP实现方法.分享给大家供大家参考.具体分析如下: 一.问题: 你觉得网络上像msn,qq之类的工具在多台机器之间互相传输数据神秘吗?你也想玩一下在两台机器之间传数据吗?今天让python告诉我们基本原理吧,当然只是做简单的了解,实际情况复杂的多. 我们今天用python实现一个简单的udp程序. 二.程序实现: 1) 使用模块 (socket)套接字模块: 套接字模块是一个非常简单的基于对象的接口,它提供对低层BSD套接字样式网络的访问 .使用

python网络编程之文件下载实例分析

  本文实例讲述了python网络编程之文件下载实现方法.分享给大家供大家参考.具体如下: 真是越看越喜欢python啊,想要了解它提供的http和ftp下载功能,原来是如此的简单. 1.相应模块 ftplib模块定义了FTP类和一些方法,用以进行客户端的ftp编程.我们可用python编写一个自已的ftp客户端程序,用于下载文件或镜像站点.如果想了解ftp协议的详细内容,请参考RFC959或是查看python帮助吧. Urllib模块提供了非常高级的接口来从网络上抓取数据,主要使用到的是url

使用rpclib进行Python网络编程时的注释问题

       这篇文章主要介绍了使用rpclib进行Python网络编程时的注释问题,作者讲到了自己在编写服务器时要用unicode注释等需要注意的地方,需要的朋友可以参考下            rpclib 是一个非常好用的 python webservice 库,可以动态的生成 wsdl, 不过这个项目已经基本停止,并被一个新的项目取代 spyne,由于旧的项目 工作已经比较稳定,所以我没有贸然升级到 spyne.         我在 rpclib 编写 service 方法时,遇到一个

python网络编程之读取网站根目录实例_python

本文实例讲述了python网络编程之读取网站根目录的方法,分享给大家供大家参考. 具体实现方法如下: import socket, sys port = 70 host = "quux.org" filename = "//" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s.sendall(filename+"\r\n") while(

python网络编程实例简析_python

本文实例讲述了python网络编程,分享给大家供大家参考. 具体方法如下: 服务端代码如下: from SocketServer import(TCPServer as TCP, StreamRequestHandler as SRH) from time import ctime HOST = '' PORT = 21567 ADDR = (HOST, PORT) class MyRequestHandle(SRH): def handle(self): print 'connecting f

python网络编程学习笔记(二):socket建立网络客户端_python

1.建立socket 建立socket对象需要搞清通信类型和协议家族.通信类型指明了用什么协议来传输数据.协议的例子包括IPv4.IPv6.IPX\SPX.AFP.对于internet通信,通信类型基本上都是AF_INET(和IPv4对应).协议家族一般表示TCP通信的SOCK_STREAM或者表示UDP通信的SOCK_DGRAM.因此对于TCP通信,建立一个socket连接的语句为:s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)对于UDP通

python 网络编程常用代码段_python

服务器端代码: # -*- coding: cp936 -*- import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#初始化socket sock.bind(("127.0.0.1", 8001))#绑定本机地址,8001端口 sock.listen(5)#等待客户连接 while True: print "waiting client connection..." connec

python网络编程学习笔记(一)_python

学习用书:<python 网络编程基础>作者John Goerzen 第一部分底层网络学习         Python提供了访问底层操作系统Socket接口的全部方法,需要的时候这些接口可以提供灵活而强有力的功能. (1)基本客户端操作         在<python 网络编程基础>一书中,作者列出了一个简单的Python客户端程序,具体如下: 复制代码 代码如下: import socket,sysport =70host=sys.argv[1] filename=sys.a

IO多路复用之epoll总结

1.基本知识 epoll是在2.6内核中提出的,是之前的select和poll的增强版本.相对于select和poll来说,epoll更加灵活,没有描述符限制.epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次. 2.epoll接口 epoll操作过程需要三个接口,分别如下: #include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(