深入浅析BIO、NIO、AIO
视频资源位置:IO模式讲解(AIO&BIO&NIO)_哔哩哔哩_bilibili
BIO、NIO、AIO
Java的I/O演进之路I/O模型
:就是用什么样的通道或者说是通信模式和架构进行数据的传输和接收,很大程度上决定了程序通信的性能,Java共支持3种网络编程的I/O模型:BIO、NIO、AIO
实际 通行需求下,要根据不同的业务场景和性能需求决定选择不同的I/O模型
1.I/O模型
BIO
同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销
NIO
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
AIO
(又称为NIO 2.0)异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用
异步非阻塞:为什么说AIO是异步非阻塞?通过AIO发起个文件IO操作之后,你立马就返回可以干别的事儿了,接下来你也不用管了,操作系统自己干完了IO之后,告诉你说ok了, 当你基于AIO的api去读写文件的时候, 当你发起一个请求之后,剩下的事情就是交给了操作系统,当读写完成后, 操作系统会来回调你的接口, 告诉你操作完成, 在这期间不需要等待, 也不需要去轮询判断操作系统完成的状态,你可以去干其他的事情。 同步就是自己还得主动去轮询操作系统,异步就是操作系统反过来通知你。所以来说, AIO就是异步非阻塞的。
2.适用场景分析
1、BlO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
2、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
3、AlO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS(OutputStream)参与并发操作,编程比较复杂,JDK7开始支持。
BIO 深入剖析
1.简介
Java BIO就是传统的java io 编程,其相关的类和接口在java.io
BlO(blocking l/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器).
2.BIO工作模式
3.实例分析
单发机制
但发单收
Server
package bio; import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) { try { //对服务端端口进行注册 9999 ServerSocket serverSocket = new ServerSocket(9999); //监听客户端socket请求 Socket socket = serverSocket.accept(); //从socket管道中得到一个字节输入流对象 InputStream is = socket.getInputStream(); //将字节输入流包装成一个缓冲字符输入流,提高效率 BufferedReader br = new BufferedReader(new InputStreamReader(is)); String msg; while ((msg = br.readLine()) != null){ System.out.println("接收到客户端消息:"+msg); } } catch (IOException e) { e.printStackTrace(); } } }
Client
package bio; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; public class Client { public static void main(String[] args) { try { //创建socket链接 Socket socket = new Socket("127.0.0.1",9999); //从socket对象中获取一个输出流 OutputStream out = socket.getOutputStream(); //把字节输出流包装成打印流 PrintStream printStream = new PrintStream(out); printStream.println("hello"); printStream.flush(); } catch (IOException e) { e.printStackTrace(); } } }
小结
- 在以上通信中,服务端会一致等待客户端的消息,如果客户端没有进行消息的发送江服务端将一直进入阻塞状态。
- 同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态!
- 可以考虑改为if判断
- 由于这种机制是端到端的,若连接的Client Socket断了,服务端的Socket也会出现异常机制。
多发和多收机制
在上一个demo的基础上作出一些修改,使得Client可以持续输入,然后后Server可以持续输出。保持两端之间通道的连接。
Server
package bio.persistent; 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 Server { public static void main(String[] args) { try { System.out.println("已开启Socket,等待客户端连接中"); //对服务端端口进行注册 9999 ServerSocket serverSocket = new ServerSocket(9999); //监听客户端socket请求 Socket socket = serverSocket.accept(); System.out.println("匹配客户端成功"); //从socket管道中得到一个字节输入流对象 InputStream is = socket.getInputStream(); //将字节输入流包装成一个缓冲字符输入流,提高效率 BufferedReader br = new BufferedReader(new InputStreamReader(is)); String msg; while ((msg = br.readLine()) != null){ System.out.println("接收到客户端消息:"+msg); } } catch (IOException e) { e.printStackTrace(); } } }
Client
package bio.persistent; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class Client { public static void main(String[] args) { try { //创建socket链接 Socket socket = new Socket("127.0.0.1",9999); //从socket对象中获取一个输出流 OutputStream out = socket.getOutputStream(); //把字节输出流包装成打印流 PrintStream printStream = new PrintStream(out); Scanner scanner = new Scanner(System.in); while (true){ String msg = scanner.nextLine(); printStream.println(msg); printStream.flush(); } } catch (IOException e) { e.printStackTrace(); } } }
接收多个客户端
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:
目标
实现服务端可以同时接收多个客户端的Socket通信需求。
思路
服务端每接收到一个客户端socket请求对象之后都交给一个独立的线程来处理客户端的数据交互需求
代码
Server
package bio.Multi; 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 Server { public static void main(String[] args) { try { //注册 端口 ServerSocket serverSocket = new ServerSocket(9999); //定义一个死循环,负责不断的去接受Client的Socket请求 while (true){ Socket socket = serverSocket.accept(); new Thread(()->{ try { InputStream in = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String msg; while ((msg = br.readLine()) != null){ System.out.println(Thread.currentThread().getName()+"接收到消息---->"+msg); } } catch (IOException e) { e.printStackTrace(); } }).start(); } } catch (IOException e) { e.printStackTrace(); } } }
Client
这里每启动一个Client,Server都会启动一个新的Thread去处理Client的请求
package bio.Multi; import java.io.IOException; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class Client { public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1",9999); PrintStream ps = new PrintStream(socket.getOutputStream()); Scanner sc = new Scanner(System.in); while (true){ System.out.print("说点啥吧:"); String msg = sc.nextLine(); ps.println(msg); ps.flush(); } } catch (IOException e) { e.printStackTrace(); } } }
小结
- 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能
- 每个线程都会占用栈空间和CPU资源;
- 并不是每个socket都进行IO操作,无意义的线程处理(等待)
- 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
4.伪异步I/O编程
概述
在上述案例中:客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
目标
开发实现伪异步通行框架
代码
Server
package bio.pseasyn; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Server { public static void main(String[] args) { try { //对服务端端口进行注册 9999 ServerSocket serverSocket = new ServerSocket(9999); /** * 手动创建线程池 * 核心线程 3 ,最大 4,等待释放5s,任务队列4 */ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,4,5, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); while (true){ //监听客户端socket请求 Socket socket = serverSocket.accept(); //线程池执行 threadPoolExecutor.execute(new Thread(()->{ try { InputStream in = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(in)); String msg; while ((msg = br.readLine()) != null){ System.out.println(Thread.currentThread().getName()+"接收到客户端消息------>"+msg); } } catch (IOException e) { e.printStackTrace(); } })); } } catch (IOException e) { e.printStackTrace(); } } }
Client
这里和前面一样,保持连接即可
package bio.pseasyn; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner; public class Client { public static void main(String[] args) { try { //创建socket链接 Socket socket = new Socket("127.0.0.1",9999); //从socket对象中获取一个输出流 OutputStream out = socket.getOutputStream(); //把字节输出流包装成打印流 PrintStream printStream = new PrintStream(out); Scanner scanner = new Scanner(System.in); while (true){ String msg = scanner.nextLine(); printStream.println(msg); printStream.flush(); } } catch (IOException e) { e.printStackTrace(); } } }
小结
- 伪异步io采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
- 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续套接字的I/O消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时.
5.基于BIO形式下的文件上传
目标
支持任意类型文件的上传
代码实现
Server
package bio.file; import java.io.DataInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.UUID; public class Server { public static void main(String[] args) { try{ ServerSocket serverSocket = new ServerSocket(8888); //如果要继续使用这种方式的话还是推荐使用线程池来管理,这里图个方便我就不用了 new Thread(()->{ try { Socket socket = serverSocket.accept(); DataInputStream in = new DataInputStream(socket.getInputStream()); OutputStream out = new FileOutputStream("保存的路径地址"+ UUID.randomUUID().toString() +in.readUTF()); int len; byte[] bytes = new byte[1024]; while ((len = in.read(bytes)) != -1){ out.write(bytes,0,len); } out.close(); } catch (IOException e) { e.printStackTrace(); } }).start(); }catch (Exception e){ e.printStackTrace(); } } }
Client
package bio.file; import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.InputStream; import java.net.Socket; public class Client { public static void main(String[] args) { try { Socket socket = new Socket("127.0.0.1",8888); InputStream in = new FileInputStream("文件路径"); //将字节流包装成一个数据输出流 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); int len; byte[] bytes = new byte[1024]; //发送文件后缀 正常开发下应该使用File去获取文件名 然后split(".")来获取后缀 out.writeUTF(".xxx"); while ((len = in.read(bytes)) != -1){ out.write(bytes,0,len); } //强制刷新输出一波 out.flush(); //通知服务端我们这边数据已经发送完毕了,不用等待了 socket.shutdownOutput(); }catch (Exception e){ e.printStackTrace(); } } }
6.BIO模式下的端口转发思想
需求:需要实现一个客户端的消息可以发送给所有的客户端去接收。
服务端
- 注册端口
- 接受客户端的Socket连接,交给一个独立的线程来处理
- 把当前连接的客户端Socket存入到一个所谓的在线Socket集合中
- 接受客户端的消息,然后推送给当前所有在线的Socket接收
Server
package bio.chat; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; public class Server { public static List<Socket> socketList = new ArrayList<>(); public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(9999); AtomicReference<Socket> localSocket = null; new Thread(()->{ try { Socket socket = serverSocket.accept(); localSocket.set(socket); BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream())); String msg; while ((msg = br.readLine()) != null){ for (Socket s:Server.socketList) { PrintStream ps = new PrintStream(s.getOutputStream()); ps.println(msg); ps.flush(); } } } catch (IOException e) { System.out.println(localSocket+"下线了!"); socketList.remove(localSocket); } }).start(); }catch (Exception e){ e.printStackTrace(); } } }
7.基于BIO模式下的即时通讯
基于BIO模式下的即时通信,我们需要解决客户端到客户端的通信,-也就是需要实现客户端与客户端的端口消息转发逻辑。
NIO 深入剖析
1.概述
NIO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
- NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
- NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。
2.NIO 与 BIO的比较
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流IO高很多
- BIO是阻塞的,NIO则是非阻塞的
- BlO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO BIO
面向缓冲区(Buffer) 面向流(Stream)
非阻塞(Non Blocking IO) 阻塞IO(Blocking IO)
选择器(Selectors)
- NIO可以先将数据写入到缓冲区,然后再有缓冲区写入通道,因此可以做到同步非阻塞,数据是双向的。
- BIO则是面向的流,读写数据都是单向的。因此是同步阻塞。
3.NIO 三大核心原理示意图
NIO有三大核心部分: Channel(通道),Buffer(缓冲区),Selector(选择器)Buffer缓冲区
Buffer(缓冲区)
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。
Channel(通道)
Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
Selector(选择器)
Selector是一个java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
- 每个channel都会对应一个 Buffer
- 一个线程对应Selector ,一个Selector对应多个channel(连接)程序
- 切换到哪个channel是由事件决定的
- Selector 会根据不同的事件,在各个通道上切换
- Buffer 就是一个内存块,底层是一个数组
- 数据的读取写入是通过 Buffer完成的,BlO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写。
- Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到lO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存取数据
4.NIO核心一:缓存区 (Buffer)
一个用于特定基本数据类型的容器。由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的
abstract Object array() 返回数组,支持这个缓冲区 (可选操作)。 abstract int arrayOffset() 返回在缓冲 第一元这个缓冲区支持数组的偏移(可选操作)。 int capacity() 返回此缓冲区的容量。 Buffer clear() 清除此缓冲区。 Buffer flip() 翻转这个缓冲区。将position赋值给limit,然后position初始化为0 abstract boolean hasArray() 告诉是否这个缓冲区是由一个可访问的数组支持的。 boolean hasRemaining() 告诉当前位置和极限之间是否有任何元素。 abstract boolean isDirect() 告诉这是否是 direct缓冲。 abstract boolean isReadOnly() 告诉是否该缓冲区是只读的。 int limit() 返回此缓冲区的限制。 Buffer limit(int newLimit) 设置此缓冲区的限制。 Buffer mark() 设置此缓冲区的标记位置。 int position() 返回此缓冲区的位置。 Buffer position(int newPosition) 设置此缓冲区的位置。 int remaining() 返回当前位置和极限之间的元素的数目。 Buffer reset() 重置此缓冲区的位置之前标记的位置。 Buffer rewind() 将此缓冲区。
/* * 一、缓冲区(Buffer):在java NIO 中负者数据的存储。缓冲区就是数组。用于存储不同类型的数据。 * * 根据数据类型的不同(boolean 除外),有以下 Buffer 常用子类: * ByteBuffer * CharBuffer * ShortBuffer * IntBuffer * LongBuffer * FloatBuffer * DoubleBuffer * * 上述缓冲区的管理方式几乎一致,通过allocate()获取缓冲区 * * 二、缓冲区存取数据的两个核心方法: * put():存入数据到缓冲区中 * put(byte b):将给定单个字节写入缓冲区的当前位置 * put(byte[] src):将 src 中的字节写入缓冲区的当前位置 * put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position) * get():获取缓存区中的数据 * get() :读取单个字节 * get(byte[] dst):批量读取多个字节到 dst 中 * get(int index):读取指定索引位置的字节(不会移动 position) * * 三、缓冲区中的四个核心属性: * capacity:容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。 * limit:界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写) * position:位置,表示缓冲区中正在操作数据的位置。 * mark:标记,表示记录当前position位置。可以通过reset()恢复到mark的位置。 * * 0<=mark<=position<=limit<=capacity * * 四、直接缓冲区与非直接缓冲区: * 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM的内存中。 * * 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率 * 此方法返回的 缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区 。 * 直接缓冲区的内容可以驻留在常规的垃圾回收堆之外. * 将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。 * 最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。 * 直接字节缓冲区还可以过 通过FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建 。该方法返回MappedByteBuffe */ public class TestBuffer { @Test public void test1(){ String str="abcde"; //1.分配一个指定大小的缓冲区 ByteBuffer buf=ByteBuffer.allocate(1024); System.out.println("--------------allocate()----------------"); System.out.println(buf.position());//0 System.out.println(buf.limit());//1024 System.out.println(buf.capacity());//1024 //2.利用put()存放数据到缓冲区中 buf.put(str.getBytes()); System.out.println("-------------put()-------------"); System.out.println(buf.position());//5 System.out.println(buf.limit());//1024 System.out.println(buf.capacity());//1024 //3.切换读取数据模式 buf.flip(); System.out.println("--------------flip()------------"); System.out.println(buf.position());//0 System.out.println(buf.limit());//5 System.out.println(buf.capacity());//1024 //4.利用get()读取缓冲区中的数据 byte[] dst=new byte[buf.limit()]; buf.get(dst); System.out.println(new String(dst,0,dst.length));//abcd System.out.println("--------------get()------------"); System.out.println(buf.position());//5 System.out.println(buf.limit());//5 System.out.println(buf.capacity());//1024 //5.rewind():可重复读 buf.rewind(); System.out.println("--------------rewind()------------"); System.out.println(buf.position());//0 System.out.println(buf.limit());//5 System.out.println(buf.capacity());//1024 //6.clear():清空缓冲区。但是缓冲区中的数据依然存在,但是处在“被遗忘”状态 buf.clear(); System.out.println("--------------clear()------------"); System.out.println(buf.position());//0 System.out.println(buf.limit());//1024 System.out.println(buf.capacity());//1024 System.out.println((char)buf.get()); } @Test public void test2(){ String str="abcde"; ByteBuffer buf=ByteBuffer.allocate(1024); buf.put(str.getBytes()); buf.flip(); byte[] dst=new byte[buf.limit()]; buf.get(dst,0,2); System.out.println(new String(dst,0,2));//ab System.out.println(buf.position());//2 //mark():标记 buf.mark(); buf.get(dst,2,2);//再读两个位置 System.out.println(new String(dst, 2, 2));//cd System.out.println(buf.position());//4 //reset():恢复到mark的位置 buf.reset(); System.out.println(buf.position());//2 //判断缓冲区中是否还有剩余数据 if(buf.hasRemaining()){ //获取缓冲区中可以操作的数量 System.out.println(buf.remaining());//3 } } @Test public void test3(){ //分配直接缓冲区 ByteBuffer buf=ByteBuffer.allocate(1024); System.out.println(buf.isDirect());//false } }
常用的Channel实现类
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过 UDP 读写网络中的数据通道。
- SocketChannel:通过 TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】
- FileChannel 类
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道
FileChannel常用方法
int read(ByteBuffer dst) 从 从 Channel 到 中读取数据到 ByteBuffer long read(ByteBuffer[] dsts) 将 将 Channel 到 中的数据“分散”到 ByteBuffer[] int write(ByteBuffer src) 将 将 ByteBuffer 到 中的数据写入到 Channel long write(ByteBuffer[] srcs) 将 将 ByteBuffer[] 到 中的数据“聚集”到 Channel long position() 返回此通道的文件位置 FileChannel position(long p) 设置此通道的文件位置 long size() 返回此通道的文件的当前大小 FileChannel truncate(long s) 将此通道的文件截取为给定大小 void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
实例测试
本地文件写数据
@Test public void write(){ try { FileOutputStream out = new FileOutputStream("data.txt"); //获取字节输出流对应通道 FileChannel fileChannel = out.getChannel(); //分配缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //将写入的内容 加载至缓冲区 buffer.put("hello".getBytes()); //将缓冲区的模式转为写出模式 buffer.flip(); fileChannel.write(buffer); fileChannel.close(); } catch (IOException e) { e.printStackTrace(); } }
本地文件读数据
@Test public void read(){ try { FileInputStream in = new FileInputStream("data.txt"); //获取字节输出流对应通道 FileChannel fileChannel = in.getChannel(); //分配缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取数据 fileChannel.read(buffer); //将缓冲区的起始位置初始化 buffer.flip(); String s = new String(buffer.array(), 0, buffer.remaining()); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } }
本地文件复制
@Test public void copy(){ try { FileInputStream in = new FileInputStream("data.txt"); FileOutputStream out = new FileOutputStream("copy_data"); //获取字节输出流对应通道 FileChannel inChannel = in.getChannel(); FileChannel outChannel = out.getChannel(); //分配缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取数据 while (true){ //必须先清空缓冲然后在写入数据到缓冲区 buffer.clear(); if (inChannel.read(buffer) == -1){ //等于-1 说明读完了 break; } //读完数据过后,下标落实到可读数据的最后一位,因此需要一个复位 buffer.flip(); outChannel.write(buffer); } inChannel.close(); outChannel.close(); System.out.println("ojbk"); } catch (IOException e) { e.printStackTrace(); } }
分散(Scatter)和聚集(Gather)
分散读取:(数据从一个通道到多个缓存)从Channel中读取的数据分散到Buffer中(按照缓冲区的顺序,从Channel中读取的数据依次填满Buffer)
聚集写入:(数据从多个缓存区到一个通道)将多个Buffer数据 “聚集“ 写入到通道中
@Test public void ScatterAndGather(){ try { FileInputStream in = new FileInputStream("data.txt"); FileOutputStream out = new FileOutputStream("sg_data.txt"); //获取字节输出流对应通道 FileChannel inChannel = in.getChannel(); FileChannel outChannel = out.getChannel(); //分配缓冲区 ByteBuffer buffer1 = ByteBuffer.allocate(2); ByteBuffer buffer2 = ByteBuffer.allocate(1024); ByteBuffer[] buffers= {buffer1,buffer2}; //分散读取数据 inChannel.read(buffers); for (ByteBuffer buffer: buffers) { buffer.flip(); System.out.println(new String(buffer.array(),0,buffer.remaining())); } //聚集写出 outChannel.write(buffers); inChannel.close(); outChannel.close(); System.out.println("ojbk"); } catch (IOException e) { e.printStackTrace(); } }
transferForm与transferTo
@Test //把原通道数据复制到目标通道 public void transferTo() throws IOException { //字节输入管道 FileInputStream in = new FileInputStream("data.txt"); FileChannel fileInChannel = in.getChannel(); //字节输出管道 FileOutputStream out = new FileOutputStream("data_out.txt"); FileChannel fileOutChannel = out.getChannel(); //复制 fileInChannel.transferTo(fileInChannel.position(),fileInChannel.size(),fileOutChannel); fileOutChannel.close(); fileInChannel.close(); } @Test //从目标通道中去复制原通道的数据 public void transferForm() throws IOException{ //字节输入管道 FileInputStream in = new FileInputStream("data.txt"); FileChannel fileInChannel = in.getChannel(); //字节输出管道 FileOutputStream out = new FileOutputStream("data_out.txt"); FileChannel fileOutChannel = out.getChannel(); //复制 fileOutChannel.transferFrom(fileInChannel, fileInChannel.position(), fileInChannel.size()); fileOutChannel.close(); fileInChannel.close(); System.out.println("复制完成"); }
while (selector.select() > 0){ System.out.println("开启事件处理"); //7.获取选择器中所有注册的通道中已准备好的事件 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); //8.开始遍历事件 while (it.hasNext()){ SelectionKey selectionKey = it.next(); System.out.println("--->"+selectionKey); //9.判断这个事件具体是啥 if (selectionKey.isAcceptable()){ //10.获取当前接入事件的客户端通道 SocketChannel socketChannel = serverSocketChannel.accept(); //11.切换成非阻塞模式 socketChannel.configureBlocking(false); //12.将本客户端注册到选择器 socketChannel.register(selector,SelectionKey.OP_READ); }else if (selectionKey.isReadable()){ //13.获取当前选择器上的读 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); //14.读取 ByteBuffer buffer = ByteBuffer.allocate(1024); int len; while ((len = socketChannel.read(buffer)) > 0){ buffer.flip(); System.out.println(new String(buffer.array(),0,len)); //清除之前的数据(覆盖写入) buffer.clear(); } } //15.处理完毕后,移除当前事件 it.remove(); } }
Scanner scan = new Scanner(System.in); while(scan.hasNext()){ String str = scan.nextLine(); buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis()) + "\n" + str).getBytes()); buf.flip(); sChannel.write(buf); buf.clear(); } //关闭通道 sChannel.close();
8.NIO非阻塞式网络通信入门案例
需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
代码实现
Server
package nio.ss; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; public class Server { public static void main(String[] args) { try { //1.获取管道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //2.设置非阻塞模式 serverSocketChannel.configureBlocking(false); //3.绑定端口 serverSocketChannel.bind(new InetSocketAddress(8888)); //4.获取选择器 Selector selector = Selector.open(); //5.将通道注册到选择器上,并且开始指定监听的接收事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //6.轮询已经就绪的事件 while (selector.select() > 0){ System.out.println("开启事件处理"); //7.获取选择器中所有注册的通道中已准备好的事件 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); //8.开始遍历事件 while (it.hasNext()){ SelectionKey selectionKey = it.next(); System.out.println("--->"+selectionKey); //9.判断这个事件具体是啥 if (selectionKey.isAcceptable()){ //10.获取当前接入事件的客户端通道 SocketChannel socketChannel = serverSocketChannel.accept(); //11.切换成非阻塞模式 socketChannel.configureBlocking(false); //12.将本客户端注册到选择器 socketChannel.register(selector,SelectionKey.OP_READ); }else if (selectionKey.isReadable()){ //13.获取当前选择器上的读 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); //14.读取 ByteBuffer buffer = ByteBuffer.allocate(1024); int len; while ((len = socketChannel.read(buffer)) > 0){ buffer.flip(); System.out.println(new String(buffer.array(),0,len)); //清除之前的数据(覆盖写入) buffer.clear(); } } //15.处理完毕后,移除当前事件 it.remove(); } } } catch (IOException e) { e.printStackTrace(); } } }
Client
package nio.ss; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Scanner; public class Client { public static void main(String[] args) { try { SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888)); socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024); Scanner scanner = new Scanner(System.in); while (true){ System.out.print("请输入:"); String msg = scanner.nextLine(); buffer.put(msg.getBytes()); buffer.flip(); socketChannel.write(buffer); buffer.clear(); } } catch (IOException e) { e.printStackTrace(); } } }
9.NIO 网络编程应用实例-群聊系统
目标
需求:进一步理解 NIO 非阻塞网络编程机制,实现多人群聊
编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
服务器端:可以监测用户上线,离线,并实现消息转发功能
客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息
代码实现
服务端
package nio.chat; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; /** * */ public class Server { //定义属性 private Selector selector; private ServerSocketChannel ssChannel; private static final int PORT = 9999; //构造器 //初始化工作 public Server() { try { // 1、获取通道 ssChannel = ServerSocketChannel.open(); // 2、切换为非阻塞模式 ssChannel.configureBlocking(false); // 3、绑定连接的端口 ssChannel.bind(new InetSocketAddress(PORT)); // 4、获取选择器Selector selector = Selector.open(); // 5、将通道都注册到选择器上去,并且开始指定监听接收事件 ssChannel.register(selector , SelectionKey.OP_ACCEPT); }catch (IOException e) { e.printStackTrace(); } } //监听 public void listen() { System.out.println("监听线程:" + Thread.currentThread().getName()); try { while (selector.select() > 0){ // 7、获取选择器中的所有注册的通道中已经就绪好的事件 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); // 8、开始遍历这些准备好的事件 while (it.hasNext()){ // 提取当前这个事件 SelectionKey sk = it.next(); // 9、判断这个事件具体是什么 if(sk.isAcceptable()){ // 10、直接获取当前接入的客户端通道 SocketChannel schannel = ssChannel.accept(); // 11 、切换成非阻塞模式 schannel.configureBlocking(false); // 12、将本客户端通道注册到选择器 System.out.println(schannel.getRemoteAddress() + " 上线 "); schannel.register(selector , SelectionKey.OP_READ); //提示 }else if(sk.isReadable()){ //处理读 (专门写方法..) readData(sk); } it.remove(); // 处理完毕之后需要移除当前事件 } } }catch (Exception e) { e.printStackTrace(); }finally { //发生异常处理.... } } //读取客户端消息 private void readData(SelectionKey key) { //获取关联的channel SocketChannel channel = null; try { //得到channel channel = (SocketChannel) key.channel(); //创建buffer ByteBuffer buffer = ByteBuffer.allocate(1024); int count = channel.read(buffer); //根据count的值做处理 if(count > 0) { //把缓存区的数据转成字符串 String msg = new String(buffer.array()); //输出该消息 System.out.println("来自客户端---> " + msg); //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理 sendInfoToOtherClients(msg, channel); } }catch (IOException e) { try { System.out.println(channel.getRemoteAddress() + " 离线了.."); e.printStackTrace(); //取消注册 key.cancel(); //关闭通道 channel.close(); }catch (IOException e2) { e2.printStackTrace();; } } } //转发消息给其它客户(通道) private void sendInfoToOtherClients(String msg, SocketChannel self ) throws IOException{ System.out.println("服务器转发消息中..."); System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName()); //遍历 所有注册到selector 上的 SocketChannel,并排除 self for(SelectionKey key: selector.keys()) { //通过 key 取出对应的 SocketChannel Channel targetChannel = key.channel(); //排除自己 if(targetChannel instanceof SocketChannel && targetChannel != self) { //转型 SocketChannel dest = (SocketChannel)targetChannel; //将msg 存储到buffer ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); //将buffer 的数据写入 通道 dest.write(buffer); } } } public static void main(String[] args) { //创建服务器对象 Server groupChatServer = new Server(); groupChatServer.listen(); } }
客户端
package nio.chat; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Scanner; public class Client { //定义相关的属性 private final String HOST = "127.0.0.1"; // 服务器的ip private final int PORT = 9999; //服务器端口 private Selector selector; private SocketChannel socketChannel; private String username; //构造器, 完成初始化工作 public Client() throws IOException { selector = Selector.open(); //连接服务器 socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT)); //设置非阻塞 socketChannel.configureBlocking(false); //将channel 注册到selector socketChannel.register(selector, SelectionKey.OP_READ); //得到username username = socketChannel.getLocalAddress().toString().substring(1); System.out.println(username + " is ok..."); } //向服务器发送消息 public void sendInfo(String info) { info = username + " 说:" + info; try { socketChannel.write(ByteBuffer.wrap(info.getBytes())); }catch (IOException e) { e.printStackTrace(); } } //读取从服务器端回复的消息 public void readInfo() { try { int readChannels = selector.select(); if(readChannels > 0) {//有可以用的通道 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if(key.isReadable()) { //得到相关的通道 SocketChannel sc = (SocketChannel) key.channel(); //得到一个Buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //读取 sc.read(buffer); //把读到的缓冲区的数据转成字符串 String msg = new String(buffer.array()); System.out.println(msg.trim()); } } iterator.remove(); //删除当前的selectionKey, 防止重复操作 } else { //System.out.println("没有可以用的通道..."); } }catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { //启动我们客户端 Client chatClient = new Client(); //启动一个线程, 每个3秒,读取从服务器发送数据 new Thread() { public void run() { while (true) { chatClient.readInfo(); try { Thread.currentThread().sleep(3000); }catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); //发送数据给服务器端 Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String s = scanner.nextLine(); chatClient.sendInfo(s); } } }
AIO 深入剖析
1.AIO编程
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
AIO
异步非阻塞,基于NIO的,可以称之为NIO2.0
BIO NIO AIO
Socket SocketChannel AsynchronousSocketChannel
ServerSocket ServerSocketChannel AsynchronousServerSocketChannel
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可, 这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
总结
BIO、NIO、AIO:
- Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
- Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
BIO、NIO、AIO适用场景分析:
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。Netty!