- 从IO模型到协程(一) 什么是IO,用户进程与内核
- 从IO模型到协程(二) BIO模型和NIO模型
- 从IO模型到协程(三) 多路复用之select、poll和epoll
- 从IO模型到协程(四) 用python实现一个多路复用程序
- 从IO模型到协程(五) python中的协程(coroutine)
- 从IO模型到协程(六) asyncio和协程实现高并发
BIO:同步阻塞I/O模式
以下面的代码为例:
先是服务端代码:
# coding=utf-8
from threading import Thread, currentThread
import socket
# 服务端代码
# 创建套接字
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定ip和端口
ip = "127.0.0.1"
port = 8000
server.bind((ip, port))
# 监听套接字
server.listen()
print("服务已开启")
def contact(client):
print("客户端 %s 已成功连接" % currentThread().name)
msg = client.recv(1024).decode("utf-8") # 接收客户端发送到服务端的消息,这里也会收到阻塞
while msg: # 允许接收客户端发送多次消息,如果对方发送空字符,则认为客户端断开连接,此时结束该线程
print("客户端 %s 发送信息:%s" % (currentThread().name, msg))
msg = client.recv(1024).decode("utf-8")
print("客户端 %s 断开连接" % currentThread().name)
while True:
print("等待接收客户端连接")
client,addr = server.accept() # 接受连接, 这里会受到阻塞
# 创建线程用于客户端和服务端通信
thread = Thread(target=contact, args=(client,))
thread.start()
这是客户端代码
# coding=utf-8
from threading import Thread, currentThread
import socket
# 客户端代码
# 创建套接字
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定ip和端口
ip = "127.0.0.1"
port = 8000
client.connect((ip, port))
while True:
msg = input()
if msg:
client.send(msg.encode("utf-8"))
else: # 如果直接输入换行则断开连接
client.close()
break
分析如下:
服务端代码使用死循环接受多个客户端的连接,每有一个连接连进服务器就开一个线程用于客户端和服务端进行通信(其实是简单的输出客户端发送的消息而已)。
主线程只负责接收连接,不负责接受消息;其他线程则只负责接受消息与客户端通信。
如果不用多线程而是用单线程,把接收连接和接受消息都放在一个线程中
while True:
print("等待接收客户端连接")
client,addr = server.accept() # 会阻塞
msg = client.recv() # 会阻塞
就会出现这样的问题:
由于accept 和 recv 都会阻塞,所以当有一个用户连接进来,但是它不发送消息的话,服务端就会被recv阻塞住。假如此时有其他客户端连接进来的话,也会被阻塞住,因为服务端的代码根本执行不到accept方法,服务端被recv阻塞住,所以其他客户端根本连不进来。
而开多线程可以解决这个问题。
上面执行 socket(),bind(),listen(),accept(),recv() 都会进行系统调用。也就是说,执行到这些方法的时候,都会进行用户态切换到内核态,把cpu让给内核程序。
像上面这样调用accept(),recv(),connect()方法进行IO操作会发生阻塞的情况就是BIO。BIO的模式需要每建立一个连接就占用一个线程用来通信。
下面是网上找到的BIO模式的流程图:
所以BIO会出现以下问题:
1.当连接数很多的时候,创建的线程数也会很多,而线程间的切换会消耗cpu资源,也会损耗切换的时间。资源和时间就都浪费再线程的切换上了。cpu跑内核的系统调用就已经挺消耗挺大了,还要切换那么多线程,那不就浪费资源吗。
2.如果客户端不给服务端发送消息,但是又不断开连接,那么这个线程就相当于什么事都没有做,开一个线程却不做事情,这就是对线程资源的一种浪费。
3.断开连接时,线程就直接被销毁了,线程本来就是一种比较珍贵的资源,随便销毁也是对资源的浪费。
但是BIO的关键弊端是因为blocking阻塞,因为recv是阻塞的所以才需要开这么多线程,blocking是根本原因,线程太多只是结果。
那怎么办?
我们程序员无法做出改变,而是需要内核发生改变,因为recv是阻塞的这个事情是内核决定的。
很幸运,之后新版的内核提供了nonblock的socket,这就产生了NIO(同步非阻塞I/O模式)
NIO模型:同步非阻塞I/O模式
这种模式下,accept和recv,接收连接和接收消息都是非阻塞的。这么一来,我们就无需开多个线程用于通信,只需在一个线程中轮询多个客户端查看他们有没有发消息即可。
代码实现如下:
# coding=utf-8
from threading import Thread, currentThread
import socket
from time import sleep
# 服务端代码
# 创建套接字
server = socket.socket()
# 绑定ip和端口
ip = "127.0.0.1"
port = 8000
server.bind((ip, port))
server.listen(3) # 只允许最多3个客户端连接
server.setblocking(False) # 非阻塞socket
print("server服务已开启")
clients = dict() # 用于存储所有建立了连接的客户端
no = 1 # 客户端编号
while True:
try:
client,addr = server.accept() # 接收连接,非阻塞, (内核)会马上返回。如果没有接收到连接则抛出一个异常
print("接收到客户端")
clients[no] = client
no += 1
client.setblocking(False) # 设置客户端socket为非阻塞,这样后面调用recv就是非阻塞的了
except BlockingIOError:
# print("未接收到客户端")
sleep(0.1)
for client_no in list(clients.keys()): # 遍历所有连接的客户端,接收他们发送的消息,这里要用list函数转一下clients.keys(),否则在删除字典中的客户端时再循环会报错说字典遇到改变
each_client = clients[client_no]
try:
msg = each_client.recv(1024) # 非阻塞,(内核)会马上返回。如果没有接收到消息则抛出一个异常
if not msg: # 如果发送空消息表示连接已断开
each_client.close()
del clients[client_no] # 从列表中移除该客户端
print("客户端 %s 断开连接" % str(client_no))
else:
print("客户端 %s 发送消息:%s" % (str(client_no), msg.decode('utf-8')))
sleep(0.1)
except BlockingIOError: # 客户端未发送消息
pass
NIO模型的优点是非阻塞所以避免了多线程,减少了线程间切换的开销
缺点:
要不断循环所有客户端查看客户端是否有发送消息。而且每一次循环都要调用accept和recv,也就是说,每一次循环都要进行系统调用,而系统调用本身就是一个比较消耗cpu的操作,涉及到用户态内核态的切换以及上下文切换。
也就是说通过循环的方式会很消耗cpu。
而且,不一定有客户端发送消息,假如有1万个客户端,只有一个客户端发送了消息,那么1万次轮询只有1次是有效的,因此用轮询的方式是很浪费,很没有效率的。
下面是网上找到的NIO模式的流程图
有没有什么办法,可以不通过轮询,而是让内核通知用户进程哪些客户端已经发送了消息可以去直接recv读取这些客户端的消息呢。
为了解决这个问题就提出了多路复用器。