上篇回顾:静态服务器+压测

3.2.概念篇

1.同步与异步

同步是指一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成。

异步是指不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作。然后继续执行下面代码逻辑,只要自己完成了整个任务就算完成了(异步一般使用状态、通知和回调)

PS:项目里面一般是这样的:(个人经验)

  1. 同步架构:一般都是和钱相关的需求,需要实时返回的业务
  2. 异步架构:更多是对写要求比较高时的场景(同步变异步)
    • 读一般都是实时返回,代码一般都是await xxx()
  3. 想象个情景就清楚了:
    • 异步:现在用户写了篇文章,可以异步操作,就算没真正写到数据库也可以返回:发表成功(大不了失败提示一下)
    • 同步:用户获取订单信息,你如果异步就会这样了:提示下获取成功,然后一片空白...用户不卸载就怪了...

2.阻塞与非阻塞

阻塞是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务(大部分代码都是这样的)

非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回(继续执行下面代码,或者重试机制走起)

PS:项目里面重试机制为啥一般都是3次?

  1. 第一次重试,两台PC挂了也是有可能的
  2. 第二次重试,负载均衡分配的三台机器同时挂的可能性不是很大,这时候就有可能是网络有点拥堵了
  3. 最后一次重试,再失败就没意义了,日记写起来,再重试网络负担就加大了,得不偿失了

3.五种IO模型

对于一次IO访问,数据会先被拷贝到内核的缓冲区中,然后才会从内核的缓冲区拷贝到应用程序的地址空间。需要经历两个阶段:

  1. 准备数据
  2. 将数据从内核缓冲区拷贝到进程地址空间

由于存在这两个阶段,Linux产生了下面五种IO模型(以socket为例

  1. 阻塞式IO:

    • 当用户进程调用了recvfrom等阻塞方法时,内核进入IO的第1个阶段:准备数据(内核需要等待足够的数据再拷贝)这个过程需要等待,用户进程会被阻塞,等内核将数据准备好,然后拷贝到用户地址空间,内核返回结果,用户进程才从阻塞态进入就绪态
    • Linux中默认情况下所有的socket都是阻塞的
  2. 非阻塞式IO:
    • 当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error
    • 用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作
    • 一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回
    • 非阻塞IO模式下用户进程需要不断地询问内核的数据准备好了没有
  3. IO多路复用
    • 通过一种机制,一个进程可以监视多个文件描述符(套接字描述符)一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作(这样就不需要每个用户进程不断的询问内核数据准备好了没)
    • 常用的IO多路复用方式有selectpollepoll
  4. 信号驱动IO:
    • 内核文件描述符就绪后,通过信号通知用户进程,用户进程再通过系统调用读取数据。
    • 此方式属于同步IO(实际读取数据到用户进程缓存的工作仍然是由用户进程自己负责的)
  5. 异步IOPOSIXaio_系列函数)
    • 用户进程发起read操作之后,立刻就可以开始去做其它的事。内核收到一个异步IO read之后,会立刻返回,不会阻塞用户进程。
    • 内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个signal告诉它read操作完成了

4.Unix图示

贴一下Unix编程里面的图:

**非阻塞IO**
![2.非阻塞IO](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181210212858009-948984805.png)
**IO复用**
![3.IO复用](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181210212908314-1267377747.png)
**信号IO**
![4.信号IO](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181210212934040-13536334.png)
**异步AIO**
![5.异步AIO](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181210212944334-1184572641.png)

3.3.IO多路复用

开始之前咱们通过非阻塞IO引入一下:(来个简单例子socket.setblocking(False))

import time
import socket def select(socket_addr_list):
for client_socket, client_addr in socket_addr_list:
try:
data = client_socket.recv(2048)
if data:
print(f"[来自{client_addr}的消息:]\n")
print(data.decode("utf-8"))
client_socket.send(
b"HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>Web Server Test</h1>"
)
else:
# 没有消息是触发异常,空消息是断开连接
client_socket.close() # 关闭客户端连接
socket_addr_list.remove((client_socket, client_addr))
print(f"[客户端{client_addr}已断开连接,当前连接数:{len(socket_addr_list)}]")
except Exception:
pass def main():
# 存放客户端集合
socket_addr_list = list() with socket.socket() as tcp_server:
# 防止端口绑定的设置
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_server.bind(('', 8080))
tcp_server.listen()
tcp_server.setblocking(False) # 服务端非阻塞
while True:
try:
client_socket, client_addr = tcp_server.accept()
client_socket.setblocking(False) # 客户端非阻塞
socket_addr_list.append((client_socket, client_addr))
except Exception:
pass
else:
print(f"[来自{client_addr}的连接,当前连接数:{len(socket_addr_list)}]")
# 防止客户端断开后出错
if socket_addr_list:
# 轮询查看客户端有没有消息
select(socket_addr_list) # 引用传参
time.sleep(0.01) if __name__ == "__main__":
main()

