之前异步IO一直没搞明白,大致的理解就是在一个大的循环中,有两部分:第一部分是监听事件;第二部分是处理事件(通过添加回调函数的方式)。就拿网络通信来说,可以先通过调用 select 模块中的 select 监听各个 socket 。当 socket 有事件到来时,针对相应的事件做出处理,就这么一直循环下去。所以异步IO也被称为事件驱动IO。原理其实我说得太简单了,所以我会以一个例子来说明一切。不过在这之前我还是要说一下 select 和 epoll 的区别。

一、IO多路服用的select

  IO多路复用相对于阻塞式和非阻塞式的好处就是它可以监听多个 socket ,并且不会消耗过多资源。当用户进程调用 select 时,它会监听其中所有 socket 直到有一个或多个 socket 数据已经准备好,否则就一直处于阻塞状态。select的缺点在于单个进程能够监视的文件描述符的数量存在最大限制,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的的开销也线性增长。同时,由于网络响应时间的延迟使得大量的tcp链接处于非常活跃状态,但调用select()会对所有的socket进行一次线性扫描,所以这也浪费了一定的开销。不过它的好处还有就是它的跨平台特性。

二、 异步IO的epoll

  epoll的优点就是完全的异步,你只需要对其中 poll 函数注册相应的 socket 和事件,就可以完全不管。当有时间发生时,数据已经从内核态拷贝到用户态,也就是完全没有阻塞。

三、基于epoll的聊天室程序

  说了这么多,我决定还是用epoll写一个多人聊天程序。epoll可以支持大量连接,select却有限制,所以这就是我决定用epoll的原因。首先看服务器程序:

 import socket, select
# 服务端 serverSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serverSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
serverSock.bind(('127.0.0.1', 8001))
serverSock.listen(5)
serverSock.setblocking(False) EOL = bytes('\n\n', 'utf-8')
QUIT = bytes('\r\n', 'utf-8')
epoll = select.epoll()
epoll.register(serverSock.fileno(), select.EPOLLIN)
print('注册事件:%s'%serverSock.fileno()) try:
connections = {}; requests = {}; responses = {}
while True:
events = epoll.poll(1)
for fileno, event in events:
# print('event:%s fileno:%s'%(event, fileno))
if fileno == serverSock.fileno():
clientSock, address = serverSock.accept()
print('连接到客户端: %s:%s'%(address[0], address[1]))
clientSock.setblocking(False)
connections[clientSock.fileno()] = (clientSock, address)
epoll.register(clientSock.fileno(), select.EPOLLOUT) # socket只能注册输入或输出一个,不能同时注册
requests[clientSock.fileno()] = bytes('', 'utf-8')
responses[clientSock.fileno()] = bytes('你已连接到服务器,IP为{}:{}\n\n'.format(*serverSock.getsockname()),
'utf-8')
elif event & select.EPOLLIN:
requests[fileno] += connections[fileno][0].recv(1024)
if requests[fileno].endswith(EOL):
msg = str(requests[fileno], 'utf-8')
msg = '来自{}的消息:{}'.format(connections[fileno][1], msg[:-2])
requests[fileno] = b''
#print(msg)
for i in responses:
if i == fileno:
continue
responses[i] += bytes(msg, 'utf-8')
epoll.modify(i, select.EPOLLOUT)
if QUIT in requests[fileno]:
epoll.modify(fileno, select.EPOLLOUT) elif event & select.EPOLLOUT:
#print('开始发送消息:%s'%str(responses[fileno], 'utf-8'))
bytesSend = connections[fileno][0].send(responses[fileno])
responses[fileno] = responses[fileno][bytesSend:]
#print('发送完成')
if responses[fileno] == b'':
epoll.modify(fileno, select.EPOLLIN)
if QUIT in requests[fileno]:
epoll.modify(fileno, 0)
connections[fileno][0].shutdown(socket.SHUT_RDWR) elif event & select.EPOLLHUP:
epoll.unregister(fileno)
connections[fileno][0].close()
del connections[fileno]
finally:
epoll.unregister(serverSock.fileno())
epoll.close()
serverSock.close()
print('已退出服务端程序')

注意,我首先定义了两个终止符:EOL表示这段话已经发完了;QUIT表示客户端想要退出。客户端的程序有点让我为难,既要在命令行输入又要同时保证能输出别人发过来的消息,所有我只好用了prompt_toolkit再加上一个线程。如下:

 import socket, prompt_toolkit, select
import threading, queue class Client:
def __init__(self, sock):
self.sock = sock
self.want_to_send = False
self.want_to_recv = True
self._msg = queue.Queue() def fileno(self):
return self.sock.fileno() def handle_recv(self):
print('接受消息..')
msg = self.sock.recv(1024)
print(str(msg, 'utf-8')) def handle_send(self):
msg = self._msg.get()
if msg == '\r\n':
self.want_to_send = False
self.want_to_recv = False
self.sock.sendall(bytes(msg, 'utf-8'))
self.want_to_send = False def handle_sock(want_to_send, want_to_recv, sock):
print('开始处理消息...')
want_to_recv.append(sock.fileno())
while True:
if sock.want_to_send:
if not want_to_send:
want_to_send.append(myclient.fileno())
else:
want_to_send.clear()
can_recv, can_send, _ = select.select(want_to_recv, want_to_send, [], 1)
if can_recv:
sock.handle_recv()
if can_send:
sock.handle_send()
if not (sock.want_to_send or sock.want_to_recv):
print('正停止客户端连接...')
break
if sock._msg.qsize():
sock.want_to_send = True s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1',8001)) myclient = Client(s)
want_to_send = []
want_to_recv = [] t = threading.Thread(target=handle_sock,
args=(want_to_send, want_to_recv, myclient),
daemon=True)
t.start() try:
while True:
messages = prompt_toolkit.shortcuts.prompt('\n\n>>> ',patch_stdout=True)
myclient._msg.put(messages+'\n\n')
except KeyboardInterrupt:
myclient._msg.put('\r\n')
finally:
t.join()
myclient.sock.close()
print('网络已断开')

