数据传输过程中经历的两个阶段:
send: 是将数据从应用程序内存 copy 到操作系统缓存, 这个过程称之为 copydata
服务器要接受数据: recv 是从操作系统缓冲区 copy 数据到应用程序内存
如果数据已经到达缓存区则直接 copy 如果数据还没有到达会进入阻塞状态一直等待有数据发过来 等待的过程称之为 waitdata
accept 三次握手 也会经历 copy wait 阶段
recvfrom wait 完后 copy
- sendto copy
- sendall copy
IO 模型
模型即套路 是解决某个固定问题的方式方法
IO 模型即解决 IO 问题的方式方法
IO 指的输入输出, 输入输出设备的速度 对比 CPU 而言是非常慢的, 比如 recv input 等都是 IO 操作
IO 操作的最大问题就是会阻塞程序执行
IO 模型要解决的也仅仅是网络 IO 操作
IO 模型有以下几个
1. 阻塞 IO
socket 默认就是阻塞的
问题: 同一时间只能服务一个客户端
方法 1: 多线程
优点: 如果并发量不高 , 效率是较高的 因为每一个客户端都有单独的线程来处理
弊端: 不可能无限的开启线程 线程也需要占用资源
方式 2: 多进程
优点: 可以多个 CPU 并行处理
弊端: 占用资源非常大 一旦客户端稍微多一点 立马就变慢了
线程池:
优点: 保证了服务器正常稳定运行, 还帮你负责创建和销毁线程以及任务分配
弊端: 一旦并发量超出最大线程数量, 就只能等前面的运行完毕
进程池:
真正导致效率低的是阻塞问题 但是上述几个方法 并没有真正解决阻塞问题 仅仅是避开了阻塞问题
协程:
基于单线程并发
优点: 不需要创建一堆线程, 也不需要在线程间做切换
弊端: 不能利用多核优势 单核处理器 性能也是由上限的 如果真的并发特别大 name 处理速度回变慢
- import socket,os
- c=socket.socket()
- c.connect(('127.0.0.1',1688))
- while True:
- msg='%s 发来问候!'%os.getpid()
- if not msg:continue
- c.send(msg.encode())
- data=c.recv(1024)
- print(data.decode())
- client
- import socket
- from threading import Thread
- s=socket.socket()
- s.bind(('127.0.0.1',1688))
- s.listen()
- print('等待连接')
- def talking(c):
- while True:
- try:
- data=c.recv(1024)
- print(data.decode())
- if not data:
- c.close()
- break
- c.send(data.upper())
- except ConnectionResetError:
- c.close()
- break
- while True:
- c,addr=s.accept()
- print('连接成功')
- t=Thread(target=talking,args=(c,))
- t.start()
- server
2. 非阻塞 IO
即遇到 IO 操作也不导致程序阻塞, 会继续执行 意味着即使遇到 IO 操作 CPU 执行权也不会被剥夺 程序效率就变高了
以下程序 占用 CPU 太高
原因是 需要无线的循环 去向操作系统拿数据
setblocking(False) 设置 socket 是否阻塞 默认为 True
- import socket
- c=socket.socket()
- c.connect(('127.0.0.1',1688))
- print('connecting..')
- while True:
- msg=input('>>>:')
- if not msg:continue
- c.send(msg.encode())
- data=c.recv(1024)
- print(data.decode())
- client
- import socket,time
- s=socket.socket()
- s.bind(('127.0.0.1',1688))
- s.listen()
- # 设置 socket 是否阻塞 默认为 True
- s.setblocking(False)
- # 所有的客户端 socket
- cs=[]
- # 所有需要返回数据的客户端
- send_cs=[]
- while True:
- try:
- c,addr=s.accept()
- #三次握手
- print('连接成功')
- cs.append(c)# 存储已经连接成功的客户端
- except BlockingIOError:
- # 没有数据准备 可以作别的事情
- # print("收数据")
- for i in cs[:]:
- try:
- data=i.recv(1024)
- if not data:
- i.close()
- cs.remove(i)
- print(data.decode())
- # 把数据和连接放进去
- send_cs.append((i,data))
- # c.send(data.upper()) # io
- # send 也是 io 操作 在一些极端情况下 例如系统缓存满了 放不进去 那肯定抛出
- # 非阻塞异常 这时候必须把发送数据 单独拿出来处理 因为 recv 和 send 都有可能抛出相同异常
- # 就无法判断如何处理
- except BlockingIOError:
- continue
- except ConnectionResetError:
- i.close()
- # 从所有客户端列表中删除这个连接
- cs.remove(i)
- for item in send_cs[:]:
- c,data=item
- try:
- c.send(data.upper())
- # 如果发送成功就把数据从列表中删除
- send_cs.remove(item)
- except BlockingIOError: # 如果缓冲区慢了 那就下次再发
- continue
- except ConnectionResetError:
- c.close()# 关闭连接
- send_cs.remove(item)# 删除数据
- # 从所有客户端中删除这个已经断开的连接
- cs.remove(c)
- server
3.IO 多路复用 *******
也是单线程并发处理所有请求
与非阻塞不同之处在于 不需要频繁不断发送系统调用
只要等待 select 选择出准备就绪 socket 然后进行处理即可
如果说 没有任何一个 socket 准备就绪 select 就会阻塞住 这意味着效率降低了吗?
并不是 服务器的工作就是处理一堆 socket 既然没有 socket 需要处理 就不需要做任何操作了
- import socket
- c = socket.socket()
- c.connect(("127.0.0.1", 1688))
- print("connect....")
- while True:
- msg = input(">>>:").strip()
- if not msg:continue
- c.send(msg.encode("utf-8"))
- data = c.recv(1024)
- print(data.decode("utf-8"))
- client
- """
- IO 多路复用
- 用一个线程来并发处理所有的客户端
- 原本我们是直接问操作系统 要数据,
- 如果是阻塞 IO 没有数据就进入阻塞状态
- 非阻塞 IO 没有数据就抛出异常 然后继续询问操作系统
- 在多路复用模型中, 要先问 select 哪些 socket 已经准备就绪 然后在处理这些已经就绪的 socket
- 既然是已经就绪 那么执行 recv 或是 send 就不会在阻塞
- select 模块只有一个函数就是 select
- 参数 1:r_list 需要被 select 检测是否是可读的客户端 把所有 socket 放到该列表中, select 会负责从中找出可以读取数据的 socket
- 参数 2:w_lirt 需要被 select 检测是否是可写的客户端 把所有 socket 放到该列表中, select 会负责从中找出可以写入数据的 socket
- 参数 3:x_list 存储要检测异常条件 .... 忽略即可
- 返回一个元组 包含三个列表
- readables 已经处于可读状态的 socket 即数据已经到达缓冲区
- writeables 已经处于可写状态的 socket 即缓冲区没满 可以发送...
- x_list: 忽略
- 从可读或写列表中拿出所有的 socket 依次处理它们即可
- """
- import socket
- import select
- s=socket.socket()
- s.bind(('127.0.0.1',1688))
- s.listen()
- # 在多路复用中 一旦 select 交给你一个 socket 一定意味着 该 socket 已经准备就绪 可读或是可写
- # s.setblocking(False)
- r_list=[s]
- w_list=[]
- # 存储需要发送的数据 已及对应的 socket 把 socket 作为 key 数据作为 value
- data_dic={}
- while True:
- readables,writeables,_=select.select(r_list,w_list,[])
- # 接收数据 以及服务器建立连接
- for i in readables:
- if i==s:# 如果是服务器 就执行 accept
- c,_=i.accept()
- r_list.append(c)
- else:# 是一个客户端端 那就 recv 收数据
- try:
- data=i.recv(1024)
- if not data:#Linux 对方强行下线或是 Windows 正常下线
- i.close()
- r_list.remove(i)
- continue
- print(data)
- # 发送数据 不清楚 目前是不是可以发 所以交给 select 来检测
- w_list.append(i)
- data_dic[i]=data# 把要发送的数据先存在 等 select 告诉你这个连接可以发送时再发送
- except ConnectionResetError:# Windows 强行下线
- i.close()
- r_list.remove(i)# 从检测列表中删除
- # 发送数据
- for i in writeables:
- try:
- i.send(data_dic[i].upper())# 返回数据
- # data_dic.pop(i)
- # w_list.remove(i)
- except ConnectionResetError:
- i.close()
- finally:
- data_dic.pop(i)# 删除已经发送成功的数
- w_list.remove(i)# 从检测列表中删除这个连接 如果不删除 将一直处于可写状态
- server
4. 异步 IO 爬虫阶段讲
5. 信号驱动 了解
迭代过程中 不允许修改元素
- # li = [1,2,3,4,5]
- #
- # rm_list = []
- #
- # for i in li:
- # rm_list.append(i)
- #
- # for i in rm_list:
- # li.remove(i)
- #
- # print(li)
- li=[1,2,3,4,5]
- # print(id(li[:]))
- # print(id(li))
- # 用切片的方式 产生一个新的列表 新列表中元素与就列表完全相同
- # 遍历新列表 删除就列表
- for i in li[:]:
- li.remove(i)
- print(li)
IO 模型
来源: http://www.bubuko.com/infodetail-2985270.html