NIO是面向缓存的非阻塞IO模型,其有三大核心组件:Buffer、Channel、Selector,如下图:
原理都好理解,接下来从Java api来看下三大核心组件的简单使用。
1、Buffer
Buffer有几大子类:ByteBuffer(最常用)、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
Buffer底层维护一个数组,由四个重要参数:
position(数组中下一个可读或可写的位置)
mark(标记)
limit(最大可读或可写的位置)
capacity(数组容量)
以ByteBuffer为例看一下常用的api:
看一段简单代码:
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(5);
for (int i=0;i<intBuffer.capacity();i++){
intBuffer.put(i*2);
}
//读写切换
intBuffer.flip();
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
输出:0、2、4、6、8
Buffer特性:
(1)Buffer支持类型化的put和get
private static void type(){
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
byteBuffer.putInt(100);
byteBuffer.putLong(3);
byteBuffer.putChar('哈');
byteBuffer.flip();
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getChar());
}
但是如果put或是get时超出的Buffer的容量,会抛出java.nio.BufferOverflowException异常。
(2)Buffer可以设置为只读模式
(3)MappedByteBuffer
MappedByteBuffer可以使文件直接在内存(堆外内存)中修改,减少了内核空间和用户空间之间的数据拷贝来提升效率。
private static void mappingBuffer(){
try {
RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/jijingyi/Desktop/test.txt","rw");
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1:FileChannel.MapMode.READ_WRITE 表示使用读写模式
* 参数2:文件可以直接在内存中修改的起始位置
* 参数3:这个5指的是可修改的字节长度,即从下标0-4,修改超出下标4会报 IndexOutOfBoundsExpection
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE,0,5);
mappedByteBuffer.put(0,(byte)'H');
mappedByteBuffer.put(3,(byte)'L');
randomAccessFile.close();
} catch (Exception e) {
e.printStackTrace();
}
}
(4)Buffer的分散和聚集
/**
* scatter(分散):将数据写入到buffer时,可以使用数组,依次写入
* gatter(聚集):从buffer读取数据时,可以使用数组,依次读取
*/
private static void serverBoost(){
try {
//创建服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
//绑定端口
serverSocketChannel.socket().bind(inetSocketAddress);
//监听连接请求
SocketChannel socketChannel = serverSocketChannel.accept();
//缓存数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(6);
byteBuffers[1] = ByteBuffer.allocate(4);
int msgLength = 10; //设定最多写10个字节
while (true){
int readByte = 0;
while (readByte < msgLength){
long l = socketChannel.read(byteBuffers);
readByte += l;
//输出一下每个buffer的position和limit
Arrays.asList(byteBuffers).stream().map(buffer -> "position="+buffer.position()+",limit="+buffer.limit()).forEach(System.out::println);
}
//buffer读写反转
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//再将客服端发送的数据写回去
int writeByte = 0;
while (writeByte < msgLength){
long l = socketChannel.write(byteBuffers);
writeByte += l;
}
//清空缓存,这里并不是把缓存中的数据删除了,而是重置position和limit
Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
}
} catch (Exception e) {
e.printStackTrace();
}
}
2、Channel
NIO的channel类似于BIO的stream,但是区别是:
• channel既可以从buffer中读,也可以向buffer中写,是双向的,而stream是单向的
• channel可以实现异步读写
常用的Channel类:FileChannel(文件读写)、DatagramChannel(UDP协议)、ServerSocketChannel和SocketChannel(TCP协议)
我们来看一下FileChannel的几个api案例,ServerSocketChannel和SocketChannel在介绍netty的时候再说。
(1)文件读取
private static void readData(){
try {
File file = new File("/Users/jijingyi/Desktop/test.txt");
FileInputStream inputStream = new FileInputStream(file);
FileChannel fileChannel = inputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
fileChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
(2)文件写入
private static void writeData(){
String str = "hello world";
//创建一个输出流
try {
//FileOutputStream中持有一个channel属性
FileOutputStream outputStream = new FileOutputStream("/Users/jijingyi/Desktop/test.txt");
FileChannel fileChannel = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(str.getBytes());
byteBuffer.flip();
//注意读和写是相对缓冲区来说的
fileChannel.write(byteBuffer);
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
(3)实现文件拷贝
private static void copyFile(){
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = new FileInputStream("/Users/jijingyi/Desktop/test.txt");
FileChannel fileChannel01 = inputStream.getChannel();
outputStream = new FileOutputStream("test.txt");
FileChannel fileChannel02 = outputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
while (true){
//如果文件读完返回-1
int read = fileChannel01.read(byteBuffer);
if (read == -1){
break;
}
//读写反转
byteBuffer.flip();
fileChannel02.write(byteBuffer);
//这里写完要重置channel,否则会由于position=limit出现死循环(因为再次读取时read会一直等于0)
byteBuffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null){
inputStream.close();
}
if (outputStream != null){
outputStream.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
(4)使用transfFrom实现文件拷贝
private static void copyFileByTransf(){
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = new FileInputStream("/Users/jijingyi/Desktop/17岁.mp3");
FileChannel channelSrc = inputStream.getChannel();
outputStream = new FileOutputStream("/Users/jijingyi/Desktop/100岁.mp3");
FileChannel channelDest = outputStream.getChannel();
channelDest.transferFrom(channelSrc,0,channelSrc.size());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null){
inputStream.close();
}
if (outputStream != null){
outputStream.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
3、Selector
Selector就是选择器,也叫多路复用器,可以同时并发处理多个连接(就是监听连接对应的channel,通过事件机制来触发响应的操作)。
3.1、Selector常用的方法
public static Selector open(); //获得一个选择器对象
public int select(); //监听所有注册的通道,将有事件发生的channel对应的selectionKey加入到集合中,返回有事件触发的通道的个数。这个方法是阻塞的。
public int select(long timeout); //作用同select,但是带超时时间
public int selectNow(); //作用同select,但是不阻塞,不管是否有可处理的channel都返回
public Selector wakeup(); //立刻唤醒选择器对象
public Set<SelectionKey> selectedKeys(); //返回所有有事件触发的selectionKey的集合
3.2、NIO非阻塞网络编程原理
NIO非阻塞网络编程主要涉及4个核心类:Selector、SelectionKey、ServerSocketChannel、SocketChannel,原理如下图
对上图的几点说明:
• 当有客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
• Selector 通过 select() 进行监听,返回有事件发生的通道个数
• 将 SocketChannel 通过 register(Selector sel, int ops) 注册到Selector上,一个 Selector 上可以注册多个通道
• 注册后返回一个 selectionKey ,它会和该 Selector 关联(Selector 维护一个 selectionKey 的集合)
• 进一步得到各个有事件发生的 selectionKey
• SelectionKey 通过 channel() 反向获取 SocketChannel
• 最后通过得到的 channel 处理相应的事件
NIOServer:
private static void startNioServer(){
try {
//创建服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(8888);
serverSocketChannel.socket().bind(inetSocketAddress);
//设置为非阻塞,否则会报 IllegalBlockingModeException 异常
serverSocketChannel.configureBlocking(false);
//创建选择器对象
Selector selector = Selector.open();
//将服务端通道注册到Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
//Selector监听所有注册的通道,这里选择带超时的监听(这里是阻塞的),返回的是有事件触发的通道个数
if (selector.select(5000) == 0){
System.out.println("服务器等待5秒,没有连接");
continue;
}
//获取有事件触发的通道关联的 SelectionKey,然后通过 SelectionKey 反向获取 SocketChannel
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历集合
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
//客户端请求连接事件
if (key.isAcceptable()){
//注意这里的 accept() 虽然是阻塞的,但是因为已经明确了是连接事件,所以会立刻执行
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功,生成SocketChannel=" + socketChannel.hashCode());
//设置为非阻塞
socketChannel.configureBlocking(false);
//将客户端通道注册到 Selector,并关联一个 buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
//读事件
else if (key.isReadable()){
//通过 SelectionKey 反向获取 SocketChannel
SocketChannel socketChannel = (SocketChannel)key.channel();
//获取关联的 buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
//读取数据客户端发送的数据
socketChannel.read(buffer);
System.out.println("客户端发送数据:" + new String(buffer.array()));
}
//手动从集合中删除 SelectionKey,避免重复操作
iterator.remove();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
NIOClient:
private static void nioClient(){
try {
//创建一个 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//提供服务端 ip和port,并连接服务端
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1",8888);
socketChannel.connect(inetSocketAddress);
//这里是非阻塞的,如果还没有连接成功,可以处理其他业务
if (!socketChannel.isConnected()){
while (!socketChannel.finishConnect()){
System.out.println("客户端还未完成连接,处理其他业务");
}
}
//向服务端发送数据
String data = "哈哈哈";
ByteBuffer byteBuffer = ByteBuffer.wrap(data.getBytes());
socketChannel.write(byteBuffer);
System.in.read();
} catch (Exception e) {
e.printStackTrace();
}
}
这段程序有一个问题:客户端如果不加 System.in.read(),服务端会一直触发读事件,一直打印客户端发送的数据,应该是服务端代码有点问题。