BIO:同步阻塞IO,客户端一个连接请求(socket)对应一个线程。阻塞体现在: 程序在执行I/O操作时会阻塞当前线程,直到I/O操作完成。在线程空闲的时候也无法释放用于别的服务只能等当前绑定的客户端的消息。
BIO的代码实现 :
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class Main {
public static void main(String[] args) throws IOException {
int id = 0;
ServerSocket socket = new ServerSocket(9090);
System.out.println("服务器成功启动...");
while (true) {
System.out.println("等待客户端连接...");
//监听等待客户端连接 这是阻塞操作
Socket client = socket.accept();
System.out.println("客户" + ++id + "成功连接到:" + client.getInetAddress().getHostAddress());
//需要为该客户分配线程执行任务
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream is = client.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while (true) {
//阻塞等待客户端消息
String line = br.readLine();
if (line == null) {
System.out.println("客户端断开...");
clinet.close();
break;
}
System.out.println(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
}
bio一个客户端对应一个线程所以每有一个客户接入服务器都会创建一个新的线程(创建新线程是通过调用内核的clone()指令来实现的)。
想必大家一定发现bio的明显的弊端了吧,随着接入的客户端越来越多服务器创建的线程数也就越多,在提供服务时在不同线程间的切换(需要保护当前现场,恢复下个线程运行的环境)也会越频繁,这会造成cpu利用的极大浪费,且这种模式的接入量存在明显的上限。
我们可以思考一下造成这一问题的原因是什么:因为是阻塞IO-->在系统调用时缺少参数(需要等待连接或等待消息传递)会被中断等待-->所以当前线程会被阻塞在原地无法提供别的服务-->此时若有新客户端接入我们不得不创建新线程为新客户端服务-->导致线程数越来越多。所以根本问题就在阻塞上,阻塞导致原线程无法提供服务。
**以上的系统调用过程**
/*
在类Unix操作系统中,文件描述符(File Descriptor)是一个非负整数,它是一个指向内核中打开文件的指针。每个打开的文件(无论是常规文件、目录、套接字、管道等)都会被分配一个文件描述符。文件描述符通常用于后续的系统调用
标准输入(stdin):通常分配文件描述符 0。
标准输出(stdout):通常分配文件描述符 1。
标准错误(stderr):通常分配文件描述符 2。
打开一个文件可能会返回文件描述符 3。
创建一个套接字可能会返回文件描述符 4。
创建一个管道可能会返回文件描述符 5 和 6(一个用于读,一个用于写)。
*/
//以下两步对应的就是java代码中的"ServerSocket socket = new ServerSocket(9090);"
socket()=3;//这个函数请求内核创建一个新的套接字,系统调用执行成功并返回了一个文件描述符3
bind(3,9090);//socke绑定9090端口
//监听这个socket
listen(3);
while(true) {
accept(3, )=5;//没有客户端连接时会阻塞,当有客户端连接时括号内空的参数就是客户端的一些信息,系统调用执行成功并返回了一个文件描述符5(代表客户端)
//接下来需要为客户端分配线程
clone(一些共享参数)=线程号;//通过系统调用clone()指令实现,执行成功会返回线程号
}
对于那个新创建的线程有如下步骤
//创建InputStream
while(true){
recv(5, //阻塞等待客户端的消息
}
所以为了解决bio在高连接数的情况下性能急速下降的问题nio就应运而生。
NIO:同步非阻塞IO,是Java 1.4版本开始引入的一个新IO API,支持面向缓冲区、基于通道的IO操作,以更加高效的方式进行文件读写。
Buffer和通道可以相互读写,程序和Buffer交互,所以NIO是面向缓冲区的编程
//nio示例代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) throws IOException {
//客户链
LinkedList<SocketChannel> clients = new LinkedList<>();
//开启服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//监听9090
serverSocketChannel.bind(new InetSocketAddress(9090));
//监听线程设置非阻塞
serverSocketChannel.configureBlocking(false);
while (true) {
//等待客户端连接但是不会阻塞等待,由连接时返回连接对象,无连接时返回null(系统调用层面返回-1)
while (true) {
SocketChannel client= serverSocketChannel.accept();
if(client!=null){
break;
} else {
//连接线程设置非阻塞
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("当前客户端port:" + port);
clients.add(client);
}
}
//执行为客户端提供的服务,以接收消息为例
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
for (SocketChannel client : clients) {
//非阻塞的读
int read = client.read(buffer);//有消息时read>0, 无消息read=0, 异常事件read=-1
if(read==-1) {//断开连接释放资源
client.close();
clients.remove(client);
break;
}
if(read > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
String s = new String(bytes);
System.out.println(client.socket().getPort() + ":" + s);
}
}
}
}
}
**系统层面流程**
//对应的就是java代码中的"ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();"
socket()=4;//这个函数请求内核创建一个新的套接字,系统调用执行成功并返回了一个文件描述符4
//对应的就是java代码中的"serverSocketChannel.bind(new InetSocketAddress(9090));"
bind(4,9090);//socke绑定9090端口
//监听这个socket
listen(4);
//多了一步设置非阻塞
fcntl(4,0_NONBLOCK)=0;
while(true){
accept(4,)=?;//返回具体客户端或-1,-1代表没有连接
fcntl(?, 0_NONBLOCK);//
clients.add(?);
for(client : clients) {
recv(?);//接收消息
}
}
但是需要thread去不断轮询clients,当clients非常大的时候循环的事件开销就会很大,并且对于客户链来说需要执行读写操作的时间和数量只占很小的一部分,所以对于轮询操作不仅耗时而且大部分操作是无效的。
为了解决这个问题又引出了多路复用器的概念,由多路复用器来监测并通知thread,再由thread执行相应操作。
//对应的就是java代码中的"ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();"
socket()=4;//这个函数请求内核创建一个新的套接字,系统调用执行成功并返回了一个文件描述符4
//对应的就是java代码中的"serverSocketChannel.bind(new InetSocketAddress(9090));"
bind(4,9090);//socke绑定9090端口
//监听这个socket
listen(4);
//多了一步设置非阻塞
fcntl(4,0_NONBLOCK)=0;
while(true){
accept(4,)=?;//返回具体客户端或-1,-1代表没有连接
clients.add(?);
int cnt = select(4,{客户列表});//由多路复用器来监听客户列表中是否需要执行读写操作并告知thread
if(cnt>0) {//有事件发生才处理
recv(cnt);
}
}
当selector连接多个channel时,它会监听每一个channel看是否有读写事件发生,然后再由selector通知thread哪个channel上需要执行什么事件由thread执行。
图片来自这位大佬的博客:BIO、NIO_bio nio-CSDN博客