前言
前面通过Socket实现了一个简单的聊天系统,且对Socket进行了一定的了解
Java网络编程(3)Socket实现一个简单的聊天系统
而前面的Socket都是通过IO实现的
现在来系统的了解IO与NIO
目录
- Java的IO演变
1.1. BIO
1.2. 伪异步IO
1.3. NIO
1.4. AIO - NIO结构
2.1. 缓冲区Buffer
2.2. 通道Channel
2.3. 多复用选择器Selector - 缓冲区操作
3.1. ByteBuffer - 通道Channel
4.1. 常用操作 - 缓冲区与通道:分散、聚集
5.1. 案例 - 选择器Selector
6.1. 常用方法
6.2. Selector的使用
6.3. Selector案例 - 总结
Java的IO演变
BIO
在jdk1.4之前,Java的Socket通信都是通过同步阻塞模式BIO(block-IO)
同步阻塞式模式在应用时性能和可靠性是非常差的
在前面的应用也可以看出:因为是阻塞式,一个线程只能实现一个通信,在高并发会消耗太多资源
一客户端一线程形式
伪异步IO
前面使用线程池完成多客户端连接服务器,就是这种伪异步IO
//创建线程池:限定最多50个线程
ExecutorService executor= Executors.newFixedThreadPool(50);
while (true) {
//接受连接,创建socket
Socket socket = serverSocket.accept();
System.out.println("IP地址:" + socket.getLocalAddress());
//线程
Runnable runnable=()->{
try {
BufferedReader reader=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
String str=null;
while ((str=reader.readLine())!=null){
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
};
executor.submit(runnable);
}
JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理,有一定的效率但本质上还是BIO
线程池的运用有一定的弊端:当网络传输慢会阻塞线程,阻塞的线程过多会影响线程池效率甚至崩溃
NIO
为了解决网络通信问题,jdk1.4推出了非阻塞模式NIO
NIO可以称New IO,也可以称Non-block IO
NIO在Java代码提供了高速的、面向块的IO,提供了很多API和类库
AIO
jdk1.7提供了异步非阻塞IO - AIO,支持文件的异步IO操作和针对网络套接字的异步操作等等
一步步来,慢慢理解
NIO结构
NIO:非阻塞式IO,可以称为New IO,或者Non-block IO
NIO是封装了IO,是IO的加强版
它与BIO不同在与通道Channel与缓冲区Buffer、多复用选择器Selector三个重要组件
缓冲区Buffer
缓冲区Buffer是一个对象,包含了一些要写入读出的数据
以前的IO是面向流,通过直接流读写数据
Java程序直接读出或写入流就可以通信了
当然现在的IO也有缓冲,例如BufferedReader等,这些都是NIO重新实现过了
在NIO库中,所有的数据都是缓冲区处理,缓冲区实质上是一个数组,常用的是字节数组ByteBuffer
所有的缓冲区类型都继承于抽象类Buffer,对于Java中的基本类型,都有一个具体Buffer类型与之相对应(除了Boolean类型)
通道Channel
在Java NIO中,通道是在实体和字节缓冲区之间有效传输数据的媒介
通道在实体与缓冲区之间,通过通道来读取、写入数据
通道的作用于流相似,但不同的是通道是双工的,可以同时进行读、写
和传统IO分为 File IO与Stream IO类似,NIO有两种类型的通道:文件通道(file)和套接字通道(socket)
多复用选择器Selector
多复用选择器Selector是NIO编程的重点
选择器用于使用单个线程处理多个通道,它会轮询注册在其上的通道,确定哪个通道准备好通信,通过SelectionKey获得就绪Channel的集合,然后进行IO操作
选择器只能管理非阻塞的通道
这就比伪异步IO的线程池方便多了,通过选择器单线程即可处理多个Channel
缓冲区操作
所有缓冲区类型继承抽象类Buffer,大部分缓冲区类型的操作都类似,仅学习一下最常用的ByteBuffer的操作(能与channel交互的只有ByteBuffer)
ByteBuffer
实例化:
Buffer、ByteBuffer等类都是抽象类
抽象类无法实例化
ByteBuffer提供了四个静态工厂方法得到ByteBuffer实例
这四个方法:
-
allocate(int capacity)
从堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器(HeapByteBuffer实例) -
allocateDirect(int capacity)
是不使用JVM堆栈而是通过操作系统来创建内存块用作缓冲区,它与当前操作系统能够更好的耦合,因此能进一步提高I/O操作速度。但是分配直接缓冲区的系统开销很大,因此只有在缓冲区较大并长期存在,或者需要经常重用时,才使用这种缓冲区 -
wrap(byte[] array)
这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方。其实ByteBuffer底层本来就有一个bytes数组负责来保存buffer缓冲区中的数据,通过allocate方法系统会帮你构造一个byte数组(本质也是HeapByteBuffer实例) -
wrap(byte[] array, int offset, int length)
在上一个方法的基础上可以指定偏移量和长度,这个offset也就是包装后byteBuffer的position,而length呢就是limit-position的大小,从而我们可以得到limit的位置为length+position(offset)
ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
ByteBuffer byteBuffer1=ByteBuffer.allocateDirect(1024);
ByteBuffer byteBuffer2=ByteBuffer.wrap(new byte[]{});
ByteBuffer byteBuffer3=ByteBuffer.wrap(new byte[]{},0,100);
get方法:
四种参数四种get方法:
- get():相对方法,读取当前位置的byte,然后position +1
- get(byte[] dst):相对体积方法,将此缓冲区传输到给定的目标数组中的字节数
- get(byte[] dst, int offset, int length) :从当前位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域
- get(int index) : 绝对方法,从指定下标开始读取
其他方法:
asIntBuffer()等:输入的数据可能是其他类型,可以使用这类方法将ByteBuffer转化成想要的类型
flip():翻转
put():放置
等等
通道Channel
channel类继承结构:
有很多种channel,分为两种类型:文件通道、套接字通道
常用的有:
- FileChannel:用于读取、写入、映射和操作文件的通道
- DatagramChannel:读写UDP通信的数据,对应DatagramSocket类
- SocketChannel:读写TCP通信的数据,对应Socket类
- ServerSocketChannel:监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类(服务器)
- ScatteringByteChannel和GatheringByteChannel:分散聚集通道,由操作系统完成
- WritableByteChannel和ReadableByteChannel:接口提供读写API
常用操作:
- 实例化:通道可以使用流的getChannel()方法创建,JDK1.7 中的NIO2针对各个通道提供了一个静态的方法open(),JDK1.7 中的NIO2的Files工具类的newByteChannel()
- isOpen():Channel自带的方法,告诉这个通道是否打开
- close: Channel自带的方法,关闭通道
- read() : Channel大部分子类拥有的方法,从通道读取数据到缓冲区,不同的参数有不同的作用,FileChannel有四种read方法
- write():Channel大部分子类拥有的方法,从缓冲区写入数据到通道,FileChannel有四种write方法
缓冲区与通道:分散、聚集
前面知道了通道类似与流,缓冲区暂时保存数据
那么程序与实体间数据交流就是通过缓冲区与通道的分散读取、聚集写入
分散读取:将数据从通道中读取到多个缓冲区(read方法)
聚集写入:将多个缓冲区的数据写入到单个通道中(write方法)
有两个专门的接口就是实现聚集写入与分散读出:ScatteringByteChannel和GatheringByteChannel
FileChannel实现了这两个接口
案例
聚集写入文件,分散读出文件
package com.company.ScatterGather;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ScatteringByteChannel;
public class ScatterGatherIO {
//聚集写入
public static void Gather(String data) throws FileNotFoundException {
//创建两个ByteBuffer存数据
ByteBuffer byteBuffer1=ByteBuffer.allocate(20);
ByteBuffer byteBuffer2=ByteBuffer.allocate(400);
//把整数放入byteBuffer1
byteBuffer1.asIntBuffer().put(1024);
//把输入的String变量放入byteBuffer2
byteBuffer2.asCharBuffer().put(data);
//GatheringByteChannel接口允许委托操作系统完成任务
//CreatChanner使用文件写入流
GatheringByteChannel gatherChannel=CreatChanner("TestOut.txt",true);
//聚集写入通道
try {
//write只允许一个ByteBuffer
gatherChannel.write(new ByteBuffer[]{byteBuffer1,byteBuffer2});
} catch (IOException e) {
e.printStackTrace();
}
}
//分散写出
public static void Scatter() throws FileNotFoundException {
//创建两个ByteBuffer存数据
ByteBuffer byteBuffer1=ByteBuffer.allocate(20);
ByteBuffer byteBuffer2=ByteBuffer.allocate(400);
//读取文件通道
ScatteringByteChannel scatterChannel=CreatChanner("TestOut.txt",false);
try {
scatterChannel.read(new ByteBuffer[]{byteBuffer1,byteBuffer2});
} catch (IOException e) {
e.printStackTrace();
}
//buffer位置置0
byteBuffer1.rewind();
byteBuffer2.rewind();
System.out.println(byteBuffer1.asIntBuffer().get());
System.out.println(byteBuffer2.asCharBuffer().toString());
}
//输入文件地址和输入方向,决定通道方向
public static FileChannel CreatChanner(String fileUrl,boolean out) throws FileNotFoundException {
FileChannel fileChannel=null;
if (out){
fileChannel=new FileOutputStream(fileUrl).getChannel();
}
else
fileChannel=new FileInputStream(fileUrl).getChannel();
return fileChannel;
}
public static void main(String[] args) throws FileNotFoundException {
String data="hello,welcome to ScatterGatherIO";
Gather(data);
Scatter();
}
}
上面展示将通道与缓冲区的使用
选择器Selector
选择器让一个线程能够处理多个通道,选择器轮询注册在其上的通道,Selector只能管理非阻塞的通道,文件通道(FileChannel等等)是阻塞的,无法管理
常用方法
- open():Selector是抽象类,实例化要通过Selector.open()方法
- select():选择一组键,该通道为IO操作准备,这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件,当这个方法返回后, 当前线程就可以处理 Channel 的事件(返回int型数据,大于0即有多少个通道就绪)
- selectedKeys():返回准备好的通道集合,返回值是Set< SelectionKey>集合型,SelectionKey是就绪通道的标识
- wakeup():唤醒在select()方法中阻塞的线程
Selector的使用
案例
服务器:
package com.company.Selector;
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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Server {
public static void main(String[] args) throws IOException {
//打开ServerSocketChannel通道,等待连接
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置非阻塞
serverSocketChannel.configureBlocking(false);
//绑定端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
//打开选择器
Selector selector = Selector.open();
//将通道注册到选择器上,监听接收事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
//轮询式的获取选择器上已经‘准备就绪’的事件
while (selector.select()>0){
//获取当前选择器中所有注册的"选择健(已就绪的监听事件)"
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
//SelectionKey表示注册的标识
SelectionKey selectionKey = iterator.next();
//判断具体事件,就绪
if (selectionKey.isAcceptable()){
//serverSocketChannel接受客户端连接,返回SocketChannel通道
SocketChannel socketChannel = serverSocketChannel.accept();
//设置非阻塞
socketChannel.configureBlocking(false);
//将客户端通道注册到选择器上
//OP_READ表示通道可读
socketChannel.register(selector,SelectionKey.OP_READ);
}else if (selectionKey.isReadable()){
//获取当前选择器上“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取客户端传过来的数据
int len = 0;
while ((len = socketChannel.read(buffer))>0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}
//取消选择键selectionKey
iterator.remove();
}
}
}
}
客户端:
package com.company.Selector;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
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 {
public static void main(String[] args) throws IOException {
//打开客户端通道SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
//设置为非堵塞模式
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//发送数据给服务端
//控制台输入数据
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String msg = scanner.next();
//写入缓存
byteBuffer.put(msg.getBytes());
//byteBuffer切换模式:读模式
byteBuffer.flip();
//读取byteBuffer的数据
socketChannel.write(byteBuffer);
byteBuffer.clear();
}
//关闭连接
socketChannel.close();
}
}
这里通过选择器实现了服务器,客户端,客户端可以给服务器发送信息,在服务器实现选择器,对于SelectionKey的事件做不同的处理
其实上面的服务器已经实现了多播
同样的代码实现客户端ClientA:
这就是选择器的作用
总结
- 大致了解了Java IO的发展
- BIO:阻塞式IO;伪异步IO:通过线程池完成BIO;NIO:非阻塞式IO;AIO:异步非阻塞式IO
- NIO有三个重要的部件:缓冲区、通道、选择器
- 缓冲区是一个数组,保存要输入输出的数据
- 通道与流类似,是在实体和字节缓冲区之间有效传输数据的媒介,可以双向传输
- 选择器让一个线程能够处理多个通道,只能管理非阻塞的通道
已完成三个部件的详解
Java网络编程(5)NIO - Buffer详解
Java网络编程(6)NIO - Channel详解
Java网络编程(8)NIO - Selector详解