一、缓冲区
1.1 基本介绍
缓冲区(buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读写或写入的数据都必须经过Buffer
1.2 Buffer类及其子类分析
Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其包含的数据元素的信息:
对于java中的基本数据类型(Boolean除外),都有一个Buffer类型与之相对应,最常用的自然是ByteBuffer类(二进制数据),该类的主要方法如下:
代码演示
package com.bio;
import java.nio.IntBuffer;
/*
* 实验目的:掌握Buffer的基本使用
* 实验步骤: 利用buffer 进行存取数据
* */
public class BasicBuffer {
public static void main(String[] args) {
//创建一个buffer对象,设置容量为10,默认为16
IntBuffer intBuffer = IntBuffer.allocate(10);
//循环往buffer中放入数据
for (int i = 0; i < 10; i++) {
intBuffer.put(i);
}
//反转buffer 当需要切换 读/写操作时 必须要调用 filp()
intBuffer.flip();
//设置开始读取的索引位置
intBuffer.position(5);
//设置读取的最大索引位置
intBuffer.limit(8);
while(intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}
二、 Channel(通道)
2.1 基本介绍
NIO的通道类似于流,但是有些区别如下:
- 通道可以同时进行读写,而流只能读或者只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读取数据,也可以写数据到缓冲区中
- Channel在NIO中是一个接口public interface Channel extends Closeable{}
- 常用的Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel.
- FileChannel 用于文件的数据读写,DatagrapChannel 用于UDP的数据读写,ServerSocketChannel 和SocketChannel用于TCP的数据读写。
2.2 FileChannel 类
FileChannel主要用来对本地文件进行IO操作,常见的方法有
- public int read(ByteBuffer dst) 从通道读取数据并放到缓冲区中
- public int write(byteBuffer src) 把缓冲区的数据写到通道中
- public long transferFrom (ReadableByteChannel src long position,long count) 从目标通道中复制数据到当前通道
- public long transferTo(long position,long count,WritableByteChannel target),把数据从当前通道复制给目标通道
2.3 实例操作
实例要求
- 使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道)
- 将 ”小沙弥,真帅“写入到file01.txt中
- 从file01.txt 中读取数据显示到控制台
package com.nio;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/*
* 实验目的:掌握对NIO中的channel使用
* 实验步骤:使用前面学习后的ByteBuffer(缓冲)和FileChannel(通道),将;小沙弥,真帅!“写入到file01.txt中
* */
public class NIOFileChannel01 {
//创建ByteBuffer对象
static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
public static void main(String[] args) throws FileNotFoundException {
// writeFile();
readFile();
}
//将字符串写入到文件中
public static void writeFile() throws FileNotFoundException {
String str="小沙弥 真帅!";
//创建一个文件输入流
FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
//通过文件输入流得到一个channel对象
FileChannel channel = fileOutputStream.getChannel();
//将数据写入到缓冲区中,并反转缓冲区
byteBuffer.put(str.getBytes());
byteBuffer.flip();
try {
channel.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//关闭文件输入流
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//从文件中读取字符串显示到控制台
public static void readFile() throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
FileChannel channel = fileInputStream.getChannel();
try {
channel.read(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
byteBuffer.flip();
byte bytes[]=new byte[1024];
bytes = byteBuffer.array();
System.out.println(new String(bytes));
}
}
流程图:
实例 2
实验目的:巩固NIO中channel的使用
实验步骤:将图片使用 channel的transferFrom() 进行拷贝
package com.nio;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
/*
* 实验目的:巩固NIO中channel的使用
* 实验步骤:将图片使用 channel的transferFrom() 进行拷贝
* */
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("d:\\a1.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
//获取各个流相对应的filechannel
FileChannel sourceChannel = fileInputStream.getChannel();
FileChannel targetChannel = fileOutputStream.getChannel();
//将数据从目标通道复制到当前通道
targetChannel.transferFrom(sourceChannel,0,sourceChannel.size());
//关闭各个流
fileInputStream.close();
fileOutputStream.close();
}
}
三 、Selector (选择器)
3.1 基本介绍
- Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
- Selector 能够检测多个注册的通道上是否有事件发生 (注意:多个channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在连接真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下下文切换导致的开销
注意重点💥
1、Netty的IO线程 NioEventLoop聚合了Selector(选择器,这叫多路复用器),可以同时并发处理成百上千客户端的连接
2、当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
3、线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
4、由于读写操作都是非阻塞的,这就可以充分提升IO线程运行效率,避免由于频繁IO阻塞导致的线程挂起
5、一个IO线程可以并发处理N个客户端连接和读写操作,这就从根本上解决了传统同步阻塞IO一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3.2 Select 相关方法
Selector 类 是一个抽象类,常用方法和说明如下:
public abstract class Selector implement Closeable{}
- public static Selector open() //得到一个选择器对象
- public innt selector long timeout; //监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
- public Set< SelectionKey> selectedKeys(); //从内部集合中得到所有的SelectionKey
- selector.select() //阻塞
- selector.select(1000); //阻塞1000毫秒,在1000毫秒后返回
- selector.wakeup(); //唤醒selector
- selector.selectNow(); 不阻塞,立马返还
3.3 Selector 、SelectionKey、ServerSocketChannel 关系梳理
上图说明💥
1、当客户端连接时,会通过ServerSocketChannel得到客户端的SocketChannel
2、将SocketChannel注册到Selector上,register(Selector sel, int ops),一个selector上可以注册多个SocketChannel ops:注册的时间,读/写/连接
3、注册后返回一个SelectionKey,会和该Selector关联(集合)
4、Selector 进行监听 select方法,返回有事件发生的通道个数
5、进一步得到各个SelectionKey(有事件发生时)
6、再通过SelectionKey反向获取 SocketChannel对象,使用此对象的channel()方法得到channel,完成业务逻辑
3.4 代码实例
3.4.1 服务端代码
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/*
* 实验目的:理解NIO非阻塞网络编程机制
* 实验步骤:编写一个NIO入门案例,
* 实现服务器端和客户端之间的数据简单通讯(非阻塞)
* */
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建一个ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selector对象
Selector selector = Selector.open();
//绑定一个端口6666,在服务器监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把serverSocketChannel注册到selector 关心事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while(true){
int i=0;
//这里我们等待1秒,如果没有事件发生,就返回
if(selector.select(1000)==0){
i++;
System.out.println("服务器等待了"+i+"秒,无连接");
continue;
}
//如果返回的>0,就获取到相关的selectionkey集合
//1.如果返回的>0,表示已经获取到关注的事件
//2、selector。selectedKeys()返回关注事件的集合
// 通过selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历Set<SelectionKey>,使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while(keyIterator.hasNext()){
//获取到selectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
//如果是连接事件
if(key.isAcceptable()){
//连接事件会生成一个socketChannel对象
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个socketChannel"+socketChannel.hashCode());
//将socketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector,同时给socketChannel关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {//发生OP_READ
//通过key 反向获取对应到对应SocketChannel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该channel关联的buffer
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
channel.read(byteBuffer);
System.out.println("form 客户端"+new String(byteBuffer.array()));
}
//手动从集合中移除当前的selectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
3.4.2 客户端代码
package com.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的ip和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if(!socketChannel.connect(inetSocketAddress)){
while(!socketChannel.finishConnect()){
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
}
}
//如果连接成功,就发送数据
String str="hello 小沙弥!";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将buffer数据写入channel
socketChannel.write(byteBuffer);
System.in.read();
}
}
结果:
四、SelectionKey
4.1 基本介绍
SelectionKey 表示Selector 和网络通道的注册关系,共四种:
- int OP_ACCEPT:有新的网络连接可以accept,值为16
- int OP_CONNECT:代表连接已经建立,值为8
- int OP_READ:代表读操作 值为1
- int OP_WRITE:代表写操作,值为4
源码中:
💥推荐阅读💥