NIO基本介绍:
buffer:
public class BasicBuffer {
public static void main(String[] args) {
//举例说明buffer的使用(简单说明)
//创建一个buffer
IntBuffer intBuffer = IntBuffer.allocate(5); //可以存放5个int
//向buffer中存放数据
for (int i = 0; i < intBuffer.capacity(); i++) {
intBuffer.put(i + 1);
}
//将buffer进行读写切换
intBuffer.flip();
//取数据
while (intBuffer.hasRemaining()) { //是否还存在数据
//get操作维护一个索引,每次调用索引后移一位
System.out.println(intBuffer.get());
}
}
}
NIO和BIO比较:
1.BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO的效率比流IO高很多
2.BIO是阻塞的,NIO是非阻塞的
3.BIO基于字节流和字符流进行操作,而NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通信的时间(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
描述NIO 的 Selector 、Channel 和 Buffer 的关系
1.每个channel 都会对应一个Buffer
2.Selector 对应一个线程, 一个线程对应多个channel(连接)
3.该图反应了有三个channel 注册到 该selector //程序
4.程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念
5.Selector 会根据不同的事件,在各个通道上切换
6.Buffer 就是一个内存块 , 底层是有一个数组
7.数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer是可以读也可以写, 需要 flip 方法切换
8.channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的.
缓冲区(Buffer)
最常用的是ByteBuffer(二进制数据)
缓冲区:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,Channel提供文件,网络读取数据的渠道,但是读取或写入的数据必须经过Buffer
缓冲区中的四个属性含义:
常用API:
public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity()//返回此缓冲区的容量
public final int position()//返回此缓冲区的位置
public final Buffer position(int newPositio)//设置此缓冲区的位置
public final int limit()//返回此缓冲区的限制
public final Buffer limit(int newLimit)//设置此缓冲区的限制
public final Buffer mark()//在此缓冲区的位置设置标记
public final Buffer reset()//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear()//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并未真正擦除
public final Buffer flip()//反转此缓冲区
public final Buffer rewind()//重绕此缓冲区
public final int remaining()//返回当前位置与限制之间的元素数
public final boolean hasRemaining()//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly();//告知此缓冲区是否为只读缓冲区
// JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}
clear()方法用于写模式,其作用为情况Buffer中的内容,所谓清空是指写上限与Buffer的真实容量相同,即limit==capacity,同时将当前写位置置为最前端下标为0处
rewind()在读写模式下都可用,它单纯的将当前位置置0,同时取消mark标记,仅此而已;也就是说写模式下limit仍保持与Buffer容量相同,只是重头写而已;读模式下limit仍然与rewind()调用之前相同,也就是为flip()调用之前写模式下的position的最后位置,flip()调用后此位置变为了读模式的limit位置,即越界位置
flip()函数的作用是将写模式转变为读模式,即将写模式下的Buffer中内容的最后位置变为读模式下的limit位置,作为读越界位置,同时将当前读位置置为0,表示转换后重头开始读,同时再消除写模式下的mark标记
通道(Channel)
1.NIO通道类似于流,区别如下:
通道可以同时进行读写,而流只能读或写
通道可以实现异步读写数据
通道可以从缓冲区读数据,也可以写数据到缓冲区
2.BIO中的Stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作
3.Channel在NIO中是一个接口
public interface Channel extends CCloseable{}
4.常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel
5.FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写
API:
利用Buffer和Channel操作文件的两个Demo
/**
* 将数据写入本地文件
*/
public static void write() throws Exception {
String str = "hello";
//创建输出流 --> Channel
FileOutputStream fos = new FileOutputStream("f:\\01.txt");
//获取对应的FileChannel
FileChannel fileChannel = fos.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将输入放入到buffer中
byteBuffer.put(str.getBytes());
byteBuffer.flip(); //读写切换
//将bytebuffer中的数据写入到channel中
fileChannel.write(byteBuffer);
fos.close();
}
/**
* 从文件中读取数据打印到控制台
*/
public static void read() throws Exception {
//创建文件的输入流
File file = new File("f:\\01.txt");
FileInputStream fis = new FileInputStream(file);
//通过fileInputStream获取对应的FileChannel
FileChannel fisChannel = fis.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将通道中的数据写入到buffer
fisChannel.read(byteBuffer);
//将bytebuffer中的字节数据转化为字符串输出
System.out.println(new String(byteBuffer.array()));
fis.close();
}
/**
* 将文件复制1
*/
public static void copy1() throws Exception {
FileInputStream fis = new FileInputStream("f:\\01.txt");
FileChannel fisChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("f:\\02.txt");
FileChannel fosChannel = fos.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(521);
while (true) {
byteBuffer.clear(); //清空buffer,防止一次没读完
int read = fisChannel.read(byteBuffer);
if (read == -1) break;
//将buffer中的数据写到fosChannel中
byteBuffer.flip();
fosChannel.write(byteBuffer);
}
fis.close();
fos.close();
}
/**
* 文件复制2(api)
*/
public static void copy2() throws Exception {
//创建相关的流
FileInputStream fis = new FileInputStream("f:\\01.txt");
FileOutputStream fos = new FileOutputStream("f:\\03.txt");
//获取对应的channel
FileChannel fisChannel = fis.getChannel();
FileChannel fosChannel = fos.getChannel();
//使用TransferForm
fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
//关闭资源
fis.close();
fos.close();
}
关于Buffer和Channel的使用细节
1.ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用想应的数据类型,否则可能会有BufferUnderflowException异常
2.可以将一个普通的Buffer转换成只读Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
3.NIO还提供了MappedByteBuffer,可以让文件直接在内存中进行修改
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("f:\\01.txt", "rw");
//获取信道
FileChannel fisChannel = randomAccessFile.getChannel();
/*
* 参数一:使用读写模式
* 参数二:0代表可以直接修改起始位置
* 参数三:映射到内存的大小(即最多可将文件的5个字节映射到内存中) (范围是0-5)
* */
MappedByteBuffer mappedByteBuffer =
fisChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'a');
mappedByteBuffer.put(1, (byte) 'b');
randomAccessFile.close();
}
4.NIO还支持多个Buffer(数组)完成读写操作,即Scattering和Gatering
public static void test1() throws Exception {
/*
Scattering:将数据写入到buffer时可以采用buffer数组 [分散]
Gathering:从buffer读取数据时可以采用buffer数组依次读 [聚合]
*/
//使用ServerSocketChannel和SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到socket
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等待客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假设从客户端接收8个字节
//循环读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength) {
long l = socketChannel.read(byteBuffers);
byteRead++;
System.out.println("byteRead=" + byteRead);
//使用流打印, 看看当前的这个buffer的position 和 limit
Arrays.asList(byteBuffers).stream().map(buffer ->
"postion=" + buffer.position() + ", limit="
+ buffer.limit()).forEach(System.out::println);
}
//将所有的buffer进行读写转换
Arrays.asList(byteBuffers).forEach(Buffer::flip);
//将数据读取到客户端
long byteWrite = 0;
while (byteWrite < messageLength) {
long l = socketChannel.write(byteBuffers);
byteWrite++;
}
//将所有的buffer进行clear防止没读取完
Arrays.asList(byteBuffers).forEach(Buffer::clear);
System.out.println("byteRead:=" + byteRead +
" byteWrite=" + byteWrite + ", messagelength" + messageLength);
}
}
Seletor(选择器)
基本介绍
1.JAVA的NIO,用非阻塞IO的方式,可以用一个线程,处理多个客户端连接,就会用到Seletor选择器
2.Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后对每个事件进行相应的处理,这样就可以只用一个线程去管理多个通道,也就是管理多个连接和请求
3.只有在连接真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
4.避免了多线程之间的上下文切换导致的开销
Selector相关API:
1.NIO中的ServerSocketChannel功能类似于ServerSocket,SocketChannel功能类似于Socket
2.selector相关方法说明
selector.select() //阻塞
selector.select(1000) //阻塞1000ms,在1000ms后返回
selector.wakeup() //唤醒
selector.selecctNow() //不阻塞,立刻返回
NIO非阻塞网络编程原理
说明:
1.当客户端连接时,会通过ServerSocketChannel得到SocketChannel
2.Selector进行监听—select方法—返回有事件发生的Channel的个数
3.将socketChannel注册到Seletor上,reguster(Selector sel,int ops)方法-一个selector上可以注册多个SocketChannel
4.注册后返回一个SelectionKey,会和该Selector关联(集合)
5.进一步得到各个SelectionKey(有事件发生)
6.再通过SelectionKey反向获取SocketChannel —-channel()
7.可以通过得到的Channel,完成业务的处理
代码实现
public static void Test() throws Exception {
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到Selector对象
Selector selector = Selector.open();
//绑定端口在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把ServerSocketChannel注册到selector关心 事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环等待客户端连接
while (true) {
int select = selector.select(1000);
if (select == 0) { //没有事件发生
System.out.println("服务器等待了1s,无连接");
continue;
}
//如果返回的大于0
//获取到相关的selectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = iterator.next();
//根据key 对应的通道发生的事件做相应的处理
if (key.isAcceptable()) { //如果时OP_ACCEPT,有新的客户端来连接
//给该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
System.out.println("连接成功,生成了socketChannel" + socketChannel.hashCode());
//将当前的socketChannel注册到selector上,关注读事件,并关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) { //读事件 OP_READ
//通过key反向获取到对应的Channel
SocketChannel channel = (SocketChannel) key.channel();
//获取到该channel关联的Buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("form 客户端" + new String(buffer.array()));
}
//手动从集合中移除当前的SelectionKey,防止多线程出现重复操作
iterator.remove();
}
}
}
SelectionKey—-API
SelectionKey,表示Seletor和网络通道的注册关系
int OP_ACCEPT:表示有新的网络连接可以accept,值为16
int OP_CONNECT:代表连接已经建立,值为8
int OP_READ:代表读操作,值为1
int OP_WRITE:代表写操作,值为4
ServerSoketchannel
SocketChannel
网络IO通道,具体负责进行读写操作,NIO把缓冲区的数据写入通道,或者把通道里的数据读取到缓冲区
生活中NIO一个简单理解
不知道大家有没有用过2010年左右(或许更早)2G时代的手机,可以运行那种基于J2ME的QQ,能聊天,看个空间,偷个菜(文字版)什么的。这种手机一般都有个缺点就是不能后台运行,一旦去做其他事情(玩游戏,看小说等),QQ就掉线了,就不能收到QQ消息了。如果想要实时接收到女神消息,就要一直保持打开着QQ,不能去做其他事情。这就类似于BIO,阻塞的。
后来QQ出了一个手机业务,叫超级QQ(每月10块呢),可以伪实时在线,同时更快的升级(太阳月亮我的最爱)。之所以叫他伪实时在线,是因为它的实现方式是:当QQ收到消息时,腾讯会以短信的形式发到手机上,告诉你某某给你发消息了,请及时处理之类的(也可以直接回复短信,QQ上也会自动转发过去,不太相关暂时忽略)。此时再去登录QQ,就能立刻收到消息了。虽然手机同一时刻依然只能做一件事情,但是在没有QQ消息的时候也无需一直等待了,从而从容不铺去做别的事情。也就是非阻塞的了。
这个超级QQ的业务就像是NIO:人就是Selector,监听事件。短信就像是一个事件。QQ就像是Channel,建立沟通通道。人看到短信,根据短信内容,从而决定要不要打开QQ,处理消息