输出:

可以思考下:

  1. 为什么Server也要设置为非阻塞?

    • PS:一个线程里面只能有一个死循环,现在程序需要两个死循环,so ==> 放一起咯
  2. 断开连接怎么判断?
    • PS:没有消息是触发异常,空消息是断开连接
  3. client_socket为什么不用dict存放?
    • PS:dict在循环的过程中,del会引发异常

1.Select

select和上面的有点类似,就是轮询的过程交给了操作系统:

kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程

来个和上面等同的案例:

import select
import socket def main():
with socket.socket() as tcp_server:
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_server.bind(('', 8080))
tcp_server.listen()
socket_info_dict = dict()
socket_list = [tcp_server] # 监测列表
while True:
# 劣势:select列表数量有限制
read_list, write_list, error_list = select.select(
socket_list, [], [])
for item in read_list:
# 服务端迎接新的连接
if item == tcp_server:
client_socket, client_address = item.accept()
socket_list.append(client_socket)
socket_info_dict[client_socket] = client_address
print(f"[{client_address}已连接,当前连接数:{len(socket_list)-1}]")
# 客户端发来
else:
data = item.recv(2048)
if data:
print(data.decode("utf-8"))
item.send(
b"HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>Web Server Test</h1>"
)
else:
item.close()
socket_list.remove(item)
info = socket_info_dict[item]
print(f"[{info}已断开,当前连接数:{len(socket_list)-1}]") if __name__ == "__main__":
main()

输出和上面一样

扩展说明:

select 函数监视的文件描述符分3类,分别是writefdsreadfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪函数返回(有数据可读、可写、或者有except)或者超时(timeout指定等待时间,如果立即返回设为null即可)

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024(64位=>2048)

然后Poll就出现了,就是把上限给去掉了,本质并没变,还是使用的轮询

2.EPoll

epoll在内核2.6中提出(Linux独有),使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,采用监听回调的机制,这样在用户空间和内核空间的copy只需一次,避免再次遍历就绪的文件描述符列表

先来看个案例吧:(输出和上面一样)

import socket
import select def main():
with socket.socket() as tcp_server:
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_server.bind(('', 8080))
tcp_server.listen() # epoll是linux独有的
epoll = select.epoll()
# tcp_server注册到epoll中
epoll.register(tcp_server.fileno(), select.EPOLLIN | select.EPOLLET) # key-value
fd_socket_dict = dict() # 回调需要自己处理
while True:
# 返回可读写的socket fd 集合
poll_list = epoll.poll()
for fd, event in poll_list:
# 服务器的socket
if fd == tcp_server.fileno():
client_socket, client_addr = tcp_server.accept()
fd = client_socket.fileno()
fd_socket_dict[fd] = (client_socket, client_addr)
# 把客户端注册进epoll中
epoll.register(fd, select.EPOLLIN | select.EPOLLET)
else: # 客户端
client_socket, client_addr = fd_socket_dict[fd]
data = client_socket.recv(2048)
print(
f"[来自{client_addr}的消息,当前连接数:{len(fd_socket_dict)}]\n")
if data:
print(data.decode("utf-8"))
client_socket.send(
b"HTTP/1.1 200 ok\r\nContent-Type: text/html;charset=utf-8\r\n\r\n<h1>Web Server Test</h1>"
)
else:
del fd_socket_dict[fd]
print(
f"[{client_addr}已离线,当前连接数:{len(fd_socket_dict)}]\n"
)
# 从epoll中注销
epoll.unregister(fd)
client_socket.close() if __name__ == "__main__":
main()

扩展:epoll的两种工作模式

LT(level trigger,水平触发)模式:当epoll_wait检测到描述符就绪,将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。LT模式是默认的工作模式。

LT模式同时支持阻塞和非阻塞socket。

ET(edge trigger,边缘触发)模式:当epoll_wait检测到描述符就绪,将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET是高速工作方式,只支持非阻塞socket(ET模式减少了epoll事件被重复触发的次数,因此效率要比LT模式高)

