Java中的IO操作分为三种模式:同步阻塞式(BIO),同步非阻塞式(NIO),异步非阻塞式(AIO),下面主要讲解BIO和NIO。
1.1 什么是BIO
BIO(Block IO):面向流传输(input/output),同步阻塞式IO。
- InputStream: 输入流(用于读取字节)
- OutputStream: 输出流(用于写入字节)
在JDK1.4之前,Java网络编程中使用java.net包中的api,数据读写则使用Socket类中i / o流来传输。
BIO概述:为了解决服务器单个线程I/O请求阻塞,服务器实现模式为一个连接请求一个线程,客户端只要有连接请求服务器就会启动一个线程去处理。
1.1.1 BIO处理方式
通过多线程实现在不同的线程中去处理不同客户端的I/O请求(1:1)。
1.1.2 服务端代码示例
-
ServerSocket serverSocket = new ServerSocket(8888); // 监听端口8888
-
try {
-
while (true) {
-
Socket socket = serverSocket.accept(); //阻塞式接收socket连接
-
new Thread(() -> { //创建一个新的线程
-
byte[] b = new byte[1024];
-
try {
-
InputStream in = socket.getInputStream();
-
int len = in.read(b); // 读取客户端发送的数据
-
System.out.println("接收到客户端数据:" + new String(b,0,len));
-
OutputStream out = socket.getOutputStream();
-
out.write("successful...".getBytes()); // 返回数据到客户端
-
//关闭连接
-
in.close(); out.close(); socket.close();
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}).start();
-
}
-
} catch (Exception e) {
-
e.printStackTrace();
-
} finally {
-
serverSocket.close();
-
}
-
-
1.1.3 存在的问题
虽然使用多线程解决了多个客户端I/O请求服务端导致阻塞的问题,但是一个连接请求去启动一个线程,在高并发环境下,线程数量过多,会消耗过多的内存资源出现内存溢出,严重则导致服务器宕机。
这里顺便提一下(伪异步IO):服务端采用固定大小的线程池和队列的方式来处理客户端的连接,无论多少个客户端连接都不会导致内存溢出等问题,但是当线程池中线程的数量达到了最大值时,新的客户端连接会一直在队列中等待(线程池释放某个客户端线程的资源)线程池有空闲位置;如果队列数量满时,会造成大量客户端请求连接等待超时。
2.1 什么是NIO
NIO(官方New IO,又称Non Block IO):面向缓冲区(channel传输),同步非阻塞式IO,选择器(多路复用器)
-
Channel:用来传输字节数据,可以进行双向传输(read/write)
-
Buffer:缓存区用来存储字节数据
-
Selector:多路复用器,所有的客户端连接请求(channel)注册到选择器上面,单个线程处理多个channel连接
在JDK1.4,Windows下采用select模型,Linux下采用Linux IO模型 epoll(有兴趣可以了解下Linux IO模式);NIO编程直接使用java.nio包中的api,数据传输和读写则使用Channel和ByteBuffer。
NIO概述:服务端实现模式为一个请求(read/write)一个线程,客户端有连接请求会被注册到Selector中,多路复用器轮询到连接有I/O请求时才会启动一个线程去处理数据。
2.1.1 处理方式
当Selector轮询到某个channel中有read/write请求时,才会去启动一个线程去处理。
Selector的好处在于:单线程来处理更多的客户端连接请求(M:1)。
2.1.2 服务端代码示例
-
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
-
serverSocketChannel.configureBlocking(false);//切换为非阻塞模式
-
serverSocketChannel.bind(new InetSocketAddress(8888));//监听端口8888
-
//获取选择器
-
Selector selector = Selector.open();
-
//将通道注册到选择器,等待连接
-
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
-
//获取选择器中已经准备就绪的事件
-
while (selector.select() > 0) {
-
//获取当前选择器所有注册的监听事件
-
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
-
while (selectionKeys.hasNext()) {
-
//获取事件
-
SelectionKey sk = selectionKeys.next();
-
if (sk.isAcceptable()) {
-
//获取客户端连接
-
SocketChannel socketChannel = serverSocketChannel.accept();
-
socketChannel.configureBlocking(false); //切换非阻塞模式
-
socketChannel.register(selector,SelectionKey.OP_READ);//注册选择器为读模式
-
}else if(sk.isReadable()){ //(读就绪)有可读数据
-
new Thread(() -> {
-
//获取当前选择器上读就绪状态的通道
-
//读取客户端数据,这里省略
-
}).start();
-
}else if(sk.isWritable()){ //(写就绪)可写数据,一般不需要去注册该(可写)事件,在读取数据后写入即可
-
new Thread(() -> {
-
//获取当前选择器上写就绪状态的通道
-
//写入数据到客户端,这里省略
-
}).start();
-
}
-
selectionKeys.remove(); //移除通道中的事件
-
}
-
}
-
//关闭通道
-
serverSocketChannel.close();
2.1.3 存在的问题
1.JDK提供的java.nio包进行NIO编程会比较复杂(需要熟练掌握nio包和Java多线程),ByteBuffer读写时需要使用flip()和clear()进行切换,包括字节数据出入站的编解码等问题,建议使用NIO框架(Netty,Mina)来进行开发。
2.传输数据量过大,读写过程长时,由于同步需要等待整个操作完成才会返回,这时需要考虑业务场景来使用。
总结
区别:
BIO:面向流传输,阻塞式IO。
NIO:面向缓冲区,非阻塞式IO,选择器。
内存开辟:
BIO:堆内存中,操作数据时需要先将堆内存拷贝到堆外内存。
NIO:直接内存,减少了垃圾回收,加快复制的速度(如果存在内存泄漏(堆外内存泄漏),难以排查)。
使用场景:
BIO:一个连接一个线程(一个线程处理一个连接),如果连接数少,它的延迟是最低的,适用于连接数少延迟低的场景。
NIO:一个请求一个线程,阻塞数据处理但不阻塞数据接收,适用于数据量小并发高的场景。