我的服务器跑在 jupyter 上,客户端跑在命令行上,效果如下:

  客户端接受和发送消息都是互不影响的,这样就实现了一个多人聊天的功能。而且服务器使用的是epoll,所以哪怕是成千上万的人同时在线也没有任何压力。至于怎么测试暂时还没想到办法。

利用epoll实现异步IO的更多相关文章

  1. Select、Poll、Epoll、 异步IO 介绍

    一.概念相关介绍 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的net ...

  2. Python之路-python(Queue队列、进程、Gevent协程、Select\Poll\Epoll异步IO与事件驱动)

    一.进程: 1.语法 2.进程间通讯 3.进程池 二.Gevent协程 三.Select\Poll\Epoll异步IO与事件驱动 一.进程: 1.语法 简单的启动线程语法 def run(name): ...

  3. Python自动化 【第十篇】:Python进阶-多进程/协程/事件驱动与Select\Poll\Epoll异步IO

    本节内容: 多进程 协程 事件驱动与Select\Poll\Epoll异步IO   1.  多进程 启动多个进程 进程中启进程 父进程与子进程 进程间通信 不同进程间内存是不共享的,要想实现两个进程间 ...

  4. python的协程和异步io【select|poll|epoll】

    协程又叫做微线程,协程是一种用户态的轻量级的线程,操作系统根本就不知道协程的存在,完全由用户来控制,协程拥有自己的的寄存器的上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来后, ...

  5. IO模型之IO多路复用 异步IO select poll epoll 的用法

    IO 模型之 多路复用 IO 多路复用IO IO multiplexing 这个词可能有点陌生,但是如果我说 select/epoll ,大概就都能明白了.有些地方也称这种IO方式为 事件驱动IO ( ...

  6. [.NET] 利用 async & await 进行异步 IO 操作

    利用 async & await 进行异步 IO 操作 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6082673.html  序 上次,博主 ...

  7. Select/Poll/Epoll异步IO

    IO多路复用 同步io和异步io,阻塞io和非阻塞io分别是什么,有什么样的区别? io模式 对于一次io 访问(以read为例),数据会先拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷 ...

  8. Select\Poll\Epoll异步IO与事件驱动

    事件驱动与异步IO 事件驱动编程是一种编程规范,这里程序的执行流由外部事件来规定.它的特点是包含一个事件循环,但外部事件发生时使用回调机制来触发响应的处理.另外两种常见的编程规范是(单线程)同步以及多 ...

  9. Day11-协程/异步IO/RabbitMQ

    协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候 ...

随机推荐

  1. (2.0)Smali系列学习之Smali语法

    一.smali的包中信息 .class  public Lcom/aaaaa;.super  Lcom/bbbbb;.source "ccccc.java" 1.它是com.aaa ...

  2. 动态添加定时任务-quartz定时器

    Quartz动态添加.修改和删除定时任务 在项目中有一个需求,需要灵活配置调度任务时间,刚开始用的Java自带的java.util.Timer类,通过调度一个java.util.TimerTask任务 ...

  3. 通过ip查看主机名和端口占用情况

      1. 知道对方ip查看对方的计算机名 方法:开始->运行->cmd->net view 对方ip 或者 开始->运行->cmd->nbtstat -a 对方ip ...

  4. 1 App Components - App组件

    Android应用框架可以让你使用一系列可重用的组件来创建极其丰富和新颖的应用.本节说明了Android应用如何工作,如何使用组件来创建应用. Managing the Activity Lifecy ...

  5. What is Web Application Architecture? How It Works, Trends, Best Practices and More

    At Stackify, we understand the amount of effort that goes into creating great applications. That’s w ...

  6. C++Primer学习笔记《2》

    数组是一种复合类型,由类型名+数组名+维度组成. 数组定义中的类型能够是C++基本内置类型.也能够是类类型的.数组元素的类型能够是除了引用类型以外的其它不论什么类型.没有全部的元素都是引用的数组. 数 ...

  7. Python_ip代理

    #encoding=utf8import urllibimport urllib2import sys sys.path.append('D:/python/beautifulsoup')sys.pa ...

  8. C++ 抽象类二(抽象类的基本语法)

    //抽象类的基本语法 #include<iostream> using namespace std; /* 有关多继承的说明 被实际开发经验抛弃的多继承 工程开发中真正意义上的多继承是几乎 ...

  9. java 理解java的三大特性之封装

    使用封装有三大好处: 1.良好的封装能够减少耦合. 2.类内部的结构可以自由修改. 3.可以对成员进行更精确的控制. 4.隐藏信息,实现细节. public class Wife { private ...

  10. 使用Using的注意事项

    参数传递 C#中有四种参数类型:值类型,Ref参数,Out参数,params参数.默认参数都是以传值方式传递,这意味着方法中的变量会在内存中被分配新的存储空间,并赋值.对于引用类型,这种传值意味着传递 ...