Code提炼一下

  1. 实例化对象:epoll = select.epoll()
  2. 注册对象:epoll.register(tcp_server.fileno(), select.EPOLLIN | select.EPOLLET)
  3. 注销对象:epoll.unregister(fd)

PS:epoll不一定比Select性能高,一般都是分场景的:

  1. 高并发下,连接活跃度不高时:epoll比Select性能高(eg:web请求,页面随时关闭)
  2. 并发不高,连接活跃度比较高:Select更合适(eg:小游戏)
  3. Select是win和linux通用的,而epoll只有linux有

其实IO多路复用还有一个kqueue,和epoll类似,下面的通用写法中有包含


3.通用写法(Selector

一般来说:Linux下使用epoll,Win下使用select(IO多路复用会这个通用的即可)

先看看Python源代码:

# 选择级别:epoll|kqueue|devpoll > poll > select
if 'KqueueSelector' in globals():
DefaultSelector = KqueueSelector
elif 'EpollSelector' in globals():
DefaultSelector = EpollSelector
elif 'DevpollSelector' in globals():
DefaultSelector = DevpollSelector
elif 'PollSelector' in globals():
DefaultSelector = PollSelector
else:
DefaultSelector = SelectSelector

实战案例:(可读和可写可以不分开)

import socket
import selectors # Linux下使用epoll,Win下使用select
Selector = selectors.DefaultSelector() class Task(object):
def __init__(self):
# 存放客户端fd和socket键值对
self.fd_socket_dict = dict() def run(self):
self.server = socket.socket()
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(('', 8080))
self.server.listen()
# 把Server注册到epoll
Selector.register(self.server.fileno(), selectors.EVENT_READ,
self.connected) def connected(self, key):
"""客户端连接时处理"""
client_socket, client_address = self.server.accept()
fd = client_socket.fileno()
self.fd_socket_dict[fd] = (client_socket, client_address)
# 注册一个客户端读的事件(服务端去读消息)
Selector.register(fd, selectors.EVENT_READ, self.call_back_reads)
print(f"{client_address}已连接,当前连接数:{len(self.fd_socket_dict)}") def call_back_reads(self, key):
"""客户端可读时处理"""
# 一个fd只能注册一次,监测可写的时候需要把可读给注销
Selector.unregister(key.fd)
client_socket, client_address = self.fd_socket_dict[key.fd]
print(f"[来自{client_address}的消息:]\n")
data = client_socket.recv(2048)
if data:
print(data.decode("utf-8"))
# 注册一个客户端写的事件(服务端去发消息)
Selector.register(key.fd, selectors.EVENT_WRITE,
self.call_back_writes)
else:
client_socket.close()
del self.fd_socket_dict[key.fd]
print(f"{client_address}已断开,当前连接数:{len(self.fd_socket_dict)}") def call_back_writes(self, key):
"""客户端可写时处理"""
Selector.unregister(key.fd)
client_socket, client_address = self.fd_socket_dict[key.fd]
client_socket.send(b"ok")
Selector.register(key.fd, selectors.EVENT_READ, self.call_back_reads) def main():
t = Task()
t.run()
while True:
ready = Selector.select()
for key, obj in ready:
# 需要自己回调
call_back = key.data
call_back(key) if __name__ == "__main__":
main()

Code提炼一下

  1. 实例化对象:Selector = selectors.DefaultSelector()
  2. 注册对象:
    • Selector.register(server.fileno(), selectors.EVENT_READ, call_back)
    • Selector.register(server.fileno(), selectors.EVENT_WRITE, call_back)
  3. 注销对象:Selector.unregister(key.fd)
  4. 注意一下:一个fd只能注册一次,监测可写的时候需要把可读给注销(反之一样)

业余拓展:

select, iocp, epoll,kqueue及各种I/O复用机制
https://blog.csdn.net/shallwake/article/details/5265287 kqueue用法简介
http://www.cnblogs.com/luminocean/p/5631336.html

下级预估:协程篇 or 网络深入篇

【经典】5种IO模型 | IO多路复用的更多相关文章

  1. Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)

    Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 背景 我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO中,对于其中的 阻塞/非阻塞IO 进行了 ...

  2. IO 模型 IO 多路复用

    IO 模型 IO 多路复用 IO多路复用:模型(解决问题的方案) 同步:一个任务提交以后,等待任务执行结束,才能继续下一个任务 异步:不需要等待任务执行结束, 阻塞:IO阻塞,程序卡住了 非阻塞:不阻 ...

  3. 五种网络IO模型以及多路复用IO中select/epoll对比

    下面都是以网络读数据为例 [2阶段网络IO] 第一阶段:等待数据 wait for data 第二阶段:从内核复制数据到用户 copy data from kernel to user 下面是5种网络 ...

  4. IO模型 IO多路复用

    阻塞IO 用socket 一定会用到accept recv recvfrom这些方法正常情况下 accept recv recvfrom都是阻塞的 非阻塞IO 如果setblocking(False) ...

  5. IO模型——IO多路复用机制

    (1)I/O多路复用技术通过把多个I/O的阻塞复用到同一个select.poll或epoll的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求.与传统的多线程/多进程模型比,I/O多路复 ...

  6. Python并发编程-IO模型-IO多路复用实现SocketServer

    Server.py import select import socket sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.setblockin ...

  7. Linux五种IO模型(同步 阻塞概念)

    Linux五种IO模型 同步和异步 这两个概念与消息的通知机制有关. 同步 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.比如,调用readfrom系统调用时,必须等待IO操 ...

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

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

  9. Linux 网络编程的5种IO模型:信号驱动IO模型

    Linux 网络编程的5种IO模型:信号驱动IO模型 背景 上一讲 Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 我们讲解了多路复用等方面的知识,以及有关例程. ...

