非阻塞io模型
TCP/IP模型下,普通的通信模型(I/O模型)只能同时监听处理一个socket,其他用户的连接只能默默等待着,这离高并发的网络服务器还遥遥无期
可能我们会想到开启多进程来实现高并发,当有连接用户到达时新开起进程去单独处理,但是问题也随之而来,如果我有成千上万的TCP连接,是要创建成千上万的进程出来,也不太现实
其实这里想一下,我们之前一直是单独的为某一个客户端服务;那么如果现在把连接到的所有的客户端先保存起来,在轮询的挨个一次次去处理,而不是单独的处理完一个之后才能处理下一个;好比一个澡堂,只有一个服务员,迎宾和大厅里的事情只有一个人来做,那么我可以先在门口观察是否有顾客要进来,如果有,则让顾客先进来大厅坐着;没有的话,也回到大厅里,去继续从第一个顾客一直询问到最后一个,来处理他们的需求,处理完成之后再回到门口去欢迎下一个客人,如此循环往复
实际操作起来,像是这样,首先我们将服务端套接字准备为非阻塞,如果没有客户端连接也不要阻塞一直等待下去;如果有客户端连接到了之后,将他保存在一个数据集中,之后轮询去获取每一个客户端的消息,那么这里就同样的也有需要把客户端连接套接字也设置为非阻塞的
- 自实现io复用模型
python
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setblocking(0) # 设置服务端套接字为非阻塞模型
c_ = [] # 所有连接到的客户端套接字保存列表
while True:
try:
c,c_addr = s.accept() #非阻塞接收其他客户端连接
except : #应对非阻塞模型下可能会因为没有客户端连接而导致错误
pass
else:
c_.setblocking(0) # 设置来访客户端套接字为非阻塞
c_.append(c) # 如果正常连接来访,保存到序列中
for client in c_: # 轮询处理已经连接到的客户端套接字
try: #轮询查看是否有客户端发来消息或者需要发送消息
client.recv()...
client.send()...
except:
pass
- 实际服务器模型代码
python
import socket
import copy
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #设置端口复用
server.bind( ('',23333) )
server.listen(5)
c_ = {} #保存所有连接客户端套接字
# key: client
# value: c_addr
server.setblocking(0) #设置服务端套接字为非阻塞模式,避免在accept时阻塞
print('[+] server open')
while True:
c_bak = copy.copy(c_) #Python中是无法直接将字典在for循环中动态删除的!
try:
try:
client,c_addr = server.accept()
except BlockingIOError:
pass
else:
client.setblocking(0) #设置套接字属性为非阻塞
print('[+] from:',c_addr)
c_[client] = c_addr #接收到了连接来访之后,立马放入保存数据集中
for c in c_bak: #遍历
try:
data = c.recv(1024).decode('utf-8') #非阻塞
except BlockingIOError:
continue
else:
if not data:
print('[%s] closed.' % (c_bak[c],) )
c.close() #关闭套接字
del c_[c] #在数据集中删除该套接字
else:
print('[%s]:%s' % (c_bak[c],data) )
msg = '自动回复'.encode("utf-8") #每一个客户端的消息都自动返回
c.send(msg)
except KeyboardInterrupt:
break
server.close() #关闭服务端套接字
在实现的代码中们使用了字典做为保存已了解TCP客户端套接字的数据类型
字典的key值为每一个唯一连接到的用户,value值为客户端套接字地址
当每一次通过服务端套接字获取到了客户端的连接之后,我们立即将客户端套接字设置为非阻塞并和客户端地址一起保存在字典中。
之后通过for循环迭代访问保存每一个连接的客户端套接字的字典
如果出现因非阻塞而报的错误,可能是因为没有数据,或者已经断开链接,那么直接跳过该套接字,或者将该套接字删除即可
Select
- io复用
I/O复用就是单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流
目前支持的模型有Select、Poll与Epoll;
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
但是select有一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024
用Select完成非阻塞方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况,会告诉程序哪些套接字是读、写或是异常状态,我们可以遍历这些套接字的序列,再进行一对一处理
- select模型来自于unix操作系统中,由c而来
C
int select(int maxfdpl, fd_set * readset, fd_set *writeset, fd_set *exceptset, const struct timeval * tiomeout)
// maxfdpl: 最大的文件描述符长度
// readset: 监听的可读集合
// writeset: 监听的可写集合
// exceptset: 监听的异常集合
// tiomeout: 超时判断
- 在python中对于select模型应用,使用select模块
python
import select
select.select(rlist, wlist, xlist[, timeout]) -> (rlist, wlist, xlist)
# rlist: 监听的可读套接字,第一个值往往是服务端套接字
# wlist: 监听的可写套接字
# xlist: 监听的异常套接字
- 使用select复用模型,需要三个循环
python
循环响应读事件 -> rlist
循环用来处理消息发送 -> wlist
循环处理异常事件 -> xlist
- 参考代码
python
import select
import socket
import queue
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 服务端套接字
s.setblocking(0) # 非阻塞
s.bind(('',8000)) # 绑定可用ip端口
s.listen(5) # 监听套接字
in_ = [s] # 待监听的可读队列
'''
s的可读行为: 有新的链接
客户端套接字行为: 有数据发来
'''
out_ = [] # 可写队列,发送消息
msg_queue = {}
while in_:
print('等待下一次事件...')
readable, writable, exceptional = select.select(in_, out_, in_)
for socket_ in readable: # 监听可读队列
if socket_ is s: # 当前套接字为服务端套接字,相应的可读事件为有用户链接来
c,c_addr = s.accept()
print('有新的套接字连接')
c.setblocking(0) # 新连接的客户端套接字设置为非阻塞
in_.append(c) # 加入可读监听队列中
msg_queue[c] = queue.Queue() # 为当前客户端放置消息存放队列
else: # 非服务端套接字,客户端套接字可读事件为有消息发来
data = socket_.recv(1024) # 接收客户端数据
if data == '': # 客户端断开连接
print('套接字关闭...[%s]' % socket_)
if socket_ in out_:
out_.remove(socket_)
in_.remove(socket_) # 在两个监听队列里删除
socket_.close()
else: # 有客户端消息发来
msg_queue[socket_].put(data) # 放置数据到该客户端的消息队列中
if socket_ not in out_: # 放置监听套接字到可写事件队列中
out_.append(socket_)
for socket_ in writable:
try:
q = msg_queue.get(socket_) # 获取当前是否含有消息队列,待群发消息
if q: # 如果存在
if q.empty(): # 无消息 踢出队列
out_.remove(socket_)
else:
send_data = q.get_nowait() # 取出消息
except:
out_.remove(socket_)
else:
for s_ in in_:
if s_ is not s: # 不是服务端套接字
try:
s_.send(send_data)
except:
in_.remove(s_)
if s_ in out_:
out_.remove(s_)
s_.close()
Epoll
Epoll被认为是linux下性能最好的多路io就绪通知方法
Epoll没有最大文件描述符数量限制
Epoll支持水平触发、边缘触发告知用户是哪一个套接字出现了可操作事件
epoll不需要我们去遍历每一个连接来的客户端套接字,去查看是否有数据到达
当有新连接或者新的消息传来时,epoll会自动将事件返回
事件有如下类型
- EPOLLIN(可读事件)
- EPOLLOUT(可写事件)
- EPOLLERR(错误事件)
- EPOLLHUP(连接断开事件)
-
关于水平触发和边缘触发,可以这样来理解:
别人在门口给我送来了一斤猪肉,我拿走了半斤回家,当我想要再去拿走剩下的半斤,如果是水平触发,那么还是可以拿走的;
但是如果是边缘触发,那么必须等待其他人再送猪肉来我才能再去拿猪肉,而且再拿的时候,可能就是老猪肉和新送来的猪肉一起拿回家。
水平触发:只要有数据到来,那么我就可以读取,并且,来了1kb的数据,我可以1字节1字节这样读取1024次
边缘触发:如果数据来了之后我只读取了512字节的数据,那么剩下的512字节数据的拿取只能等待下次数据到来
- 水平触发:
- 缓冲区不为空,返回读就绪
- 缓冲区不为满,返回写就绪
- 边缘触发:
- 缓冲区由空变为非空,数据变多时,缓冲区数据可读时,返回读就绪。
- 缓冲区由满变为非满时,旧数据被传输走时,缓冲区内容变少、缓冲区可写时,返回写就绪。
- 用法
python
import select # 导入select模块
epoll = select.epoll() # 创建一个epoll对象
epoll.register(文件句柄,事件类型) # 注册要监控的文件句柄和事件
'''
事件类型:
select.EPOLLIN 可读事件
select.EPOLLOUT 可写事件
select.EPOLLERR 错误事件
select.EPOLLHUP 客户端断开事件
'''
epoll.poll(timeout) # 当文件句柄发生变化,则会以列表的形式主动报告给用户进程
'''
timeout:为超时时间,默认为-1,即一直等待直到文件句柄发生变化,如果指定为1,那么epoll每1秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
'''
epoll.unregister(文件句柄) # 从epoll事件队列中注销文件句柄
epoll.close() # 关闭epoll对象的控制文件描述符
- 参考代码
python
import select
import socket
import time
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建socket对象
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # IP地址端口复用
server.bind(('',9999))
server.listen(5) # 启动监听,设置非阻塞
server.setblocking(0)
epoll = select.epoll()
# 创建epoll事件对象,后续要监控的事件添加到其中
epoll.register(server.fileno(), select.EPOLLIN)
# 注册服务端套接字,响应事件为可读事件,代表客户端链接
socket_d = {} # 存储套接字句柄与套接字
socket_d[server.fileno()] = server
print('start server ...')
while 1:
events = epoll.poll() # 创建当次事件队列,阻塞等待事件发生
for fileno,event in events:
if event & select.EPOLLIN :
if fileno == server.fileno(): # 当前文件句柄为服务器套接字,那么接收客户端链接
client,client_addr = server.accept()
print('客户端链接:',client_addr)
client.setblocking(0)
# 设置客户端套接字为非阻塞
epoll.register(client.fileno(),select.EPOLLIN)
# 注册事件队列
socket_d[client.fileno()] = client
# 句柄做key,value为实际套接字,方便取出操作
else :# 出现其他可读事件,为客户端套接字发来数据
data = socket_d[fileno].recv(4096)
if data == b"":
epoll.unregister(fileno)
print('客户端关闭:',socket_d[fileno])
socket_d[fileno].close()
del socket_d[fileno]
else:
print('客户端:',data.decode())
elif event & select.EPOLLHUP:
print('客户端关闭:',socket_d[fileno])
epoll.unregister(fileno)
# 从事件队列中删除
socket_d[fileno].close()
# 关闭客户端套接字
del socket_d[fileno]
# 从句柄存储对象中删除
epoll.unregister(server.fileno()) # 事件队列中取消注册
epoll.close() # 关闭epoll
server.close() # 关闭服务端套接字