一, IO 模型
五种 IO 模型:
blocking IO : 阻塞 IO
nonblocking IO 非阻塞 IO
IO multiplexing IO 多路复用
signal driven IO 信号驱动 IO
asynchronous IO 异步 IO
对于一个 network IO, 它会涉及到两个系统对象, 一个是调用这个 IO 的 process(or thread), 另一个就是系统内核. 当一个 read/recv 读数据的操作发生时, 该操作会经历两个阶段:
1, 等待数据准备
2, 将数据从内核拷贝到进程中
补充:
1, 输入操作: read,readv,recv,recvfrom,recvmsg 共 5 个函数, 如果会阻塞状态, 则会经历 wait data 和 copy data 两个阶段, 如果设置为非阻塞则在 wait 不到 data 时抛出异常
2, 输出操作: write,writev,send,sendto,sendmsg 共 5 个函数, 在发送缓冲区满了会阻塞在原地, 如果设置为非阻塞, 则会抛出异常
3, 接收外来链接: accept, 与输入操作类似
4, 发起外出链接: connect, 与操作类似
二, 阻塞 IO
回顾同步 / 异步 / 阻塞 / 非阻塞:
同步: 提交一个任务之后要等待这个任务执行完毕
异步: 只管提交任务, 不等待这个任务执行完毕就可以去做其他的事情
阻塞: recv,recvfrom,accept, 线程阶段 运行状态>>>阻塞状态>>>就绪
非阻塞: 没有阻塞状态
在一个线程的 IO 模型中, 我们 recv 的地方阻塞, 我们就开启多线程, 但是不管你开启多少个线程, 这个 recv 的时间是不是没有被规避掉, 不管是多线程还是多进程都没有规避掉 IO 这个时间
实际上, 除非特别指定, 几乎所有的 IO 接口 (包括 socket 接口) 都是阻塞的. 这给网络编程带来了一个很大的问题, 如在调用 recv(1024)的同时, 线程将被阻塞, 在此期间, 线程将无法执行任何运算或响应任何的网络请求
一个简单的解决方案:
在服务端使用多线程 (或多进程). 多线程(或多进程) 的目的是让每个连接都拥有独立的线程(或进程), 这样任何一个连接的阻塞都不会影响其他的连接.
该方案的问题是:
开启多线程或都线程的方式, 在遇到要同时响应成百上千的连接请求, 则无论多线程还是多进程都会占用严重的系统资源, 降低系统对外界相应效率, 而且线程与进程本身也更容易进入假死状态
改进方案:
很多程序员可能会考虑使用 "线程池" 或 "进程池"."线程池" 旨在减少创建和销毁线程的频率, 其维持一定合理数量的线程, 并让空闲的线程重新承担新的执行任务."连接池" 维持连接的缓存池, 尽量重用已有的连接, 减少创建和关闭连接的频率. 这两种技术都可以很好的降低系统开销, 都被广泛应用很大型系统, 如 websphere,tomcat 和各种数据库等
改进后方案其实也存在着问题:
"线程池" 和 "连接池" 技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用. 而且, 所谓 "池" 始终有其上限, 当请求大大超过上限时,"池" 构成的系统对外界的响应并不比没有池的时候效果好多少. 所以使用 "池" 必须其面临的响应规模, 并根据规模调整 "池" 的大小
三, 非阻塞 IO
缺点:
1, 循环调用 recv()将大幅推高 CPU 占用率, 这也是我们在代码中留一句 time.sleep(2)的原因, 否则在低配主机下极容易出现卡机情况
2, 任务完成的响应延迟增大了, 因为每过一段时间才去轮询一次 read 一次, 而任务可能在两次轮询之间的任意时间完成. 这会导致整体数据吞吐量的降低.
- import time
- import socket
- server = socket.socket()
- ip_port = ('127.0.0.1',8001)
- server.bind(ip_port)
- server.listen()
- server.setblocking(False)
- conn_list = []
- while 1:
- while 1:
- try:
- conn,addr = server.accept()
- conn_list.append(conn)
- break
- except BlockingIOError:
- time.sleep(0.1)
- print('此时还没有人链接我')
- for sock in conn_list:
- print(sock)
- while 1:
- try:
- from_client_msg = sock.recv(1024)
- print(from_client_msg.decode('utf-8'))
- sock.send(b'hello')
- break
- except BlockingIOError:
- print('还没有任何的消息啊')
非阻塞 iO 模型服务端
- import socket
- client = socket.socket()
- client.connect(('127.0.0.1',8001))
- while 1:
- to_server_msg = input('我想对你说>>>>')
- client.send(to_server_msg.encode('utf-8'))
- from_server_msg = client.recv(1024)
- print(from_server_msg.decode('utf-8'))
非阻塞 IO 模型客户端
四, 多路复用 IO
结论: select 的优势在于可以处理多个连接, 不适用于单个连接
IO 多路复用的机制:
Select:Windows ,Linux
Poll 机制: Linux 和 select 监听机制一样, 但是对监听列表里面的数量没有限制, select 默认限制是 1024 个, 但是他们两个都是操作系统轮询每一个被监听的文件描述符(如果数量很大, 其实效率不太好), 卡是否有可读操作
Epoll:Linux 它的监听机制和上面两个不同, 他给每一个监听的对象绑定了一个回调函数, 你这个对象有消息, 那么触发回调函数给用户, 用户就进行系统调用来拷贝数据, 并不是轮询监听所有的被监听对象, 这样的效率高很多.
- import select
- import socket
- server = socket.socket()
- server.bind(('127.0.0.1',8001))
- rlist = [server,]
- server.listen()
- while 1:
- print('11111')
- rl,wl,el = select.select(rlist,[],[])# 创建 rl 对象, 监听
- print(222222)
- print('server 对象>>>',server)
- print(rl) #rl 对象其实跟 server 对象 (内容) 一致
- for sock in rl: #当 rl 有值的时候, 循环列表
- if sock == server: #值与 server 相同
- conn,addr = sock.accept() #建立连接
- rlist.append(conn) #把管道信息加入列表
- else:
- from_client_msg = sock.recv(1024)#conn
- print(from_client_msg.decode('utf-8')) #打印接收
IO 多路复用服务端
- import socket
- client = socket.socket()
- client.connect(('127.0.0.1',8001))
- to_server_msg = input('发给服务端的消息:')
- client.send(to_server_msg.encode('utf-8'))
- # from_server_msg = client.recv(1024)
- # print(from_server_msg.decode('utf-8'))
io 多路复用客户端
五, 异步 IO
- from gevent import monkey;monkey.patch_all()
- import time
- import gevent
- def func1(n):
- print('xxxxxx',n)
- # gevent.sleep(2)
- time.sleep(2)
- print('cccccc',n)
- def func2(m):
- print('111111',m)
- # gevent.sleep(2)
- time.sleep(2)
- print('222222',m)
- start_time = time.time()
- g1 = gevent.spawn(func1,'alex')
- g2 = gevent.spawn(func2,'德玛西亚')
- # g1.join() #
- # g2.join()
- gevent.joinall([g1,g2])
- end_time = time.time()
- print(end_time - start_time)
- print('代码结束')
来源: http://www.bubuko.com/infodetail-2872773.html