Java NIO和IO的主要区别
经典IO | NIO |
---|---|
Stream oriented(面向流) | Buffer oriented (面向缓冲) |
Blocking IO (阻塞式IO) | Non blocking IO (非阻塞式IO) |
– | Selectors |
1,BIO的Stream(流)和NIO的缓冲区
Java NIO和IO之间的第一个重要区别是IO是面向流的,其中NIO是面向缓冲区的。那么,这意味着什么?
面向流的Java IO意味着您可以从流中一次读取一个或多个字节。你对读取的字节做什么取决于你。它们不会缓存在任何地方。此外,您无法在流中的数据中前后移动。如果需要在从流中读取的数据中前后移动,则需要先将其缓存在缓冲区中。
Java NIO的面向缓冲区的方法略有不同。数据被读入缓冲区,稍后处理该缓冲区。你可以根据需要在缓冲区中前后移动。这使你在处理过程中具有更大的灵活性。但是,你还需要检查缓冲区是否包含完整处理所需的所有数据。并且,你需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。
2,BIO阻塞和NIO非阻塞
Java IO的各种流都是blocking(阻塞)的。这意味着,当线程调用read()或write()时,该线程将被阻塞,直到有一些数据要读取,或者数据被完全写入,在此期间,该线程无法执行任何其他操作。
Java NIO的非阻塞模式允许线程请求从通道读取数据,并且只获取当前可用的内容,或者根本没有数据,如果当前没有数据可用。线程可以继续使用其他内容,而不是在数据可供读取之前保持阻塞状态。
非阻塞写入也是如此,线程可以请求将某些数据写入通道,但不要等待它完全写入。然后线程可以继续并在同一时间做其他事情。
线程在IO调用中没有阻塞时花费空闲时间,通常在此期间在其他通道上执行IO。也就是说,单个线程现在可以管理多个输入和输出通道。
3,选择器Selectors
Java NIO的选择器允许单个线程监视多个输入通道。你可以使用选择器注册多个通道,然后使用单个线程“选择”具有可用于处理的输入的通道,或者选择准备写入的通道。这种选择器机制使单个线程可以轻松管理多个通道。
通过三个方面的对比,NIO比BIO的性能更高,NIO是异步非阻塞,读写的效率非常高。
二,NIO和BIO使用中区别
BIO
服务端代码:
package com.yitang.test;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端
* @author 汤义
* @create 2024-06-15:12
*/
public class test_BIO {
public static void main(String[] args) {
byte[] buffer = new byte[1024];
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器已启动并监听8080端口");
while (true) {
System.out.println();
System.out.println("服务器正在等待连接...");
Socket socket = serverSocket.accept();
System.out.println("服务器已接收到连接请求...");
System.out.println();
System.out.println("服务器正在等待数据...");
socket.getInputStream().read(buffer);
System.out.println("服务器已经接收到数据");
System.out.println();
String content = new String(buffer);
System.out.println("接收到的数据:" + content);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
步骤:
1,创建ServerSocket
2,等待连接服务
3,等待接受数据
客户端:
package com.yitang.test;
import java.io.IOException;
import java.net.Socket;
/**
* 客户端
* @author 汤义
* @create 2024-06-15:14
*/
public class test_client_BIO {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1",8080);
String message = null;
Scanner sc = new Scanner(System.in);
message = sc.next();
socket.getOutputStream().write(message.getBytes());
socket.close();
sc.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
步骤:
1,连接服务
2,发送数据
执行结果:
启动服务端
启动客户端,服务端打印结果:
客户端向服务器发送数据打印结果:
这里是三个步骤,启动服务器,连接服务器,接受数据。
这是一个很典型的阻塞BIO,启动服务器。没有客户端连接是,阻塞队列,执行到第一个“服务器正在等待连接…” 。但有客服端连接时,会执行接受连接请求,执行到第二个“服务器正在等待连接…” 。当客户端发送数据过来,会执行收到数据步骤。这就是BIO的执行流程。这里有很明显的两个阻塞。
以上是BIO的单线程的,如果向让其如何变为多线程的,同时可以接受多个请求了?
其实也很简单,加线程。将服务端进行改造:
package com.yitang.test;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务端
* @author 汤义
* @create 2024-06-15:12
*/
public class test_BIO {
public static void main(String[] args) {
byte[] buffer = new byte[1024];
try {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器已启动并监听8080端口");
while (true) {
System.out.println();
System.out.println("服务器正在等待连接...");
Socket socket = serverSocket.accept();
//创建线程,可以同时处理多个请求
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("服务器已接收到连接请求...");
System.out.println();
System.out.println("服务器正在等待数据...");
try {
socket.getInputStream().read(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("服务器已经接收到数据");
System.out.println();
String content = new String(buffer);
System.out.println("接收到的数据:" + content);
}
}).start();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端还是保持不变,启动两个客户端打印结果:
客户端发送数据打印结果:
多线程BIO服务器虽然解决了单线程BIO无法处理并发的弱点,但是也带来一个问题:如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力。
NIO
package com.yitang.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* @author 汤义
* @create 2024-06-16:05
*/
public class test_NIO {
public static void main(String[] args) throws InterruptedException {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
List<SocketChannel> socketList = new ArrayList<SocketChannel>();
try {
//Java为非阻塞设置的类
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8081));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel==null) {
//表示没人连接
System.out.println("正在等待客户端请求连接...");
Thread.sleep(5000);
}else {
System.out.println("当前接收到客户端请求连接...");
socketList.add(socketChannel);
}
for(SocketChannel socket:socketList) {
socket.configureBlocking(false);
int effective = socket.read(byteBuffer);
if(effective!=0) {
byteBuffer.flip();//切换模式 写-->读
String content = Charset.forName("UTF-8").decode(byteBuffer).toString();
System.out.println("接收到消息:"+content);
byteBuffer.clear();
}else {
System.out.println("当前未收到客户端消息");
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
客户端还是用的原来的。
执行结果:
这里没有没有阻塞,但有个新问题了,为了能接受消息,这里使用一个轮巡,遍历已连接的通道,是否有新消息数据。如果数据量大,这种处理方式效率太低了。
比如有1000万连接中,我们可能只会有100万会有消息,剩下的900万并不会发送任何消息,那么这些连接程序依旧要每次都去轮询,这显然是不合适的。
完整的NIO简单使用
这里会加上多路复用器Selector,这里作用类似上部例子中轮询,但效率比上面要高的多。
//使用多路复用器
public static void main(String[] args) throws InterruptedException {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
List<SocketChannel> socketList = new ArrayList<SocketChannel>();
try {
//Java为非阻塞设置的类
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8081));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 打开Selector 处理Channel, 即创建epoll
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
//阻塞等待需要处理的事件发生
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if (key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("客户端连接成功");
}else if (key.isReadable()){
SocketChannel socketChannel = (SocketChannel)key.channel();
ByteBuffer byteBuffer1 = ByteBuffer.allocate(6);
int len = socketChannel.read(byteBuffer1);
if (len > 0){
System.out.println("接收到消息"+new String(byteBuffer1.array()));
}else if (len == -1){
System.out.println("客户端断开连接");
socketChannel.close();
}
}
}
iterator.remove();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
执行结果:
先介绍代码中对象的含义:
SocketChannel:网络套接字IO通道,TCP协议,客户端通过 SocketChannel 与服务端建立TCP连接进行通信交互。与传统的Socket操作不同的是,SocketChannel基于非阻塞IO模式,可以在同一个线程内同时管理多个通信连接,从而提高系统的并发处理能力。
ServerSocketChannel:网络套接字IO通道,TCP协议,服务端通过ServerSocketChannel监听来自客户端的连接请求,并创建相应的SocketChannel对象进行通信交互。ServerSocketChannel同样也是基于非阻塞IO模式,可以在同一个线程内同时管理多个通信连接,从而提高系统的并发处理能力
什么是多路复用机制:
一个线程可以处理成千上万的客户端连接,底层调用的3个epoll函数,通过事件响应机制(监听机制)实现事件的轮询。epoll实现事件的监听机制,有事件收发就处理,没有就会堵塞进行监听。
SelectionKey:
每个 Channel向Selector 注册时,都会创建一个 SelectionKey 对象,通过 SelectionKey 对象向Selector 注册,且 SelectionKey 中维护了 Channel 的事件。常见的四种事件如下:
1, OP_READ:当操作系统读缓冲区有数据可读时就绪。
2, OP_WRITE:当操作系统写缓冲区有空闲空间时就绪。
3, OP_CONNECT:当 SocketChannel.connect()请求连接成功后就绪,该操作只给客户端使用。
4,OP_ACCEPT:当接收到一个客户端连接请求时就绪,该操作只给服务器使用。
下图是NIO运行的流程: