(源码之后放Gitee上)
1. IO模型
1.1 IO模型基本说明
IO模型类别 | 概念 | 优点 | 缺点 |
---|---|---|---|
BIO | 同步并阻塞 ,服务器实现方式为一个请求就开辟一个线程处理,除非使用线程池,但终归是一对一的线程和请求 | 编程简单,使用🐟连接数少且架构固定的项目 | 连接空闲时,开销时是不必要的 |
NIO | 同步非阻塞 ,服务器实现方式为一个线程去处理多个io请求(连接),客户端发送的请求都会注册到io多路复用器上,多路复用器轮询到连接通道上有io请求时就进行处理 | 连接数多,并且短的架构,比如聊天服务器,弹幕系统 | 编程复杂 |
AIO | 异步 ,启用了proactor模式,有效请求才去启动线程,由操作系统完成后再去通知服务端程序处理请求 | 连接数多且时间长的服务 |
1.2 bio工作机制
模型:
工作流程:
问题分析:
-
每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。
-
当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
-
连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
代码实例
public class BIOServer {
public static void main(String[] args) throws IOException {
//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
System.out.println("wating for connecting");
//监听,等待客户端连接, 若无连接是阻塞状态
final Socket accept = serverSocket.accept();
System.out.println("connect to one client");
newCachedThreadPool.execute(() -> {
// 可以和客户端通讯
handle(accept);
});
}
}
public static void handle(Socket socket) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
try {
//通过socket 获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println(Thread.currentThread().getName() + "---wating for reading");
//阻塞式读取
int read = inputStream.read(bytes);
// -1代表读到文件尽头了
if (read != -1) {
System.out.println(new String(bytes, 0, read));
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("---closing connection");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
terminal连接成功后,不断输入字符
1.3nio工作机制
1.3.1基本概念
- Java NIO 全称 java non-blocking IO,是同步非阻塞的
- NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
,其基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道 - Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果
目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞
,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
简单理解可以类比配送外卖,假设有个智障配置系统,骑手只能挨家去查看有无外卖需要配送,并且不断在固定的商家之间循环配送,但是由于时间关系,假设有一家外卖还没做出来,骑手不会等待会继续查看下一家的外卖准备情况,有就拿走配送,无就下一家继续查看。
1.3.2 和bio的比较
io模式 | 处理方式 | 是否阻塞 |
---|---|---|
bio | 基于字符流和字节流 | 阻塞 |
nio | 基于块的,有三大组件,Channel(通道)和 Buffer(缓冲区)和Selector(选择器) , | 非阻塞 |
1.3.3 核心原理
selector会根据不同的事件在各个通道上进行切换。
1.3.3.1Buffer
Buffer:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),即使Channel 是提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
简单的例子可以更好认识buffer,注释部分可以逐个去掉试下效果
public class BasicBuffer {
public static void main(String[] args) {
// 创建一个Buffer, 大小为 5, 即可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i * 2);
}
// 从缓冲区读取数据
// 写时需要切换模式
intBuffer.flip();
//intBuffer.position(1);
//intBuffer.limit(3);
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
可以点击进入java.nio.Buffer
源码部分,查看主要的继承关系和主要的成员变量。其中mark是反转读写的用于标记的位置。
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
1.3.3.2Channel
NIO的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
首先入手两个例子
public static void main(String[] args) {
File file = new File("D:\\Java\\channel.txt");
try {
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream(file);
//通过 fileOutputStream 获取 对应的 FileChannel
FileChannel channel = fileOutputStream.getChannel();
String s = "真的会有人选择阿福吗";
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将 str 放入 byteBuffer
byteBuffer.put(s.getBytes());
//对byteBuffer 进行flip,准备进行写操作
byteBuffer.flip();
//将byteBuffer 数据写入到 fileChannel
channel.write(byteBuffer);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
public static void main(String[] args) throws IOException {
File file = new File("D:\\Java\\channel.txt");
//创建文件的输入流
FileInputStream fileInputStream = new FileInputStream(file);
//通过fileInputStream 获取对应的FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将 通道的数据读入到Buffer
fileChannel.read(byteBuffer);
//将byteBuffer 的 字节数据 转成String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
进入Channel类查看继承关系,发现其下有几个主要实现类。
其中FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
1.3.3.3Selector
Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
实现照样是例子先行。体验到底怎么个非阻塞法。
//服务端
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//通过channel得到ServerSocket,然后绑定(监听)一个端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//得到一个Selecor对象
Selector selector = Selector.open();
//把 serverSocketChannel 注册到selector,设置关心事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//select能返回各个通道处发生事件的数量
if (selector.select(1000) == 0) {
System.out.println("sys has been waiting for 1s");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//判断key 对应的通道发生的事件类型,做对应的处理
if (key.isAcceptable()) {//代表OP_ACCEPT, 有新的客户端连接
//为该客户端的连接,新建一个socketChannel,相当于bio中的socket一般
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//并将该通道注册到selector上,设置该通道的关注事件为OP_READ
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {//代表OP_READ,客户端有数据传送过来
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
int read = channel.read(buffer);
System.out.println(new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
iterator.remove();
}
}
}
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(address)) {
while (!socketChannel.finishConnect()) {
System.out.println("client can do other thing...");
}
}
String s = "hah shssssssssssssssssssssssssssssssssssssssssss";
ByteBuffer byteBuffer = ByteBuffer.wrap(s.getBytes());
socketChannel.write(byteBuffer);
System.in.read();
}
进入Selector抽象类,可以查看几个主要的方法。
工作流程: