认识 ByteBuffer
内部结构 ==> https://www.cnblogs.com/nicechen/p/15167899.html
分配空间
可以使用 allocate 方法为 ByteBuffer 分配空间,其他 Buffer 类也有该方法
package netty.c1; import java.nio.ByteBuffer; public class TestByteBufferAllocate { public static void main(String[] args){ System.out.println(ByteBuffer.allocate(16).getClass()); // -- class java.nio.HeapByteBuffer System.out.println(ByteBuffer.allocateDirect(16).getClass()); // -- class java.nio.DirectByteBuffer /* class java.nio.HeapByteBuffer -- java 堆内存,读写效率较低,受到 GC 的影响。 class java.nio.DirectByteBuffer -- 直接内存(系统内存),读写效率高(少一次拷贝),不会受 GC 影响; 缺点:分配的效率低,使用后需要释放资源,否则可能导致内存泄漏。 * */ } }
向 ByteBuffer 写入数据
有两种办法
- 调用 channel 的 read 方法
- 调用 buffer 自己的 put 方法
向 ByteBuffer 读取数据
有两种办法
- 调用 channel 的 write 方法
- 调用 buffer 自己的 get 方法
get 方法会让 position 读指针向后走,如果想重复读取数据
- 可以调用 rewind 方法,将 position 重新置为0
- 或者调用 get(int i) 方法获取索引 i 的内容,他不会移动读指针
mark & reset
mark 做一个标记,记录 position 位置,reset 是将position 重置到 mark 的位置
package netty.c1; import java.nio.ByteBuffer; import java.nio.charset.Charset; public class TestByteBufferAllocate { public static void main(String[] args){ Charset charset = Charset.forName("UTF-8"); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); byteBuffer.put(new byte[]{‘a‘,‘b‘,‘c‘,‘d‘,‘e‘,‘f‘}); byteBuffer.flip(); System.out.println((char) byteBuffer.get()); // a System.out.println((char) byteBuffer.get()); // b byteBuffer.mark(); //加标记,索引2位置 System.out.println((char) byteBuffer.get()); // c System.out.println((char) byteBuffer.get()); // d byteBuffer.reset(); //将 position 重置到索引 2 System.out.println((char) byteBuffer.get()); // c System.out.println((char) byteBuffer.get()); // d } }
向 ByteBuffer 存入/取出字符串
package netty.c1; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; public class TestByteBufferAllocate { public static void main(String[] args){ //将数据存入ByteBuffer中 //1、使用getBytes(),不会自动调用 flip()方法 ByteBuffer buffer1 = ByteBuffer.allocate(1024); buffer1.put("hello".getBytes()); //2、使用Charset,不会自动调用 flip()方法 Charset charset = Charset.forName("UTF-8"); ByteBuffer buffer2 = ByteBuffer.allocate(1024); buffer2.put(charset.encode("hello")); //3、使用StandardCharsets,会自动调用 flip()方法 ByteBuffer buffer3 = StandardCharsets.UTF_8.encode("hello"); //4、使用wrap,会自动调用 flip()方法 ByteBuffer buffer4 = ByteBuffer.wrap("hello".getBytes()); //重ByteBuffer中读取数据 //1、使用StandardCharsets buffer1.flip(); String str1 = StandardCharsets.UTF_8.decode(buffer1).toString(); System.out.println(str1); //2、使用Charset buffer2.flip(); String msg = String.valueOf(charset.decode(buffer2)); System.out.println(msg); } }
半包、黏包
网络上传输多条数据给服务器,数据之间使用 \n 分隔。
但由于某种原因(多条数据合并发送会快)这些数据在接收时,被进行了重新组合,例如3条原始数据:
Hello world!\n
I‘m Lihua.\n
How are you?\n
变成了2个 ByteBuffer,一个叫黏包,一个叫半包:
Hello world!\nI‘m Lihua.\nHow a
re you?\n
如何使用代码将错乱的数据恢复成原来使用 \n 分割的样子
private static void buffExample2() { // 模拟处理黏包、半包现象 ByteBuffer buf1 = ByteBuffer.allocate(50); // 接受到网络传输第一条消息 buf1.put("Hello world!\nI‘m Lihua.\nHow a".getBytes(StandardCharsets.UTF_8)); System.out.println("第一次调用:"); msgSplit(buf1); // 调用处理方法 buf1.put("re you?\n".getBytes(StandardCharsets.UTF_8)); // 接受到网络传输第二条消息 System.out.println("第二次调用:"); msgSplit(buf1); // 调用处理方法 } //处理方法 private static void msgSplit(ByteBuffer buff) { buff.flip(); // 切换成读模式,为下面的读取字符做准备 for (int i = 0; i < buff.limit(); i++) { if (buff.get(i) == ‘\n‘) { // 判断 \n 所在位置 // 计算将要截取字符串的长度,包含 \n 符号在内 int len = i + 1 - buff.position(); // buff.position() 指针的位置 System.out.println("position:" + buff.position() + ",limit:" + buff.limit() + ",len:" + len); ByteBuffer readBuff = ByteBuffer.allocate(len); for (int j = 0; j < len; j++) { readBuff.put(buff.get()); } readBuff.flip(); System.out.println("读取到的消息:" + StandardCharsets.UTF_8.decode(readBuff)); } } System.out.println("切换成写模式!"); buff.compact(); // 切换成写模式,为后面消息写入做准备 }
文件编程
注意:FileChannel 只能工作在阻塞模式下
获取
不能直接打开 FileChannel ,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel ,它们都有 getChannel 方法。
- 通过 FileInputStream 获取的 channel 只能读
- 通过 FileOutputStream 获取的 channel 只能写
- 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取
从 channel 读取数据,写入 ByteBuffer 中,返回值表示读到了多少字节,-1表示到达了文件的末尾。
int readBytes = channel.read(buffer)
写入
从 ByteBuffer 读取数据,写入 channel 中。在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel 中。
while(buffer.hasRemaining()) { channel.write(buffer); }
关闭
channel 必须关闭,不过调用了 FileInputStream、FileOutputStream、RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法。
位置
//获取当前位置 long pos = channel.position(); //设置当前位置 long newPos = ...; channel.position(newPos);
设置当前位置时,如果设置为文件的末尾
- 这时读取会返回 -1
- 这时写入,会追加内容,但是要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
大小
使用 size 方法获取文件的大小
强制写入
操作系统处于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法將文件内容和元数据(文件的权限等信息)立刻写入磁盘。
文件船输
package netty.c1; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.channels.FileChannel; public class TestByteBufferAllocate { public static void main(String[] args){ try{ FileChannel data = new FileInputStream("D:\\11AAnicechen\\idea\\idea Preject\\data.txt").getChannel(); FileChannel file = new FileOutputStream("D:\\11AAnicechen\\idea\\idea Preject\\to.txt").getChannel(); //效率高,底层会利用操作系统的零拷贝进行优化,但是只能传输2g的数据 //data.transferTo(0,data.size(),file); //使用多次传输,解决传输大小问题 long size = data.size(); //left遍历代表还剩余多少字节 for(long left = size;left>0;){ left -= data.transferTo((size - left),left,file); } }catch (Exception e){ e.printStackTrace(); } } }
Selecttor
Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。
Selector(选择器)的使用方法介绍
1. Selector的创建
通过调用Selector.open()方法创建一个Selector对象,如下:
Selector selector = Selector.open();
2. 注册Channel到Selector
channel.configureBlocking(false); SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
Channel必须是非阻塞的。
所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。
SelectableChannel抽象类 有一个 configureBlocking() 方法用于使通道处于阻塞模式或非阻塞模式。
abstract SelectableChannel configureBlocking(boolean block)
register() 方法的第二个参数。这是一个“ interest集合 ”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
- Connect -- 是客户端,,连接建立后触发
- Accept -- 会在有链接请求时触发
- Read -- 可读事件
- Write -- 可写事件
通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个Server Socket Channel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。
这四种事件用SelectionKey的四个常量来表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,使用或运算符即可,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
3. SelectionKey介绍
一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。 key.channel(); // 返回该SelectionKey对应的channel。 key.selector(); // 返回该SelectionKey对应的Selector。 key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。