随机推荐

  1. python全栈开发day60-django_app ORM 完整登录案例

    day60 内容回顾: 1. HTTP协议: 1.请求(浏览器发给服务端的消息——request) 请求方法 URL 协议版本\r\n k1:v1\r\n k2:v2\r\n \r\n 请求体 —— ...

  2. gitlab之三: gitlab邮件通知的配置

    参考 :  https://www.cnblogs.com/lovelinux199075/p/9072265.html gitlab 添加新用户后,会自动发送邮件到填写的邮箱. 实验版本:  11. ...

  3. exporter API(导出、输出器api)moodel3.3

    Moodle[导出器]是接收数据并将其序列化为一个简单的预定义结构的类.它们确保输出的数据格式统一,易于维护.它们也用于生成外部函数的签名(参数和返回值) 外部函数定义在moodle/lib/exte ...

  4. JavaOO面向对象中的注意点(一)

    1.JavaOO宗旨思想: ★万物皆对象,对象因关注而产生★ ☆类是对象的抽取,对象是类的实例☆ 2.JavaOO的三大特征: 封装.继承.多态  (第四大特征 抽象 现还有争议) 3.属性与行为: ...

  5. boost 正则表达式 regex

    boost 正则表达式 regex   环境安装 如果在引用boost regex出现连接错误,但是引用其他的库却没有这个错误,这是因为对于boost来说,是免编译的,但是,正则这个库 是需要单独编译 ...

  6. HDU 1540 Tunnel Warfare(经典)(区间合并)【线段树】

    <题目链接> 题目大意: 一个长度为n的线段,下面m个操作 D x 表示将单元x毁掉 R  表示修复最后毁坏的那个单元 Q x  询问这个单元以及它周围有多少个连续的单元,如果它本身已经被 ...

  7. hdu 1518 Square 木棍建正方形【DFS】

    题目链接 题目大意: 题意就是输入棍子的数量和每根棍子的长度,看能不能拼成正方形. #include <bits/stdc++.h> using namespace std; int n, ...

  8. Python之路【第八篇】:面向对象的程序设计

    阅读目录 一 面向对象的程序设计的由来二 什么是面向对象的程序设计及为什么要有它三 类和对象3.1 什么是对象,什么是类3.2 类相关知识3.3 对象相关知识3.4 对象之间的交互3.5 类名称空间与 ...

  9. Spring Boot 项目实战(四)集成 Redis

    一.前言 上篇介绍了接口文档工具 Swagger 及项目监控工具 JavaMelody 的集成过程,使项目更加健壮.在 JAVA Web 项目某些场景中,我们需要用缓存解决如热点数据访问的性能问题,业 ...

  10. vue cli3.0 结合echarts3.0和地图的使用方法

    echarts 提供了直观,交互丰富,可高度个性化定制的数据可视化图表.而vue更合适操纵数据. 最近一直忙着搬家,就没有更新博客,今天抽出空来写一篇关于vue和echarts的博客.下面是结合地图的 ...