IO操作是不占用CPU的,IO多路复用,是要管理起所有的IO操作。IO多路复用的典型场景是监听socket对象内部是否发生了变化
socket内部什么时候会有变化:
- 建立连接
- 发送消息
socket实例
这里使用socket实现一个简单的Echo Server的功能
- <font color="red">server.py</font>
import socket
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
# 循环接受每一个连接池中的连接
while True:
# 接受客户端连接
conn, address = sk.accept()
# 向客户端发送欢迎消息
conn.sendall(bytes('hello', encoding='utf8'))
# 进入到收发消息的循环中
while True:
# Windows客户端在异常断开后抛出异常,这里是处理Windows的断开情况
try:
recv = conn.recv(1024)
# Linux客户端断开recv会是空值,这里处理Linux的断开情况
if not recv:
break
# 这里处理客户端主动发出断开请求的情况
if str(recv, encoding='utf-8') == 'exit':
break
except Exception as ex:
break
# 向客户端发送数据
conn.sendall(recv)
- <font color="blue">client.py</font>
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 1559))
# 接收欢迎消息
data = sk.recv(1024)
print(data)
while True:
i = input("> ")
# 向服务端发送消息
sk.sendall(bytes(i, encoding='utf8'))
# 接收服务端发来的消息
msg = sk.recv(1024)
print(str(msg, encoding='utf-8'))
sk.close()
以上的socket代码同一时间仅能处理一个客户端的请求,之后连接上来的客户端在第一个客户端还没有断开的时候,会一直等待,直到上一个客户端的请求断开
select.selext()中的第一个参数
建立连接
上面说到,一种socket会变的情况是建立连接
上面的代码中,涉及到建立连接的socket是sk对象(变量)sk对象在执行到sk.accept()
时,接受了一个新客户端的连接请求时候,socket内部就发生了变化,我们就需要监听这种变化,用以分辨出新的客户端的连接
创建socket,绑定并监听之后socket一般不会发生变化,只有当有新的客户端连接进来的时候,socket才会发生变化,我们需要监听的也是这个阶段的变化
得出结论:当socket被创建、绑定并监听之后发生变化,就是有新的客户端进行连接
- <font color="red">server.py</font>
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
while True:
rList, w, e = select.select([sk,], [], [], 1)
print(rList)
上面代码引入了IO多路复用中的select模型,使用select.select()方法,会返回一个有三个元素的元祖
在select.select()方法中的第一个参数,暂时只添加了一个服务端的socket对象,只要服务端socket对象sk有变化(新的客户端连接)就立即把变化的socket对象加入到rList列表中
监听的socket列表中,sk对象有变化 ---> rList = [sk]
监听的socket列表中,没有socket发生变化 ---> rList = []
以上代码跑起来的效果:
[]
[]
[]
...
# 每秒打印一个[]
# select.sekect()中的第四个参数起了作用
# 超时时间,监听的对象没有发生变化的时候,多少秒循环一次
说明没有新的连接请求,下面将试验有客户端连接产生的情况
- <font color="blue">client.py</font>
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 1559))
sk.close()
客户端一旦连接到服务端,服务端的回显如下
[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1559)>]
[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1559)>]
[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1559)>]
...
# 疯狂的快速打印...
从上面可以看出,有一个客户端连接进来了。服务端的sk对象内部发生了变化,当select监听到sk对象发生变化后,立即将发生变化的对象赋值给了rList列表(append到列表)从打印出来的内容中可以看出,列表中的元素是发生变化的socket对象
处理rList(服务端socket)
rList中保存了所有发生变化的socket对象,以上代码中只监听了服务端socket对象,这里暂时只讨论服务端socket变化的情况
- <font color="red">server.py</font>
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
while True:
# 监听服务端socket对象sk
rList, w, e = select.select([sk,], [], [], 1)
print(rList)
# 遍历rList中的每一个socket对象
# 目前rList中只会出现服务端的socket对象
for s in rList:
conn, address = s.accept()
conn.sendall(bytes('hello', encoding='utf8'))
- <font color="blue">client.py</font>
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 1559))
data = sk.recv(1024)
print(data)
while True:
input("> ")
sk.close()
连续运行三个客户端连接时,服务端的回显:
[]
[]
[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1559)>]
[]
[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1559)>]
[]
[<socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 1559)>]
[]
[]
每个客户端的回显:
b'hello'
>
以上通过对rList中服务端socket对象执行accept()方法,来实现了一个类似并发连接的效果,每一个连接进来的客户端都会被服务端接受请求,“同时”提供服务
接收客户端消息
上面提到,不仅创建连接会触发socket的变化,与客户端连接建立后,客户端发来消息,也会引发客户端socket连接的内部变化
- <font color="red">server.py</font>
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
inputs = [sk]
while True:
rList, w, e = select.select(inputs, [], [], 1)
print(rList)
for s in rList:
conn, address = s.accept()
# conn也是一个socket对象
# 当服务端socket接收到客户的请求后,会分配一个新的socket对象专门用来和这个客户端进行连接通信
# 当服务端分配新的socket对象给新连接进来的客户端的时候
# 我们也需要监听这个客户端的socket对象是否会发生变化
# 一旦发生变化,意味着客户端向服务器端发来了消息
inputs.append(conn)
conn.sendall(bytes('hello', encoding='utf8'))
在上面的代码中,我把服务端socket为新客户端创建的socket也加入到了监听列表中,那么如果有客户端发来消息,select监听到客户端socket(conn)发生变化并加入到rList列表中后,在for循环处理中,客户端的socket并没有accept()方法,而且也不要这个方法,这就需要在for循环中对两类socket区分对待
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
inputs = [sk]
while True:
rList, w, e = select.select(inputs, [], [], 1)
print("select当前监听socket对象的数量>", len(inputs), " | 发生变化的socket数量>", len(rList))
for s in rList:
# 判断socket对象如果是服务端的socket对象的话
if s == sk:
conn, address = s.accept()
# conn也是一个socket对象
# 当服务端socket接收到客户的请求后,会分配一个新的socket对象专门用来和这个客户端进行连接通信
# 当服务端分配新的socket对象给新连接进来的客户端的时候
# 我们也需要监听这个客户端的socket对象是否会发生变化
# 一旦发生变化,意味着客户端向服务器端发来了消息
inputs.append(conn)
conn.sendall(bytes('hello', encoding='utf8'))
# 其他的就都是客户端的socket对象了
else:
# 意味着客户端给服务端发送消息了
msg = s.recv(1024)
print(msg)
- <font color="blue">client.py</font>
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 1559))
# 接收欢迎消息
data = sk.recv(1024)
print(data)
while True:
i = input("> ")
# 向服务端发送消息
sk.sendall(bytes(i, encoding='utf8'))
sk.close()
当我运行一个客户端,并Ctrl+C退出时,服务端回显界面在疯狂的打印消息。问题出在了服务端监听的客户端socket连接,当客户端与服务端断开连接时,应在服务端select监听socket对象列表中将该客户端socket对象移除
- <font color="red">server.py</font>
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
inputs = [sk]
while True:
rList, w, e = select.select(inputs, [], [], 1)
print("select当前监听socket对象的数量>", len(inputs), " | 发生变化的socket数量>", len(rList))
for s in rList:
# 判断socket对象如果是服务端的socket对象的话
if s == sk:
conn, address = s.accept()
# conn也是一个socket对象
# 当服务端socket接收到客户的请求后,会分配一个新的socket对象专门用来和这个客户端进行连接通信
# 当服务端分配新的socket对象给新连接进来的客户端的时候
# 我们也需要监听这个客户端的socket对象是否会发生变化
# 一旦发生变化,意味着客户端向服务器端发来了消息
inputs.append(conn)
conn.sendall(bytes('hello', encoding='utf8'))
# 其他的就都是客户端的socket对象了
else:
try:
# 意味着客户端给服务端发送消息了
msg = s.recv(1024)
# Linux平台下的处理
if not msg:
raise Exception('客户端已断开连接')
print(msg)
except Exception as ex:
# Windows平台下的处理
inputs.remove(s)
使用最新的server.py与client.py进行测试时,依次运行多个客户端,再依次关闭多个客户端,服务端的回显如下:
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 1
select当前监听socket对象的数量> 2 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 2 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 2 | 发生变化的socket数量> 1
select当前监听socket对象的数量> 3 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 3 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 3 | 发生变化的socket数量> 1
select当前监听socket对象的数量> 4 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 4 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 4 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 4 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 4 | 发生变化的socket数量> 1
b''
select当前监听socket对象的数量> 3 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 3 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 3 | 发生变化的socket数量> 1
b''
select当前监听socket对象的数量> 2 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 2 | 发生变化的socket数量> 1
b''
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
select当前监听socket对象的数量> 1 | 发生变化的socket数量> 0
特别注意:这里的服务端定义,当我收到客户端发来的空值的时候,我就默认认为客户端主动需要断开与服务端的连接。由于服务端的这个默认规则,在写客户端的时候,一定要注意处理客户端输入的值为空的情况
给客户端回复消息
- <font color="red">server.py</font>
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
inputs = [sk]
while True:
rList, w, e = select.select(inputs, [], [], 1)
print("select当前监听socket对象的数量>", len(inputs), " | 发生变化的socket数量>", len(rList))
for s in rList:
# 判断socket对象如果是服务端的socket对象的话
if s == sk:
conn, address = s.accept()
# conn也是一个socket对象
# 当服务端socket接收到客户的请求后,会分配一个新的socket对象专门用来和这个客户端进行连接通信
# 当服务端分配新的socket对象给新连接进来的客户端的时候
# 我们也需要监听这个客户端的socket对象是否会发生变化
# 一旦发生变化,意味着客户端向服务器端发来了消息
inputs.append(conn)
conn.sendall(bytes('hello', encoding='utf8'))
# 其他的就都是客户端的socket对象了
else:
try:
# 意味着客户端给服务端发送消息了
msg = s.recv(1024)
# Linux平台下的处理
if not msg:
raise Exception('客户端已断开连接')
print(msg)
# 向客户端回复消息
# 这种写法是完全可以的,但是缺点是读写都混在了一起
s.sendall(msg)
except Exception as ex:
# Windows平台下的处理
inputs.remove(s)
但是一般情况下,会做读写分离,可以通过select,实现读写分离(收发分离)
select.selext()中的第二个参数
rList, wList, e = select.select([], [], [], 1)
select.select()的第二个参数有什么值,wList中就会有什么值
利用select的这个特性,可以把需要回复消息的客户端socket对象赋值给select的第二个参数
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
inputs = [sk]
outputs = []
while True:
rList, wList, e = select.select(inputs, outputs, [], 1)
print("---" * 20)
print("select当前监听inputs对象的数量>", len(inputs), " | 发生变化的socket数量>", len(rList))
print("select当前监听outputs对象的数量>", len(outputs), " | 需要回复客户端消息的数量>", len(wList))
# 遍历rList(建立连接和接收数据)
for s in rList:
# 判断socket对象如果是服务端的socket对象的话
if s == sk:
conn, address = s.accept()
# conn也是一个socket对象
# 当服务端socket接收到客户的请求后,会分配一个新的socket对象专门用来和这个客户端进行连接通信
# 当服务端分配新的socket对象给新连接进来的客户端的时候
# 我们也需要监听这个客户端的socket对象是否会发生变化
# 一旦发生变化,意味着客户端向服务器端发来了消息
inputs.append(conn)
conn.sendall(bytes('hello', encoding='utf8'))
# 其他的就都是客户端的socket对象了
else:
try:
# 意味着客户端给服务端发送消息了
msg = s.recv(1024)
# Linux平台下的处理
if not msg:
raise Exception('客户端已断开连接')
else:
outputs.append(s)
print(msg)
# 向客户端回复消息
# 这种写法是完全可以的,但是缺点是读写都混在了一起
# s.sendall(msg)
except Exception as ex:
# Windows平台下的处理
inputs.remove(s)
# 遍历wList(遍历给服务端发送过消息的客户端)
for s in wList:
# 给所有的客户端统一回复内容
s.sendall(bytes('server response', encoding='utf8'))
# 回复完成后,一定要将outputs中该socket对象移除
outputs.remove(s)
wList = 所有给服务端发送消息的客户端,也是需要回复消息客户端列表
- <font color="blue">client.py</font>
import socket
sk = socket.socket()
sk.connect(('127.0.0.1', 1559))
# 接收欢迎消息
data = sk.recv(1024)
print(data)
while True:
i = input("> ")
# 向服务端发送消息
sk.sendall(bytes(i, encoding='utf8'))
# 接收服务端发来的消息
msg = sk.recv(1024)
print(str(msg, encoding='utf-8'))
sk.close()
执行过程:
- 依次连接三个客户端
- 第一个客户端依次向服务发送了两次消息
服务端的回显:
------------------------------------------------------------
select当前监听inputs对象的数量> 1 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 1 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 2 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 2 | 发生变化的socket数量> 1
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 3 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 3 | 发生变化的socket数量> 1
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 1
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
b'ps'
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 1 | 需要回复客户端消息的数量> 1
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 1
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
b'ps'
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 1 | 需要回复客户端消息的数量> 1
------------------------------------------------------------
select当前监听inputs对象的数量> 4 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 3 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 3 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 2 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 2 | 发生变化的socket数量> 1
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
------------------------------------------------------------
select当前监听inputs对象的数量> 1 | 发生变化的socket数量> 0
select当前监听outputs对象的数量> 0 | 需要回复客户端消息的数量> 0
第一个客户端的回显:
b'hello'
> ps
server response
> ps
server response
>
以上代码实现了简单的收发消息的分离,现在又多了一点需求,目前所有给服务端发送消息的客户端,服务端都统一回复了相同的内容,现在要服务端实现Echo Server的功能,即客户端发送什么消息,服务端就给客户端回复什么消息
import socket
import select
# 创建socket对象,绑定IP端口,监听
sk = socket.socket()
sk.bind(('127.0.0.1', 1559))
sk.listen(5)
inputs = [sk]
outputs = []
messages = {}
"""
messages = {
socket_obj1: [msg]
socket_obj2: [msg]
}
"""
while True:
rList, wList, e = select.select(inputs, outputs, [], 1)
print("---" * 20)
print("select当前监听inputs对象的数量>", len(inputs), " | 发生变化的socket数量>", len(rList))
print("select当前监听outputs对象的数量>", len(outputs), " | 需要回复客户端消息的数量>", len(wList))
# 遍历rList(建立连接和接收数据)
for s in rList:
# 判断socket对象如果是服务端的socket对象的话
if s == sk:
conn, address = s.accept()
# conn也是一个socket对象
# 当服务端socket接收到客户的请求后,会分配一个新的socket对象专门用来和这个客户端进行连接通信
# 当服务端分配新的socket对象给新连接进来的客户端的时候
# 我们也需要监听这个客户端的socket对象是否会发生变化
# 一旦发生变化,意味着客户端向服务器端发来了消息
inputs.append(conn)
# 在messages中为该对象创建key
messages[conn] = []
conn.sendall(bytes('hello', encoding='utf8'))
# 其他的就都是客户端的socket对象了
else:
try:
# 意味着客户端给服务端发送消息了
msg = s.recv(1024)
# Linux平台下的处理
if not msg:
raise Exception('客户端已断开连接')
else:
outputs.append(s)
messages[s].append(msg)
# 向客户端回复消息
# 这种写法是完全可以的,但是缺点是读写都混在了一起
# s.sendall(msg)
except Exception as ex:
# Windows平台下的处理
inputs.remove(s)
# 在客户端断开连接后,相对应的该客户端的messages中的信息也需要删除
del messages[s]
# 遍历wList(遍历给服务端发送过消息的客户端)
for s in wList:
# 在该客户端连接对象的messages信息中取出一个进行回复
msg = messages[s].pop()
# 根据客户端发来的消息进行回复
s.sendall(msg)
# 回复完成后,一定要将outputs中该socket对象移除
outputs.remove(s)
服务端做出以上修改,客户端不需要改变
- 11行 为了让一个客户端socket对象收消息和发消息产生关联,引入了一个新的全局变量messages
- 40行 在新客户端连接进来的时候,就预先为该socket对象在messages中创建对应的key
- 54行 在该对象中添加消息
- 64行 在客户端关闭连接收,清理该对象的消息列表
- 70行 将该对象在第40行插入的消息中取出来
总结
使用IO多路复用,实际上实现了类似并发效果的伪并发。内部实际使用了循环来高效的处理阻塞请求
Python中有一个select模块,其中提供了:select、poll、epoll三个方法,分别调用系统的 select,poll,epoll 从而实现IO多路复用。
- Windows Python:提供: select
- Mac Python:提供: select
- Linux Python:提供: select、poll、epoll
注意:网络操作、文件操作、终端操作等均属于IO操作,对于windows只支持Socket操作,其他系统支持所有IO操作,但是无法检测普通文件操作自动上次读取是否已经变化
对于select方法:
句柄列表11, 句柄列表22, 句柄列表33 = select.select(句柄序列1, 句柄序列2, 句柄序列3, 超时时间)
参数: 可接受四个参数(前三个必须)
返回值:三个列表
select方法用来监视文件句柄,如果句柄发生变化,则获取该句柄。
1、当 参数1 序列中的句柄发生可读时(accetp和read),则获取发生变化的句柄并添加到 返回值1 序列中
2、当 参数2 序列中含有句柄时,则将该序列中所有的句柄添加到 返回值2 序列中
3、当 参数3 序列中的句柄发生错误时,则将该发生错误的句柄添加到 返回值3 序列中
4、当 超时时间 未设置,则select会一直阻塞,直到监听的句柄发生变化
当 超时时间 = 1时,那么如果监听的句柄均无任何变化,则select会阻塞 1 秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。
附加:select poll epoll的区别
IO多路复用是系统内核实现的,系统内部维护了一个for循环,一个一个地去检测对象是否有变化
首先需要明确一点的是,for循环的效率是不高的
IO多路复用种类 | 实现原理 | 监听对象个数 |
---|---|---|
select | 系统内部维护了一个for循环 | 1024 |
poll | 系统内部维护了一个for循环 | 没有限制 |
epoll | 句柄序列发生变化时自动通知epoll | 没有限制 |
网友评论