1.NIO
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);//大小为10的一个缓冲区
for (int i = 0; i < buffer.capacity(); i++) { //小位缓冲区的大小
int randomNumber = new SecureRandom().nextInt(20); //生成随机数更好的方法
buffer.put(randomNumber);
}
buffer.flip();//翻转 io流的角色变换
while(buffer.hasRemaining()) { //buffer是否有剩余
System.out.println(buffer.get());
}
}
//12 0 4 6 13 11 16 15 0 4
1.NIO的体系分析
- java.io中最为核心的一个概念是流,面向流编程,java中,一个流要么是输入流,要么是输出流,不可能同时既是输入流又是输出流.
- java.nio中拥有3个核心概念:Selector,Channel与Buffer,面向块(block)或是缓冲区(buffer)编程的.Buffer本身就是一块内存,底层实现上,它实际上是个数组,数据的读丶写都是通过buffer来实现的.
- 除了数组以为,buffer还提供了对数据的结构化访问方式,并且可以追踪到系统的读写过程.
- Java中的7种原生数据类型都有各自对应的Buffer类型,如IntBuffer,LongBuffer等,并没有BooleanBuffer类型.
- channel指的是可以向其写入数据或是从中读取数据的对象,它类似于java.stream.但是Channel是双向的,既可以读取流也可以是写入流.
- 所有数据的读写都是通过Buffer来进行的,永远不会出现直接向Channel写入数据的情况,或是直接从Channel读取数据的情况.
- Channel反映出底层操作系统的真实情况:在linux系统中,底层操作系统的通道就是双向.
//从文件读取数据
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("NioTest2.txt");
FileChannel fileChannel = fileInputStream.getChannel(); //文件流获取通道对象
ByteBuffer byteBuffer = ByteBuffer.allocate(512); //创建一个byteBuffer对象存储
fileChannel.read(byteBuffer); //将fileChannel读到ByteBuffer中去,主要是channel从流中读取.
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
byte b = byteBuffer.get();
System.out.println("Character: "+ (char)b);
}
fileInputStream.close();
}
//把一个信息写入系统文件中
public static void main(String[] args) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("NioTest3.txt");
FileChannel fileChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byte[] messages = "hello world welcome, da jia hao".getBytes();
for (int i = 0; i < messages.length; i++) {
byteBuffer.put(messages[i]);
}
byteBuffer.flip();
fileChannel.write(byteBuffer); //byteBuffer写入channel中,理解为写入到读取的流中.
fileOutputStream.close();
}
不管是读还是写,都跟Channel关联着.都是Channel对buffer的操作.
2.Buffer.filp()方法
public final Buffer flip() { //buffer.filp()方法源码
// Invariants: 0 <= mark <= position <= limit <= capacity
limit = position;
position = 0;
mark = -1;
return this;
}
buffer中的flip方法涉及到bufer中的Capacity,Position和Limit三个概念。其中Capacity在读写模式下都是固定的,就是我们分配的缓冲大小,Position类似于读写指针,表示当前读(写)到什么位置,Limit在写模式下表示最多能写入多少数据,此时和Capacity相同,在读模式下表示最多能读多少数据,此时和缓存中的实际数据大小相同。在写模式下调用flip方法,那么limit就设置为了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读。也就是说调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)。
-
NIO Buffer中的3个重要状态属性含义:
- position:
- limit:
- capacity:指buffer包含元素的个数.
-
InBuffer.allocate();完成初始化动作:
- 调用HeapIntBuffer构造函数
- position = 0;limit = capacity;
-
Buffer.put()方法,参数有hb[]数组,向buffer放数据,然后position++;
-
Buffer.clear();回到初始状态/重新读取会进行覆盖所以相当于清空.
3.读取文件,文件通道法
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("input.txt");
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
FileChannel inputChannel = fileInputStream.getChannel();
FileChannel outputChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(4);
while(true) {
byteBuffer.clear(); //如果注释掉这句会怎么样??? *会出现死循环,不停的读取*
int read = inputChannel.read(byteBuffer);
if (read == -1) {
break;
}
byteBuffer.flip();
outputChannel.write(byteBuffer);
}
inputChannel.close();
outputChannel.close();
}
fileChannel在调用read方法时会先对position和limit的值进行检查,如果limit与position的值相等或者position大于limit则直接返回0,不会再调用操作系统的read方法了。
所以read == 0,不等于-1就不会跳出,然后进行filp(),position指针指向0,继续write(),所以就会一直写.
- NIO读取文件涉及的3个步骤:
- 1.从FileInputStream获取到FileChannel对象.
- 2.创建Buffer
- 3.将数据Channel读取到Buffer中.
- 绝对方法与相对方法的含义:
- 1.相对方法:limit值与position值会在操作时被考虑到 //filp()
- 2.绝对方法:完全忽略掉limit值和position值 //get() put()
4.Buffer深入了解
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
//ByteBuffer类型化的put和get方法
byteBuffer.putInt(10);
byteBuffer.putLong(5000000000L);
byteBuffer.putDouble(14.5659);
byteBuffer.putChar('我');
byteBuffer.putShort((short)2);
byteBuffer.putChar('你');
byteBuffer.flip();
System.out.println(byteBuffer.getInt());
System.out.println(byteBuffer.getLong());
System.out.println(byteBuffer.getDouble());
System.out.println(byteBuffer.getChar()); //改为getInt() 会报BufferUnderflowException
System.out.println(byteBuffer.getShort());
System.out.println(byteBuffer.getChar());
System.out.println(byteBuffer);
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte)i);
}
buffer.position(2);
buffer.limit(6);
//Slice Buffer和原有的buffer底层数据就是一份,是原有buffer的备份
ByteBuffer sliceBuffer = buffer.slice();
for (int i = 0; i < sliceBuffer.capacity(); i++) {
byte b = sliceBuffer.get(i);
b *= 2;
sliceBuffer.put(i,b);
}
buffer.position(0);
buffer.limit(buffer.capacity());
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
- 1.Slice Buffer和原有的buffer底层数据就是一份,是原有buffer的备份.
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.getClass());
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte)i);
}
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
readOnlyBuffer.position(0);
// readOnlyBuffer.put((byte)2);
- 1.两个buffer的pos.lim.cap都是独立的.
- 2.只读buffer,我们可以随时将一个普通Buffer调用asReadOnlyBuffer方法返回一个只读Buffer,但是不能将一个只读Buffer转换为读写Buffer
5.NIO的堆外内存和零拷贝
1.Buffe.allocateDirect()的方法:返回一个DirectByteBuffer类
return new DirectByteBuffer(capacity);
2.DirectByteBuffer肯定有一个对象可以访问堆外内存(native上),就是Long address,保存的是堆外内存的地址
3.ByteBuffery是在堆上产生的,数据存储在堆上,而IO设备要进行交互,堆会将数据拷贝到操作系统生成的堆外内存上进行交互.称为间接缓冲区.
4.如果不进行拷贝,直接进行堆内存和IO设备交互,会发生,如果正在交互中,发生GC,会将一部分不用的数据标记整理进行压缩腾出大块内存给新的Java对象,会造成本来的不被回收的数据压缩好数据顺序变化乱套了.不能进行GC,会出现OutMarayErrorEX异常(内存溢出).
5.DirectByteBuffer同过address指向操作系统生成的堆外内存,直接将数据存储到堆外内存的区域中,跟IO设备进行交互,省去一步拷贝过程,称为零拷贝.数据用完后,操作系统会自动释放生成的堆外内存.
6.内存映射文件
1.MappedByteBuffer:内存映射文件是一种允许Java程序直接从内存访问的一种特殊文件.
2.我们可以将整个文件的一部分映射到内存当中,由操作系统来负责相关的页面请求,并且将内存的修改写入文件当中,我们的应用成为只需要处理内存的数据,可以实现非常迅速的IO操作.
3.用于内存映射的内存本身是在Java的堆外内存.
//第二个参数读写状态rw,可读可写
RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");
FileChannel fileChannel = randomAccessFile.getChannel();
//根据map获取内存映射文件,参数为:模式(读写模式),起始位置(起始映射),映射大小
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
mappedByteBuffer.put(0,(byte)'a');
mappedByteBuffer.put(3,(byte)'b');
randomAccessFile.close();
- 可以直接将文件在内存里面去修改,数据改变.
//文件锁的概念
RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest10.txt","rw");
FileChannel fileChannel = randomAccessFile.getChannel();
//获取文件锁,参数3个为:锁的起始位置,锁多少个,是否为共享锁
FileLock fileLock = fileChannel.lock(3,6,true);
System.out.println("valid: " + fileLock.isValid());//是否有效 //true
System.out.println("lock: " + fileLock.isShared()); //true
fileLock.release();
randomAccessFile.close();
7.Buffer的Scattering和Gathering(分散读取和聚集写入)
//建立客户端和服务端的链接 aio异步io
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(8899);
serverSocketChannel.socket().bind(address);
int messageLength = 2 + 3 + 4;
ByteBuffer[] buffers = new ByteBuffer[3];
buffers[0] = ByteBuffer.allocate(2);
buffers[1] = ByteBuffer.allocate(3);
buffers[2] = ByteBuffer.allocate(4);
SocketChannel socketChannel = serverSocketChannel.accept();
while (true) {
//进行读操作
int bytesRead = 0;
while (bytesRead < messageLength) {
long r = socketChannel.read(buffers);
bytesRead += r;
System.out.println("bytesRead : " + bytesRead);
//System.out.println(Arrays.toString(buffers));
Arrays.asList(buffers).stream().map(buffer -> "pos: " + buffer.position()+
", limit: " + buffer.limit()).forEach(System.out::println);
}
Arrays.asList(buffers).forEach(buffer -> buffer.flip());
//进行写操作
long bytesWritten = 0;
while (bytesWritten < messageLength) {
long r = socketChannel.write(buffers);
bytesWritten += r;
}
Arrays.asList(buffers).forEach(buffer -> buffer.clear());
System.out.println("bytesRead :" + bytesRead + ", bytesWritten: " + bytesWritten +
", messageLength: " + messageLength);
}
- 采用telnet localhost 8899去测试
8.字符集Charset
编码: 字符串 -> 字节数组
解码: 字节数组 -> 字符串
Map<String,Charset>Charset.availableCharsets(); //包含的字符集
Charset cs1 = Charset.forName("GBK");
//获取编码器
CharsetEncoder charsetEncoder = cs1.newEncoder();
//获取解码器
CharsetDecoder charsetDecoder = cs1.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("我爱学习");
charBuffer.flip();
//编码
ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);
for (int i = 0; i < 8; i++) {
System.out.println(byteBuffer.get());
}
byteBuffer.flip();
//解码
CharBuffer charBuffer1 = charsetDecoder.decode(byteBuffer);
System.out.println(charBuffer1.toString());
输出:
-50
-46
-80
-82
-47
-89
-49
-80
我爱学习
9.Selector源码分析
1.回顾传统IO的socket写法,缺点:连接一个客户端就要生成一个线程,在一个操作系统上,可运行的线程是有限的.
//服务端
ServerSocket serverSocket = .....
serverSocket.bind(8899);
while(true) {
Socket socket - serverSocket.accept();//阻塞方法
new Tread(socket);
run(){
socket.getInputStream().
....
....
}
}
//客户端
Socket socket = new Socket("ip","port");
socket.connect();
2.selector选择器的概述
- selector可以调用这个类的open方法创建.open本身是会从系统的选择提供器中提供一个选择器.
Selector selector = Selector.open();
- 将selector注册到Channel上,这个步骤是通过SelectionKey进行判断表示的,一个选择器会包含3种SelectionKey.
- key set: 对应selector和channel注册完成后的所有的key(事件的可能性).
- selected-key: 是key set中包含的所有key的一个子集.在所有里面只选择我们要的.
- cancelled-key: 表示在key set中选择好的key,但是后来取消了,保存在cancelled-key集合中.
- select()是一个阻塞的方法,当在channel上发生一个或多个事件时,返回的对象是一个selectKey的集合.其中的 每个selectkey都标识者channel通道上事件感兴趣的部分.
- 我们系统所用的selector对象实际上就是sun.nio.ch.DefaultSelectorSelectorProvider.create()创建的KQueueSelectorProvider创建的一个selector对象.
//由此证明
sout(SelectorProvider.provider().getClass());
sout(sun.nio.ch.DefaultSelectorSelectorProvider.create())
输出:
sun.nio.ch.KQueueSelectorProvider
sun.nio.ch.KQueueSelectorProvider
//是同一个
- 体现出NIO编程和传统编程的显著特点,主要就是通过selector实现的
- 5个channel跟客户发起连接的socketchannel,创建好selector从里面开始去选择,每一个事件一旦产生后optionkey就出来了,携带者与每一个channel所关联的特性,我们通过channel方法可以获取到数据.
- 在整个NIO中事件模型非常重要!