BIO NIO AIO
1.同步阻塞的BIO
1.1 BIO介绍
BIO是最传统的网络通信模式,通信模式是首先服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,此时就进入了同步阻塞模式的通信,客户端Socket发送一个请求,服务端Socket处理后进行响应,响应必须是处理完以后,在这之前任何事情都干不了,是一个阻塞的过程。通常服务端Socket需要对每个请求建立一个线程来服务这个客户端,服务端启用了多个线程会消耗系统的大量资源。
BIO的应用场景
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
BIO通信模式图:
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 基于BIO模式下的通信,客户端 - 服务端是完全同步,完全耦合的。
1.2 BIO通信案例
1.简单通信
功能要求:客户端发送一个消息,服务端接收一个消息。
public class Server {
public static void main(String[] args) throws Exception{
//注册端口
ServerSocket serverSocket = new ServerSocket(8888);
//开始等待接收客户端的连接,得到一个端到端的Socket管道
Socket socket = serverSocket.accept();
// 从Socket管道中得到一个字节输入流。把字节输入流包装成自己需要的流进行数据的读取。
BufferedInputStream bis=new BufferedInputStream(socket.getInputStream());
byte[] bytes=new byte[1024];
int length=0;
while((length= bis.read(bytes))>0){
System.out.println(new String(bytes,0, length));
}
//关闭流
socket.close();
serverSocket.close();
bis.close();
}
}
public class Client {
public static void main(String[] args) {
Socket socket=null;
BufferedOutputStream bos=null;
try {
//创建一个Socket的通信管道,请求与服务端的端口连接。
socket=new Socket(InetAddress.getLocalHost(),8888);
//从Socket管道中得到一个字节输出流。
// 把字节流改装成自己需要的流进行数据的发送
bos=new BufferedOutputStream(socket.getOutputStream());
bos.write("hello world".getBytes());
bos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
bos.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2.多个客户端通信
在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:
public class Server {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(9999);
while (true){
//循环监听客户端的请求
Socket socket = serverSocket.accept();
//收到一个客户端的请求则开启一个线程为其服务
new ServerSocketThread(socket).start();
}
}
}
//服务线程用于处理客户端发来的消息
public class ServerSocketThread extends Thread{
private Socket socket;
public ServerSocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader=null;
try {
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg;
while ((msg=reader.readLine())!=null){
System.out.println("客户端说:"+msg);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class Client {
public static void main(String[] args) throws Exception{
Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
PrintStream ps=new PrintStream(socket.getOutputStream());
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("请说:");
String msg=scanner.nextLine();
ps.println(msg);
ps.flush();
}
}
}
缺点分析
- 1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
- 2.每个线程都会占用栈空间和CPU资源;
- 3.并不是每个socket都进行IO操作,无意义的线程处理;
- 4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
3.伪异步I/O通信
在多用户客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。图示如下:
public class Server {
public static void main(String[] args) {
try {
// 1、注册端口
ServerSocket ss = new ServerSocket(9999);
// 2、定义一个循环接收客户端的Socket链接请求
// 初始化一个线程池对象
HandlerSocketServerPool pool = new HandlerSocketServerPool(3,10);
while(true){
Socket socket = ss.accept();
// 3、把socket对象交给一个线程池进行处理,
// 把socket封装成一个任务对象交给线程池处理
Runnable target = new ServerRunnableTarget(socket);
pool.execute(target);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public class HandlerSocketServerPool {
// 1、创建一个线程池的成员变量用于存储一个线程池对象
private ExecutorService executorService;
/**
* 2、创建这个类的对象的时候就需要初始化线程池对象
* public ThreadPoolExecutor(int corePoolSize,
* int maximumPoolSize,
* long keepAliveTime,
* TimeUnit unit,
* BlockingQueue<Runnable> workQueue)
*/
public HandlerSocketServerPool(int maxThreadNum , int queueSize){
executorService = new ThreadPoolExecutor(3,maxThreadNum,120
, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
}
/**
* 3、提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
*/
public void execute(Runnable target){
executorService.execute(target);
}
}
public class ServerRunnableTarget implements Runnable{
private Socket socket;
public ServerRunnableTarget(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 1、从socket管道中得到一个字节输入流对象
InputStream is = socket.getInputStream();
// 2、把字节输入流包装成一个缓冲字符输入流
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("服务端接收到:"+ msg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Client {
public static void main(String[] args) {
try {
// 1、请求与服务端的Socket对象链接
Socket socket = new Socket("127.0.0.1" , 9999);
// 2、得到一个打印流
PrintStream ps = new PrintStream(socket.getOutputStream());
// 3、使用循环不断的发送消息给服务端接收
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();
}
}
}
4.BIO模式下端口转发思想
需求分析:要求实现客户端与客户端之间交流,或者一个客户端可以把消息转发给除自己之外的其他在线客户端。这个时候我们就需要有端口转发的思想,一个客户端把消息发送给服务端,服务端再把消息转发给另一个客户端或者其他所有的在线客户端。
public class Server {
//用于管理在线socket
public static ArrayList<Socket> list=new ArrayList<>();
public static void main(String[] args) throws Exception{
ServerSocket serverSocket = new ServerSocket(8989);
while (true){
Socket socket = serverSocket.accept();
//放入集合管理
list.add(socket);
//创建线程与客户端通信
new ServerThread(socket).start();
}
}
}
与客户端通信的线程
public class ServerThread extends Thread{
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//循环一直与客户端通信,有消息就读
while (true){
try {
DataInputStream dis=new DataInputStream(socket.getInputStream());
String msg = dis.readUTF();
System.out.println("客户端发来的消息:"+msg);
//将客户端的消息发送给其他客户端
sendToAll(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
//用于将客户端的消息发送给其他客户端
public void sendToAll(String msg){
ArrayList<Socket> list = Server.list;
for (Socket sk : list) {
try {
DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
dos.writeUTF(msg);
} catch (IOException e) {
//出异常了说明当前线程出问题了
System.out.println("有客户端下线");
Server.list.remove(socket);
e.printStackTrace();
}
}
}
}
BIO的缺点:
服务端每增加一个连接便要开启一个线程与客户端通信当面对几万甚至成千上百的请求时,该模式就显得效率低下,会导致服务器负载过高,最后崩溃等。服务端的许多线程不仅难管理而且耗费系统的资源,如果这个连接不做任何事情会造成不必要的线程开销,虽然可以通过线程池机制改善(实现多个客户连接服务器),但是线程池的做法依旧不能好好的改善。因此我们需要更加高效的网络通信模式也就是NIO模型。
2.同步非阻塞的NIO
2.1 NIO的介绍
为什么要有NIO,NIO能解决什么问题?
我们知道传统的BIO网络通信模式服务端每增加一个连接便要开启一个线程与客户端通信当面对几万甚至成千上百的请求时,该模式就显得效率低下,会导致服务器负载过高,最后崩溃等。服务端的许多线程不仅难管理而且耗费系统的资源,如果这个连接不做任何事情会造成不必要的线程开销,虽然可以通过线程池机制改善(实现多个客户连接服务器),但是线程池的做法依旧不能好好的改善。因此我们需要更加高效的网络通信模式也就是NIO模型。
NIO采用的是一种多路复用的机制,利用单线程轮询事件,高效定位就绪的Channel来决定做什么,只是Select阶段是阻塞式的,能有效避免大量连接数时,频繁线程的切换带来的性能或各种问题。
NIO到底是什么
Java NIO(New IO)也有人称之为 java non-blocking IO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
什么是NIO 的非阻塞模式
Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配20 或者 80个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个。
NIO使用场景
1 有很大的数据需要存储,它的生命周期又很长
2 适合频繁的IO操作,比如网络并发场景
NIO和BIO之间的比较
- BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
- BIO 是阻塞的,NIO 则是非阻塞的
- BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道 读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO | BIO |
---|---|
Channel(通道) 和 面向缓冲区(Buffer) | 面向流(Stream) |
非阻塞(Non Blocking IO) | 阻塞IO(Blocking IO) |
选择器(Selectors) |
NIO 三大核心
Buffer( 缓冲区) :缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理。
Channel(通道):Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。 通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
Selector( 选择器):Selector是 一个Java NIO组件,可以能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率。
Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。如图所示:
- 每个 channel 都会对应一个 Buffer
- 一个线程对应Selector , 一个Selector对应多个 channel(连接)
- 程序切换到哪个 channel 是由事件决定的
- Selector 会根据不同的事件,在各个通道上切换
- Buffer 就是一个内存块 , 底层是一个数组
- 数据的读取写入是通过 Buffer完成的 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写。
- Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,需要获取 用于连接 IO 设备的通道以及用于容纳数据的缓冲 区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据
2.2 NIO核心一Buffer缓冲区
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理。
Buffer的子类继承关系如下:
其中用的最多的是ByteBuffer和CharBuffer。
缓冲区的基本属性:
capacity容量:是指缓冲区可以具体存储多少个数据.容量在创建Buffer缓冲区时指定大小不能为负数,且创建后不能再修改如果缓冲区满了,需要清空后才能继续写数据。
position表示当前位置:即缓冲区写入读取的位置.刚刚创建Buffer对象后,positin初始化为0,写入一个数据,position就向后移动一个单元,它的最大值是capacity-1.当Buffer从写模式切换到读模式position会被重置为0.从Buffer的开始位置读取数据,每读-一个数据postion.就向后移动一个单元。
limit上限:表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。
mark标记:设置一个标记位置,可以调用mark0方法,把标记就设置在position位置,当调用reset)方法时,就把postion设置为mark标记的位置。
标记、位置、限制、容量遵守以下不变式:
0<= mark <= position <= limit capacity
图示:
常用API
方法名 | 功能 |
---|---|
array() | array() 返回支持此缓冲区的数组 (可选操作) 。 |
capacity() | 返回此缓冲区的容量。 |
clear() | 清除此缓冲区。 |
flip() | 翻转这个缓冲区。 |
hasArray() | 告诉这个缓冲区是否由可访问的数组支持。 |
hasRemaining() | 告诉当前位置和极限之间是否存在任何元素。 |
isDirect() | 告诉这个缓冲区是否为 direct 。 |
isReadOnly() | 告知这个缓冲区是否是只读的。 |
limit() | 返回此缓冲区的限制。 |
limit(int newLimit) | 设置此缓冲区的限制。 |
position() | 返回此缓冲区的位置。 |
position(int newPosition) | 设置这个缓冲区的位置。 |
mark() | 将此缓冲区的标记设置在其位置。 |
remaining() | 返回当前位置和限制之间的元素数。 |
reset() | 将此缓冲区的位置重置为先前标记的位置。 |
rewind() | 倒带这个缓冲区。 |
get(…) | 读方法 |
allocate(int capacity) | 分配一个缓冲区 |
put() | 写方法 |
compact() | 压缩此缓冲区 |
wrap(…) | 将一个基本类型数组包装成对应的缓冲区 |
slice() | 缓冲区的分割[position,limit] |
duplicate() | 缓冲区的复制 |
演示BufferAPI的使用
使用Buffer读写数据一般遵循以下四个步骤:
- 写入数据到Buffer
- 调用flip()方法,转换为读取模式
- 从Buffer中读取数据
- 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区
public class Demo1 {
public static void main(String[] args) {
//创建一个charBuffer缓冲区对象
CharBuffer buffer= CharBuffer.allocate(10);
//10 10 0
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//向缓冲区存数据
buffer.put('A');
buffer.put('B');
buffer.put('C');
buffer.put('D');
buffer.put('E');
buffer.put('F');
// 10 10 6
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//切换为读模式 10 6 0
buffer.flip();
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//调用get读取数据 10 6 1
buffer.get();
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//再次存储数据把数据存储在position位置处 10 6 2
buffer.put("G");
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//设置标记 把当前position位置当做标记
buffer.mark();
//10 6 3
System.out.println(buffer.get());
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//position重置为mark位置 10 6 2
buffer.reset();
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//压缩 把buffer中没有读取的复制到position位置为0处 10 10 4
buffer.compact();
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
//clear 缓冲区的数据依旧存在 10 10 0
buffer.clear();
System.out.println("capacity:"+ buffer.capacity()+",limit:"+ buffer.limit()+",position:"+buffer.position());
// CDEFEF
while (buffer.hasRemaining()){
System.out.print(buffer.get());
}
}
}
测试buffer缓冲区的多数据操作
public class Demo2 {
public static void main(String[] args) {
CharBuffer buffer=CharBuffer.allocate(16);
String content="我爱java我爱编程,努力奋斗!";
buffer.put(content);
//切换为读模式
buffer.flip();
System.out.println(buffer);
//定义一个字符数组
char[] chars=new char[12];
//get 方法把缓冲区的数据读到数组中
//批量传输的大小是固定的没有指定大小则会把数组填充满
CharBuffer charBuffer = buffer.get(chars);
System.out.println(Arrays.toString(chars));
//出现异常 当缓冲区的数据不足以填满整个数组,会抛出异常
//buffer.get(chars);
//把小缓冲区的数据填充大的数组中要指定大小
buffer.get(chars,0,buffer.remaining());
System.out.println(Arrays.toString(chars));
//循环遍历
buffer.clear();
while (buffer.hasRemaining()) {
int len=Math.min(chars.length,buffer.remaining());
buffer.get(chars,0,len);
System.out.print(new String(chars,0,len));
}
System.out.println();
//批量写入
char[] contents={'a','b','c'};
}
}
缓冲区的创建方式
public class Demo3 {
public static void main(String[] args) {
//1.分配操作创建缓冲区
ByteBuffer.allocate(1024);
//2.使用包装操作创建缓冲区
char[] chars = new char[1024];
//把已经存在的数组包装成为一个Buffer对象
CharBuffer buffer = CharBuffer.wrap(chars);
//数组和缓冲区的改变都会造成相互影响
buffer.put("hello world");
buffer.flip();
System.out.println(buffer);
System.out.println(new String(chars));
//chars[0]='W';
//hasArray方法会返回是否有一个备份数组 通过array()返回备份数组的引用
}
}
缓冲区的复制与分离
public class Demo4 {
public static void main(String[] args) {
CharBuffer buffer = CharBuffer.allocate(16);
buffer.put("hello");
//缓冲区的复制
CharBuffer buffer2 = buffer.duplicate();
//buffer2与buffer实际引用的是同一个数组
buffer2.clear();
buffer2.put("heel,world");
buffer.flip();
//把limit设置成为position的值
System.out.println(buffer);//heel,
//分割缓冲区,slice方法根据[position,limit)区间创建一个新的缓冲区
CharBuffer buffer1 = buffer.slice();
System.out.println(buffer1.length());
}
}
直接与非直接缓冲区
什么是直接内存与非直接内存?
根据官方文档的描述:
byte bufffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。
从数据流的角度,非直接内存是下面这样的作用链:
本地IO–>直接内存–>非直接内存–>直接内存–>本地IO
而直接内存是:
本地IO–>直接内存–>本地IO
很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用ByteBuffer.allocateDirect()
方法创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect()
方法来确定。
2.3 NIO核心二Channel管道
通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。
- NIO 的通道类似于流,但有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲:
- BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel) 是双向的,可以读操作,也可以写操作。
- Channel 在 NIO 中是一个接口
java public interface Channel extends Closeable{}
Channel是一种新的I/O的访问方式,用于在字节缓冲区与通道另一侧的实体(可以是文件也可以是Socket)之间进行传输数据Channel可以双向读写数据数据,也可以实现异步读写程序不能直接访问Channel, Channel只能与Buffer缓冲区进行交互即把通道中的数据读到Buffer缓冲区中,程序从缓冲区中读取数据;在写操作时, 程序把数据写入Buffer缓冲区中,再把缓冲区的数据写入到Channel中。
Channel的类关系如下图:
FileChannel:用于读取,写入,映射和操作文件的通道。 获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
获取通道的其他方式是使用 Files 类的静态方法
newByteChannel() 获取字节通道。
或者通过通道的静态方法 open() 打开并返回指定通道
FileChannel API
方法名 | 功能 |
---|---|
map(FileChannel.MapMode mode, long position, long size) | 将此频道文件的区域直接映射到内存中。 |
open(Path path, OpenOption… options) | 打开或创建文件,返回文件通道以访问该文件。 |
position() | 返回此通道的文件位置。 |
position(long newPosition) | 设置此通道的文件位置。 |
read(ByteBuffer dst) | 从该通道读取到给定缓冲区的字节序列。 |
read(ByteBuffer[] dsts) | 从该通道读取到给定缓冲区的字节序列。 |
read(ByteBuffer[] dsts, int offset, int length) | 从该通道读取字节序列到给定缓冲区的子序列中。 |
size() | 返回此通道文件的当前大小。 |
transferFrom(ReadableByteChannel src, long position, long count) | 从给定的可读字节通道将字节传输到该通道的文件中。 |
transferTo(long position, long count, WritableByteChannel target) | 将该通道文件的字节传输到给定的可写字节通道。 |
write(ByteBuffer src) | 从给定的缓冲区向该通道写入一个字节序列。 |
write(ByteBuffer[] srcs) | 从给定的缓冲区向该通道写入一系列字节。 |
write(ByteBuffer[] srcs, int offset, int length) | 从给定缓冲区的子序列将一个字节序列写入该通道。 |
write(ByteBuffer src, long position) | 从给定的缓冲区向给定的文件位置开始,向该通道写入一个字节序列。 |
FileChanel读取和写出文件
//从磁盘读取文件内容:
FileChannel In = new FileInputStream("e:\\data.txt").getChannel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
In.read(buffer);
//切换为读模式
buffer.flip();
System.out.println(new String(buffer.array()));
//将内容写到磁盘中
FileChannel Out = new FileOutputStream("e:\\test.txt").getChannel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
buffer.put("hello world".getBytes());
buffer.flip();
Out.write(buffer);
Out.close();
FileChanel操作文件
public class Demo6 {
public static void main(String[] args) {
//把文件中的所有数据映射到虚拟内存中,访问效率高
File file = new File("e:\\data.txt");
//获得channel
try(
//读取文件内容
FileChannel channelIn = new FileInputStream(file).getChannel();
//保存到文件中
FileChannel channelOut = new FileOutputStream("e:\\out.txt").getChannel();
) {
//将inchannel中的数据映射到内存中,直接字节缓冲器
MappedByteBuffer buffer = channelIn.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
//把缓冲区的数据输出到outchannel
channelOut.write(buffer);
//也可以把buffer中的数据打印
buffer.flip();
Charset charset=Charset.defaultCharset();
CharBuffer decode = charset.decode(buffer);
System.out.println(decode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
FileChannel双向传输
public class Demo7 {
public static void main(String[] args) {
File file = new File("e:\\data.txt");
try(RandomAccessFile rw=new RandomAccessFile(file,"rw");
FileChannel channel = rw.getChannel()
){
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
//设置channel的position位置
channel.position(file.length());
//
channel.write(buffer);
}catch (Exception e){
e.printStackTrace();
}
}
}
Channel到Channel的传输 只有FileChannel支持
public class Demo9 {
public static void main(String[] args) {
File file = new File("e:\\data.txt");
try(RandomAccessFile rw=new RandomAccessFile(file,"rw");
FileChannel inChannel = rw.getChannel();
FileChannel outChannel= new FileOutputStream("").getChannel();
){
//把inchannle中的所有字节传输到outChannel
inChannel.transferTo(0,file.length(),outChannel);
//outChannel.transferFrom(inChannel,0,file.length());
}catch (Exception e){
e.printStackTrace();
}
}
}
分散 (Scatter) 和聚集 (Gather)
分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去
从channel通道中读取数据到Buffer1中Buffer1满后读到Buffer2,Buffer2满后读取到Buffer3
聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel。
Buffer1,Buffer2,Buffer3依次把数据写到Channel管道中。
@Test
public void test() throws IOException{
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
//1. 获取通道
FileChannel channel1 = raf1.getChannel();
//2. 分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3. 分散读取
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("-----------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
//4. 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
}
//把文件的属性与内容提供gather写到另一个文件中
public class Demo10 {
public static void main(String[] args) throws Exception{
File file = new File("e\\data.txt");
String path = file.getAbsolutePath();
Date date = new Date(file.lastModified());
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String headerTxt="filename:"+path+"\n"+"lastModified"+format.format(date)+"\n";
//把文件属性存储到缓冲区中
ByteBuffer allocate = ByteBuffer.wrap(headerTxt.getBytes());
ByteBuffer buffer = ByteBuffer.allocate(128);
//创建一个缓冲区数组
ByteBuffer[] gather={allocate,buffer,null};
String contentType="unkonow";
long contentLength=0;
FileInputStream inputStream = new FileInputStream(file);
FileChannel channel = inputStream.getChannel();
MappedByteBuffer byteBuffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
//把文件内容的缓冲区保存到gather中
gather[2]=byteBuffer;
contentLength=channel.size();
contentType= URLConnection.guessContentTypeFromName(file.getName());
String str=contentType+contentLength;
buffer.put(str.getBytes());
buffer.flip();
//把gather数组中的内容写到目标文件中
FileChannel channel1 = new FileOutputStream("").getChannel();
while (channel1.write(gather)>0){
}
channel1.close();
}
}
ServerSocketChannel:用于面向流的侦听套接字的可选通道。 类似ServerSocket
方法名 | 功能 |
---|---|
accept() | 接受与此频道套接字的连接。 |
bind(SocketAddress local) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接。 |
bind(SocketAddress local, int backlog) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接。 |
getLocalAddress() | 返回此通道的套接字所绑定的套接字地址。 |
open() | 打开服务器套接字通道。 |
socket() | 检索与此通道关联的服务器套接字。 |
validOps() | 返回确定此频道支持的操作的操作集。 |
SocketChannel:用于面向流的连接套接字的可选通道。 类似Socket
方法名 | 功能 |
---|---|
bind(SocketAddress local) | 将通道的套接字绑定到本地地址。 |
connect(SocketAddress remote) | 连接此通道的套接字。 |
getLocalAddress() | 返回此通道的套接字所绑定的套接字地址。 |
getRemoteAddress() | 返回此通道的套接字所连接的远程地址。 |
open() | 打开套接字通道。 |
open(SocketAddress remote) | 打开套接字通道并将其连接到远程地址。 |
read(ByteBuffer dst) | 从该通道读取到给定缓冲区的字节序列。 |
read(ByteBuffer[] dsts) | 从该通道读取到给定缓冲区的字节序列。 |
shutdownInput() | 关闭连接进行阅读,不关闭频道。 |
shutdownOutput() | 关闭连接以进行写入,而不关闭通道。 |
write(ByteBuffer src) | 从给定的缓冲区向该通道写入一个字节序列。 |
write(ByteBuffer[] srcs) | 从给定的缓冲区向该通道写入一系列字节。 |
基于NIO的网络通信
public class ClientSocketChannel {
public static void main(String[] args) throws Exception{
SocketChannel socketChannel=SocketChannel.open();
socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(),9999));
while (!socketChannel.finishConnect()){
System.out.println("等待连接");
}
//向服务器发送消息
ByteBuffer byteBuffer=ByteBuffer.allocate(128);
byteBuffer.put("hello server".getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
ReadableByteChannel newChannel = Channels.newChannel(socketChannel.socket().getInputStream());
byteBuffer.clear();
newChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().decode(byteBuffer).toString());
}
}
public class ServerSocketChannel {
public static void main(String[] args) throws Exception{
java.nio.channels.ServerSocketChannel socketChannel = java.nio.channels.ServerSocketChannel.open();
ServerSocket socket = socketChannel.socket();
socket.bind(new InetSocketAddress(9999));
socketChannel.configureBlocking(false);
System.out.println("服务端准备就绪");
while (true){
SocketChannel channel = socketChannel.accept();
if(channel==null){
Thread.sleep(500);
}else {
//有客户端来
//给他发送问候。并读取客户端发来的内容
ByteBuffer buffer=ByteBuffer.allocate(128);
buffer.put("hello".getBytes());
buffer.flip();
channel.write(buffer);
System.out.println("from client address"+channel.getRemoteAddress());
buffer.clear();
channel.read(buffer);
buffer.flip();
CharBuffer charBuffer = Charset.defaultCharset().decode(buffer);
System.out.println(charBuffer);
channel.close();
}
}
}
}
DatagramChannel:面向数据报的套接字的可选通道。
方法名 | 功能 |
---|---|
bind(SocketAddress local) | 将通道的套接字绑定到本地地址。 |
connect(SocketAddress remote) | 连接此通道的套接字。 |
disconnect() | 断开此通道的套接字。 |
getLocalAddress() | 返回此通道的套接字所绑定的套接字地址。 |
getRemoteAddress() | 返回此通道的套接字所连接的远程地址。 |
open() | 打开数据报通道。 |
read(ByteBuffer dst) | 从此频道读取数据报。 |
receive(ByteBuffer dst) | 通过该频道接收数据报。 |
send(ByteBuffer src, SocketAddress target) | 通过此频道发送数据报。 |
write(ByteBuffer src) | 将数据报写入此通道。 |
write(ByteBuffer[] srcs) | 将数据报写入此通道。 |
write(ByteBuffer[] srcs, int offset, int length) | 将数据报写入此通道。 |
基于DatagramChannel的通信
public class DataGramReceive {
public static void main(String[] args) throws Exception{
DatagramChannel datagramChannel=DatagramChannel.open();
datagramChannel.bind(new InetSocketAddress(8888));
datagramChannel.configureBlocking(false);
Scanner scanner = new Scanner(System.in);
ByteBuffer buffer=ByteBuffer.allocate(1024);
while (true){
//接收数据
InetSocketAddress socketAddress = (InetSocketAddress) datagramChannel.receive(buffer);
//判断是否有数据
if(socketAddress==null){
Thread.sleep(1000);
continue;
}
System.out.println("data form:"+socketAddress);
buffer.flip();
String msg=new String(buffer.array(),0,buffer.limit());
System.out.println(":"+socketAddress.getPort()+"--->"+msg);
//发送数据
String line = scanner.nextLine();
datagramChannel.send(ByteBuffer.wrap(line.getBytes()),socketAddress);
}
}
}
public class DataGramSend {
public static void main(String[] args) throws Exception{
DatagramChannel datagramChannel=DatagramChannel.open();
// datagramChannel.connect(new InetSocketAddress(8888));
datagramChannel.configureBlocking(false);
Scanner scanner = new Scanner(System.in);
ByteBuffer buffer=ByteBuffer.allocate(1024);
while (scanner.hasNext()){
String text = scanner.nextLine();
buffer.clear();
buffer.put(text.getBytes());
buffer.flip();
//发送数据
datagramChannel.send(buffer,new InetSocketAddress("localhost",8888));
//接收数据
SocketAddress receive = datagramChannel.receive(buffer);
while (receive == null) {
receive = datagramChannel.receive(buffer);
}
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
}
}
}
pipe通道:用于两个线程之间通信数据传输
public class TestPipe {
public static void main(String[] args) throws Exception{
PipedOutputStream pipedOutputStream = new PipedOutputStream();
PipedInputStream pipedInputStream=new PipedInputStream();
pipedInputStream.connect(pipedOutputStream);
new Thread(new SenderThread(pipedOutputStream)).start();
new Thread(new ReceiverThread(pipedInputStream)).start();
}
}
class SenderThread implements Runnable{
PipedOutputStream outputStream;
public SenderThread(PipedOutputStream outputStream) {
this.outputStream = outputStream;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
String text="hello,receiver:"+i+"\r\n";
outputStream.write(text.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class ReceiverThread implements Runnable{
PipedInputStream inputStream;
public ReceiverThread(PipedInputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public void run() {
byte[] bytes = new byte[1024];
try {
int len=inputStream.read(bytes);
while (len>0){
System.out.println(new String(bytes,0,len));
len=inputStream.read(bytes);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.4 NIO核心三Selector选择器
选择器(Selector):
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管 理多个通道,也就是管理多个连接和请求。
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都 创建一个线程,不用去维护多个线程避免了多线程之间的上下文切换导致的开销。
Selector三大常用类介绍
SelectableChannel:可通过Selector复用的通道 。
可以通过调用isRegistered方法来确定信道是否被注册到一个或多个选择器。此方法返回一个新的SelectionKey对象,表示通道与选择器的注册。
一旦注册了选择器,通道保持注册,直到它被注销 。 这包括通过选择器取消分配给频道的任何资源。
通道不能直接注销; 而是代表其注册的密钥必须被取消 。 取消密钥请求在选择器的下一个选择操作期间注销通道。 可以通过调用其cancel方法来明确地取消密钥。 无论通过调用其close方法还是通过中断通道上的I / O操作中阻塞的线程,通道关闭时,所有通道的密钥都将被隐式取消。
如果选择器本身被关闭,那么该通道将被注销,并且表示其注册的密钥将被无效,而不再延迟。 任何特定选择器最多可以注册一个通道。
SelectableChannel API
方法名 | 功能 |
---|---|
blockingLock() | 检索 configureBlocking和 register方法同步的对象。 |
configureBlocking(boolean block) | 调整此频道的屏蔽模式。 |
isBlocking() | 告诉这个通道上的每个I / O操作是否会阻塞直到完成。 |
isRegistered() | 告知这个频道当前是否在任何选择器上注册。 |
keyFor(Selector sel) | 检索表示频道注册的键与给定的选择器。 |
register(Selector sel, int ops) | 使用给定的选择器注册此频道,返回一个选择键。 |
register(Selector sel, int ops, Object att) | 使用给定的选择器注册此频道,返回一个选择键。 |
validOps() | 返回一个 operation set标识该频道支持的操作。 |
Selector:SelectableChannel对象的多路复用器 。 可以通过调用此类的open方法来创建选择器,该方法将使用系统的默认值selector provider创建一个新的选择器。 还可以通过调用自定义选择器提供程序的openSelector方法来创建选择器。 选择器保持打开,直到通过其close方法关闭。 常用方法:
方法名 | 功能 |
---|---|
close() | 关闭此选择器。 |
isOpen() | 告诉这个选择器是否打开。 |
keys() | 返回此选择器的键集。 |
open() | 打开选择器。 |
select() | 选择一组其相应通道准备好进行I / O操作的键。 |
selectedKeys() | 返回此选择器的选择键集。 |
selectNow() | 选择一组其相应通道准备好进行I / O操作的键。 |
Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
SelectionKey:一个Key代表一个SelectableChannel与Selector的注册 。 每当通道被选择器注册时,都会创建一个选择键。 直到它被调用其取消一个关键保持有效cancel方法,通过关闭它的信道,或通过关闭它的选择器。 取消键不会立即将其从选择器中删除; 而是在下一个选择操作期间添加到选择器的cancelled-key set中以进行删除。 密钥的有效性可以通过调用其isValid方法来测试。 常用方法和属性:
static int OP_ACCEPT :操作集位用于套接字接受操作。
static int OP_CONNECT : 用于套接字连接操作的操作集位。
static int OP_READ : 读操作的操作位。
static int OP_WRITE :写操作的操作位。
方法名 | 功能 |
---|---|
attach(Object ob) | 将给定对象附加到此键。 |
cancel() | 要求取消该密钥的通道与其选择器的注册。 |
channel() | 返回创建此键的通道。 |
isConnectable() | 测试此密钥的通道是否已完成或未完成其套接字连接操作。 |
isAcceptable() | 测试此密钥的通道是否已准备好接受新的套接字连接。 |
isReadable() | 测试此密钥的频道是否可以阅读。 |
isValid() | 告知这个密钥是否有效。 |
isWritable() | 测试此密钥的通道是否准备好进行写入。 |
selector() | 返回创建此键的选择器。 |
选择器Selector的使用
1.创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
Selector selector = Selector.open();
2.向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
- 读 : SelectionKey.OP_READ (1)
- 写 : SelectionKey.OP_WRITE (4)
- 连接 : SelectionKey.OP_CONNECT (8)
- 接收 : SelectionKey.OP_ACCEPT (16)
- 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
2.5 NIO非阻塞式网络开发流程
服务端流程
-
1、当客户端连接服务端时,服务端会通过 ServerSocketChannel 得到 SocketChannel:获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
-
2、切换非阻塞模式
ssChannel.configureBlocking(false);
-
3、绑定连接
ssChannel.bind(new InetSocketAddress(9999));
-
4、 获取选择器
Selector selector = Selector.open();
-
5、 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
-
6、轮询式的获取选择器上已经“准备就绪”的事件
//轮询式的获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
System.out.println("轮一轮");
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
客户端流程
-
1、 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
-
2.、切换非阻塞模式
sChannel.configureBlocking(false);
-
3.、分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
-
4.、发送数据给服务端
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();
2.6 NIO非阻塞式网络通信案例
需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
/**
客户端
*/
public class Client {
public static void main(String[] args) throws Exception {
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
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();
}
//5. 关闭通道
sChannel.close();
}
}
/**
服务端
*/
public class Server {
public static void main(String[] args) throws IOException {
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9999));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
System.out.println("轮一轮");
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if (sk.isAcceptable()) {
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buf)) > 0) {
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
}
2.7BIO开发群聊系统
- 编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatServer() {
try {
//得到选择器
selector = Selector.open();
//ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该listenChannel 注册到selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
}catch (IOException e) {
e.printStackTrace();
}
}
//监听
public void listen() {
System.out.println("监听线程: " + Thread.currentThread().getName());
try {
//循环处理
while (true) {
int count = selector.select();
if(count > 0) {//有事件处理
//遍历得到selectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//取出selectionkey
SelectionKey key = iterator.next();
//监听到accept
if(key.isAcceptable()) {
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该 sc 注册到seletor
sc.register(selector, SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress() + " 上线 ");
}
if(key.isReadable()) { //通道发送read事件,即通道是可读的状态
//处理读 (专门写方法..)
readData(key);
}
//当前的key 删除,防止重复处理
iterator.remove();
}
} else {
System.out.println("等待....");
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//发生异常处理....
}
}
//读取客户端消息
private void readData(SelectionKey key) {
//取到关联的channle
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("form 客户端: " + msg);
//向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
sendInfoToOtherClients(msg, channel);
}
}catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + " 离线了..");
//取消注册
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) {
//创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1"; // 服务器的ip
private final int PORT = 6667; //服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器, 完成初始化工作
public GroupChatClient() 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 {
//启动我们客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程, 每个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);
}
}
}
3.异步非阻塞的AIO
3.1 AIO介绍
JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO 2.0,Java正式提供了异步文件I/O操作,同时提供了与UNIX网络编程事件驱动I/O对应的AIO。AIO是真正的异步非阻塞I/O。它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。NIO 2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。目前的AIO与NIO底层都使用了epoll(Linux中),所以二者性能都很好,主要差异在于同步与异步,NIO是同步的,始终只有一个线程在进行结果处理,而AIO的异步回调则是基于多线程的。
AIO不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写。什么意思呢?NIO采用轮询的方式,一直在轮询的询问stream中数据是否准备就绪,如果准备就绪发起处理。但是AIO就不需要了,AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO, 即:应用程序向操作系统注册IO监听,然后继续做自己的事情。操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数(这就是一种以订阅者模式进行的改造)。由于应用程序不是“轮询”方式而是订阅-通知方式,所以不再需要selector轮询,由channel通道直接到操作系统注册监听。
什么是epoll?
epoll是Linux中多路复用IO接口select/poll的增强版本,select/poll模型是忙轮询,即一直不停地轮询看哪些操作已经结束可以获取操作结果了,而epoll则是将已经结束的操作的操作结果放入队列中,然后只需要遍历处理队列中的操作就可以了,避免了CPU的浪费,提升程序运行效率。
3.2 AIO与NIO的区别
-
NIO是同步非阻塞I/O,AIO是异步非阻塞I/O,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
-
AIO与NIO的操作结果获取方式不同,NIO的操作结束后会将操作就绪的I/O放在队列中,由Selector依次循环获取处理;AIO操作结束后则会直接回调CompletionHandler的实现类的相应函数来进行处理;
-
处理操作结果时NIO是单线程,即由Selector依次在当前线程中进行处理,如果需要多线程处理需要自行实现,这也是为什么它是同步而非异步;而AIO在回调处理操作结果时,是多线程的,其底层设有线程池。
-
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可, 这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
3.3 AIO的相关API
java.nio.channels.AsynchronousFileChannel:
用于读取,写入和操作文件的异步通道。
当通过调用此类定义的open方法之一来打开文件时,将创建异步文件通道。 该文件包含可以读写的可变长度的字节序列,当前大小可以是queried 。 当字节写入超过其当前大小时,文件的大小会增加; 文件大小在truncated时会减小 。
异步文件通道在文件中没有当前位置 。 而是将文件位置指定给启动异步操作的每个读写方法。 A CompletionHandler被指定为参数,并被调用以消耗I / O操作的结果。 该类还定义了启动异步操作的读取和写入方法,返回一个Future以表示待处理的操作结果。 Future可用于检查操作是否完成,等待其完成,并检索结果。
方法名 | 功能 |
---|---|
force(boolean metaData) | 强制将此通道文件的任何更新写入包含该通道的存储设备。 |
lock() | 获取此通道文件的排他锁。 |
open(Path file, OpenOption… options) | 打开或创建用于读取和/或写入的文件,返回异步文件通道以访问该文件。 |
read(ByteBuffer dst, long position) | 从给定的文件位置开始,从该通道读取一个字节序列到给定的缓冲区。 |
size() | 返回此通道文件的当前大小。 |
truncate(long size) | 将此频道的文件截断为给定大小。 |
write(ByteBuffer src, long position) | 从给定的缓冲区向给定的文件位置开始,向该通道写入一个字节序列。 |
tryLock() | 尝试获取此频道文件的排他锁。 |
java.nio.channels.AsynchronousServerSocketChannel:ServerSocket的aio版本,创建TCP服务端,绑定地址,监听端口等。
方法名 | 功能 |
---|---|
accept() | 接受连接。 |
bind(SocketAddress local) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接。 |
bind(SocketAddress local, int backlog) | 将通道的套接字绑定到本地地址,并配置套接字以监听连接。 |
getLocalAddress() | 返回此通道的套接字所绑定的套接字地址。 |
open() | 打开异步服务器套接字通道。 |
open(AsynchronousChannelGroup group) | 打开异步服务器套接字通道。 |
provider() | 返回创建此通道的提供程序。 |
setOption(SocketOption name, T value) | 设置套接字选项的值。 |
java.nio.channels.AsynchronousSocketChannel:
面向流的异步socket channel,表示一个连接
方法名 | 功能 |
---|---|
bind(SocketAddress local) | 将通道的套接字绑定到本地地址。 |
connect(SocketAddress remote) | 连接此通道的套接字。 |
getLocalAddress() | 返回此通道的套接字所绑定的套接字地址。 |
getRemoteAddress() | 返回此通道的套接字所连接的远程地址。 |
open() | 打开异步套接字通道。 |
open(AsynchronousChannelGroup group) | 打开异步套接字通道。 |
read(ByteBuffer dst) | 从该通道读取到给定缓冲区的字节序列。 |
shutdownInput() | 关闭连接进行阅读,不关闭频道。 |
shutdownOutput() | 关闭连接以进行写入,而不关闭通道。 |
write(ByteBuffer src) | 从给定的缓冲区向该通道写入一个字节序列。 |
java.nio.channels.AsynchronousChannelGroup:
异步channel的分组管理,目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池,这个线程池执行两个任务:处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个 AsynchronousChannelGroup,那么通过AsynchronousServerSocketChannel创建的 AsynchronousSocketChannel将同属于一个组,共享资源。
方法名 | 功能 |
---|---|
awaitTermination(long timeout, TimeUnit unit) | 等待该组终止。 |
isShutdown() | 告知这个异步通道组是否关闭。 |
isTerminated() | 告诉本组是否已经终止。 |
provider() | 返回创建此通道组的提供程序。 |
shutdown() | 启动组的有序关闭。 |
shutdownNow() | 关闭组并关闭组中的所有打开的通道。 |
withCachedThreadPool(ExecutorService executor, int initialSize) | 创建具有给定线程池的异步通道组,根据需要创建新线程。 |
withFixedThreadPool(int nThreads, ThreadFactory threadFactory) | 创建具有固定线程池的异步通道组。 |
withThreadPool(ExecutorService executor) | 使用给定的线程池创建异步通道组。 |
3.4 AIO案例
AIO通信案例
public class SimpleTimeClient {
private String host;
private int port;
private CountDownLatch latch;
private AsynchronousSocketChannel channel;//异步socket通道
public static void main(String [] args){
while (true) {
new Thread(() -> {
try {
System.out.println("time client thread: " + Thread.currentThread());
SimpleTimeClient client = new SimpleTimeClient("localhost", 8088);
client.latch.await();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private SimpleTimeClient(String host, int port) throws IOException{
this.host = host;
this.port = port;
this.latch = new CountDownLatch(1);
initChannel();
}
private void initChannel() throws IOException
{
channel = AsynchronousSocketChannel.open();// 打开异步socket通道
// 异步连接指定地址,连接完成后会回调ConnectionCompletionHandler
//A attachment :AsynchronousSocketChannel的附件,用于回调通知时作为参数传递,调用者可以自定义。
//CompletionHandler<Void,? super A> handler 异步操作回调通知接口
channel.connect(new InetSocketAddress(host, port), null, new ConnectionCompletionHandler());
}
private class ConnectionCompletionHandler implements CompletionHandler<Void, Void> {
@Override
public void completed(Void result, Void attachment)
{
System.out.println("connection thread: " + Thread.currentThread());
String msg = "query time order";
ByteBuffer writeBuffer = ByteBuffer.allocate(msg.length());
writeBuffer.put(msg.getBytes(StandardCharsets.UTF_8)).flip();
// 异步写入发送数据,写入完成后会回调WriteCompletionHandler
channel.write(writeBuffer, writeBuffer, new WriteCompletionHandler());
}
@Override
public void failed(Throwable exc, Void attachment)
{
exc.printStackTrace();
latch.countDown();//异常时执行让线程执行完毕
}
}
//写数据完成回调处理类
private class WriteCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
@Override
public void completed(Integer result, ByteBuffer buffer){
System.out.println("write thread: " + Thread.currentThread());
if(buffer.hasRemaining())
channel.write(buffer, buffer, this);
else{
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//异步读取返回的数据,读取结束后会回调ReadCompletionHandler
channel.read(readBuffer, readBuffer, new CompletionHandler<Integer, ByteBuffer>(){
@Override
public void completed(Integer result, ByteBuffer buffer)
{
System.out.println("read thread: " + Thread.currentThread());
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String body;
body = new String(bytes, StandardCharsets.UTF_8);
System.out.println("now is " + body);
latch.countDown();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment)
{
try{
channel.close();
latch.countDown();
} catch (IOException e){
e.printStackTrace();
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment)
{
exc.printStackTrace();
latch.countDown();
}
}
}
服务端
public class SimpleTimeServer implements Runnable{
//维持服务线程的门闩
private CountDownLatch latch;
//异步socket服务通道
private AsynchronousServerSocketChannel asynchronousServerSocketChannel;
public static void main(String [] args){
try{
System.out.println("我是主线程: " + Thread.currentThread());
new SimpleTimeServer(8088).run();
System.out.println("监听线程已挂");
} catch (IOException e) {
e.printStackTrace();
}
}
private SimpleTimeServer(int port) throws IOException{
//开启异步socket服务
asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
//绑定端口
asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("simple time server start in " + port);
}
@Override
public void run()
{
latch = new CountDownLatch(1);//阻塞当前线程防止服务端任务执行完退出,
//在实际项目中,不需要启动独立的线程来处理asynchronousServerSocketChannel,这里仅仅是个demo。
System.out.println("我是监听线程:" + Thread.currentThread());
//异步socket服务接收请求,传递一个attach对象和实现了CompletionHandler的回调来处理AIO操作结果
asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
try{
latch.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
//接收请求的结束动作处理类,当异步socket服务接收到一个请求时,会回调此handler,从而对收到的请求进行处理
//AsynchronousSocketChannel为处理结果 SimpleTimeServer为发起调用时传入的参数
private class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, SimpleTimeServer>{
@Override
public void completed(AsynchronousSocketChannel channel, SimpleTimeServer attachment)
{
System.out.println("我是处理线程:" + Thread.currentThread());
//循环监听,进行监听操作的是SimpleTimeServer运行的线程,这样做的目的是因为一个asynchronousServerSocketChannel
//可以接收成千上万个客户端,所以当系统回调我们传入的CompletionHandler时,表示新的客户端已经接入成功,
//所以继续调用accept接受其他客户端 如果处理不过来回用新的线程来接收其他接入
attachment.asynchronousServerSocketChannel.accept(attachment, this);
ByteBuffer buffer = ByteBuffer.allocate(1024);
//ByteBuffer dst :接受缓冲区,用于从异步的channel中读取数据包
//A attachment :异步channel携带的附件,通知回调时作为参数传递使用。
//CompletionHandler<Integer,? super A> handler 接收通知回调的业务handleer
channel.read(buffer, buffer, new ReadCompletionHandler(channel));
}
@Override
public void failed(Throwable exc, SimpleTimeServer attachment)
{
//接收请求失败,打印异常信息,将门闩减一,服务线程终止
exc.printStackTrace();
attachment.latch.countDown();
}
//读取数据的结束动作处理类,当系统将数据读取到buffer中,会回调此handler
private class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
//将AsynchronousSocketChannel通过参数传递到ReadCompletionHandler中 当做成员变量用来读取包中消息和发送应答
private AsynchronousSocketChannel channel;
ReadCompletionHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
@Override
public void completed(Integer result, ByteBuffer attachment)
{
//首先对attachment进行flip操作,为后续从缓冲区读取数据做准备
//根据缓冲区的可读字节创建byte数组,然后通过newString方法创建请求消息,对请求消息进行判断
//如果是“query time order” 则获取当前系统的服务器时间,调用dowrite方法发送客户端。
attachment.flip();
byte[] body = new byte[result];//attachment.remaining()
attachment.get(body);
String req = new String(body, StandardCharsets.UTF_8);
System.out.println("the time server received order: " + req);
String currentTime = "query time order".equalsIgnoreCase(req) ? new Date(System.currentTimeMillis())
.toString() : "BAD ORDER";
doWrite(currentTime);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment)
{
try{//读取失败关闭通道
channel.close();
} catch (IOException e){
e.printStackTrace();
}
}
private void doWrite(String msg)
{
//将字符串解码为字节数组再调用AsynchronousSocketChannel的write方法
if(msg != null){
byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>(){
@Override
public void completed(Integer result, ByteBuffer attachment)
{
//如果没有发送完成就继续发送
if(attachment.hasRemaining())
channel.write(attachment, attachment, this);
}
@Override //可以对异常判断如果是I/O异常,就关闭链路,释放资源,如果是其他异常按照业务逻辑处理。
public void failed(Throwable exc, ByteBuffer attachment)
{
try{
channel.close();
} catch (IOException e){
e.printStackTrace();
}
}
});
}
}
}
}
}
AIO读取文件:AsynchronousFileChannel
第一种方式是调用返回值为Future的read()方法:这种方式中,read()接受一个ByteBuffer座位第一个参数,数据会被读取到ByteBuffer中。 第二个参数是开始读取数据的文件位置。read()方法会立刻返回,即使读操作没有完成。我们可以通过isDone()方法检查操作是否完成。
static void read1() throws IOException{
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("E:\\data.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
Future<Integer> operation = fileChannel.read(buffer, position);
while (!operation.isDone());
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
}
第二种通过CompletionHandler读取数据
static void read2() throws IOException
{
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("E:/ModMudEngine/mt/trunk/logic/gsever/test/ser.ser"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("result = " + result);
attachment.flip();
byte[] data = new byte[attachment.limit()];
attachment.get(data);
System.out.println(new String(data));
attachment.clear();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try
{
fileChannel.close();
} catch (IOException e){
e.printStackTrace();
}
}
});
}
通过Future写数据: 第二个参数是开始写入数据的文件位置
static void write() throws IOException{
Path path = Paths.get("E:/ModMudEngine/mt/trunk/logic/gsever/test/copy.txt");
AsynchronousFileChannel fileChannel =
AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
buffer.put("test data".getBytes());
buffer.flip();
Future<Integer> operation = fileChannel.write(buffer, position);
buffer.clear();
while(!operation.isDone());
System.out.println("Write done");
}
通过CompletionHandler写数据
static void write2() throws IOException{
Path path = Paths.get("E:/ModMudEngine/mt/trunk/logic/gsever/test/copy.txt");
if(!Files.exists(path)){
Files.createFile(path);
}
AsynchronousFileChannel fileChannel =
AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
buffer.put("test dataa".getBytes());
buffer.flip();
fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("bytes written: " + result);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("Write failed");
exc.printStackTrace();
try{
fileChannel.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
});
}
close时如果有未完成的操作或继续启动操作会抛出异常。
3.5 BIO,NIO,AIO对比总结
BIO、NIO、AIO:
- Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
- Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
BIO | NIO | AIO |
---|---|---|
Socket | SocketChannel | AsynchronousSocketChannel |
ServerSocket | ServerSocketChannel | AsynchronousServerSocketChannel |
BIO、NIO、AIO适用场景分析:
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
参考黑马程序员的BIO NIO AIO教程