NIO 非阻塞 IO
1介绍
1.1 JAVA NIO
BIO:阻塞式IO,我们传统的流式IO就是阻塞式IO,意思式在读写的过程中可能发生阻塞
NIO:非阻塞IO,读写过程中不会产生阻塞。
AIO:在NIO基础上完成异步非阻塞。 NIO属于同步非阻塞。
同步:单线程上执行的操作,执行存在先后顺序。
异步:多个线程执行的操作,执行没有先后顺序。
IO发生阻塞最常见的场景出现与网络上的IO操作。好比TCP连接中Socket产生IO阻塞。
之前使用流式IO完成TCP通讯式,只能依靠多线程来完成同时多客户端交互。
那么使用了NIO后,就可以以单线程形式完成上述操作。
好处:资源开销小,运行性能好。
缺点:NIO设计不够优雅。以后有了NIO知识后,可以学习netty框架。
NIO实现(复制文件)代码演示:
public class NIODemo {
public static void main(String[] args) throws IOException {
/*
从读写文件入手,初识NIO的API
声明:虽然java对于读写本地文件也提供NIO的API,但是本身读写文件
并不会产生阻塞问题!
BIO是面向"流"的。它是单向操作:要么读(输入流),要么写(输出流)
NIO是面向"通道"的。它是双向的:能读能写。
*/
FileInputStream fis = new FileInputStream("NARUTO.JPG");
FileOutputStream fos = new FileOutputStream("NARUTO_CP.JPG");
//通过流获取对应的NIO通道,用通道对文件进行读写操作
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
/*
NIO的读写是基于缓冲区的Buffer。
类似与我们使用传统的流进行的"块"读写操作。
*/
//创建一个10KB的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024*10);
int len;//记录每次实际读取到的字节数
while((len = inChannel.read(buffer))!=-1) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
System.out.println("复制完毕");
inChannel.close();
outChannel.close();
}
}
1.2 NIO缓存操作
读入数据装满空间 读入空间的一部分数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SQVWPDgB-1656237914892)(C:\Users\tarena\AppData\Roaming\Typora\typora-user-images\image-20220626163953324.png)]
小项目体验IO与NIO区别:
服务端代码:
/**
* 基于NIO的聊天室服务端
*/
public class NIOServer {
//存放所有客户端Channel,便于广播消息
private List<SocketChannel> allChannel = new ArrayList<>();
public void start(){
try {
/*
NIO的API中对应的ServerSocket为:ServerSocketChannel
作用:
1:打开服务端口
2:等待客户端的连接(非阻塞的)
它相较于ServerSocket而言,accept操作并不会阻塞线程。
*/
ServerSocketChannel serverSocketChannel
= ServerSocketChannel.open();
//需要显示的设置为非阻塞状态,必须为非阻塞状态才能交给多路选择器监控事件
serverSocketChannel.configureBlocking(false);
//打开服务端口8088,客户端就可以通过8088端口进行连接了
serverSocketChannel.bind(new InetSocketAddress(8088));
/*
NIO的核心API:多路选择器Selector select:选择
多路选择器就是一个"监控设备",可以同时监控多路"通道",一旦一个
通道触发了某个需要处理的事件时
比如:
ServerSocketChannel的accept操作(一个客户端连接时,接受连接操作)
SocketChannel的read操作(当一个客户端发送消息过来,需要读取的操作)
(以上两点在原有的:ServerSocket的accept和Socket获取的输入流上进行
读取操作都会产生阻塞。)
此时多路选择器就会通知我们来进行该通道的处理动作。因此有了多路选择器
原来会导致线程阻塞的操作全部由多路选择器接管,解除了线程阻塞的现象。
*/
//创建一个多路选择器
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
/*
询问多路选择器,现在有没有事件待处理?
若现在注册在多路选择器上所有通道都没有事件待处理时,该方法会阻塞
若存在待处理的事件,此方法会立即返回一个数字,表示待处理事件的个数
*/
while(true) {
System.out.println("主线程等待多路选择提示是否有事件要处理...");
selector.select();//该方法会形成阻塞
System.out.println("多路选择器监听到有事件要进行处理!!");
//通过多路选择器将所有待处理的事件全部获取回来进行逐一处理
Set<SelectionKey> set = selector.selectedKeys();
//遍历每一个事件并进行对应处理
for (SelectionKey key : set) {
//使用分支判断该事件是什么事件,以便做对应的处理
//判断该事件是否为ServerSocketChannel中的accept事件(有客户端连接了)
if (key.isAcceptable()) {
//通过事件获取相关通道来进行对应的操作
//对于accept事件而言,只有ServerSocketChannel会触发。
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
/*
通过ServerSocketChannel的accept得到一个SocketChannel
通过该SocketChannel的read和write方法就可以与连接的客户端交互了
*/
SocketChannel socket = ssc.accept();//接电话
/*
当使用ServerSocketChannel调用accept方法时,可能返回的
SocketChannel为null(坑爹)
*/
if (socket != null) {
//将当前SocketChannel存入共享集合
allChannel.add(socket);
//将该SocketChannel注册到多路选择上,让其监听发送消息过来的事件
socket.configureBlocking(false);//将SocketChannel设置为非阻塞的,这样才能交给多路选择器
socket.register(selector, SelectionKey.OP_READ);
}
//判断事件是否为有的通道上有消息可以读取(某个客户端发送消息过来了)
}else if(key.isReadable()){
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024*10);
int len = socketChannel.read(buffer);
if(len==-1){//客户端断开连接,服务端这里才会读取到-1
socketChannel.close();//断开连接
allChannel.remove(socketChannel);
}
buffer.flip();//position-limit之间就是刚刚读取到的数据了。
/*
flip后
position:0
limit:读取到的字节长度
*/
byte[] data = new byte[buffer.limit()];
/*
Buffer的get方法定义:
get(byte[] data)
此方法会将Buffer内部数据中从position开始的连续data.length
个字节存入到data数组中
*/
buffer.get(data);//调用后data中保存的就是position-limit之间的数据
/*
get完毕后:
position和limit位置又一样了。
*/
String line = new String(data, StandardCharsets.UTF_8);
System.out.println("客户端说:"+line);
//遍历allChannel集合,将消息发送给所有客户端
for(SocketChannel sc : allChannel){
buffer.flip();//limit=position,position=0
sc.write(buffer);//limit与position值相同了
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
NIOServer server = new NIOServer();
server.start();
}
}
客户端代码:
/**
* 聊天室的客户端
*/
public class Client {
/*
java.net.Socket
封装了TCP协议的通讯细节,我们基于它就可以与远端计算机建立TCP连接,并基于一对
输入与输出流的读写完成与远端计算机的数据交互操作。
可以把Socket想象成"电话"
*/
private Socket socket;
/*
构造器中完成初始化操作
*/
public Client(){
try {
/*
Socket的构造器:
Socket(String host,int port)
参数1:要连接的远端计算机(服务器)的IP地址
参数2:要连接的运行在远端计算机上的服务端应用程序开启的服务端口
我们通过IP地址可以找到网络上的服务器计算机,通过端口可以连接到运行在
该服务器计算上的服务端应用程序。
并且这个构造器实例化Socket的过程就是与服务端建立连接的过程,如果连接
失败会抛出异常.
如果指定的服务端地址与端口不对,那么连接时会抛出异常:
java.net.ConnectException:Connection refused : connect
*/
System.out.println("正在连接服务端...");
socket = new Socket("localhost",8088);
System.out.println("已连接服务端!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
ServerHandler serverHandler = new ServerHandler();
Thread t = new Thread(serverHandler);
t.setDaemon(true);
t.start();
/*
Socket提供的方法:
OutputStream getOutputStream()
通过socket获取一个字节输出流,使用该输出流写出的字节数据会发送给
建立连接的远端计算机。而对方也可以通过建立连接的Socket获取输入流
读取到我们写出的字节。
*/
//低级流,通过该流写出的字节就发送给了远端计算机
OutputStream out = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw);
PrintWriter pw = new PrintWriter(bw,true);
Scanner scanner = new Scanner(System.in);
while(true){
String line = scanner.nextLine();//获取控制台输入的一行字符串
if("exit".equalsIgnoreCase(line)){
break;//停止循环
}
pw.println(line);//将输入的内容发送给服务端
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//与服务端断开连接
try {
/*
Socket提供了close方法,该方法内部会将通过它获取的输入流和输出流
全部关闭,同时还会跟远端计算机做最后的4次挥手动作(TCP协议断开的操作)
*/
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
private class ServerHandler implements Runnable{
public void run() {
try{
/*
通过socket获取输入流,读取服务端发送过来的消息
*/
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr);
String message;
while((message = br.readLine()) != null){
System.out.println(message);
}
}catch(IOException e){
}
}
}
}
BufferedReader br = new BufferedReader(isr);
String message;
while((message = br.readLine()) != null){
System.out.println(message);
}
}catch(IOException e){
}
}
}
}