Java NIO学习

来源:互联网 发布:vi系统 知乎 编辑:程序博客网 时间:2024/06/06 03:05
1:   预备知识

操作系统IO模型
    
    IO操作其实在性能方面占有很大的一部分;
    操作系统在底层进行文件操作的时候,通常是大块大块操作;
    A:  缓冲区操作
      缓冲区其实就是操作系统内核的一部分内存空间,对于IO其实就是两部分,一个是外存储器,也叫做硬盘能存储器;另一个是内存;
IO就是在这两个地方进行文件移动;I就是输入,就是把外存储器的文件数据移到内存中去,O就是输出,就是把内存中的数据移到外存储器中;
     对于操作系统层面,内存又分为内核内存和用户内存,JVM处于用户内存的位置;而一般操作系统在进行IO操作的时候,比如I操作,就会先把外存储器的数据移到内核内存空间中,然后操作系统再把内核内存数据移到用户进程空间中;  常规进程都在用户空间中,这些进程是没有访问硬件权限的;所以IO操作都是间接经由内核空间的;

   B:  内存映射文件
    一般内核空间到用户空间移动数据都是要经过缓冲区拷贝的,但是我们还有一种方法可以进行移动数据;就是内存映射文件;用户进程的空间直接映射到内存页;
   优点:用户进程把文件数据当做内存,不需要调用read和write
            适用于大数据拷贝


  C:  流IO
    流IO不是面向块的,是面向流的;javaIO设计是面向流的,与操作系统面向块的模型不匹配,所以操作系统通常把一大块数据放到内存中,而用javaIO操作则是把这一大块的数据一点一点蚕食,速度慢,影响效率;所以JavaNIO操作设计就非常切合操作系统模型;适合大数据传送;


第二章:   缓冲区操作

1:缓冲区是什么?
其实缓冲区就是一个数组,什么类型的缓冲区里面就有一个什么类型的数组,缓冲区对象里面包含了这样一个数组数据还有很多在这个数据上操作的API;Buffer类是缓冲区的高度抽象;一般都有四个特点:
A:  Capacity(容量):  缓冲区能容纳的数据容量,这个在缓冲区被创建的时候被制定,不能改变;
B:   Limit(上界):    缓冲区现存元素的数量
C:      positon (位置):  位置根据读写自动更新,指着下一个要被读写操作的元素
D:     Mark(标记 ):       一个备忘位置;相当于某一个位置的备份;

例子:


在图中:Capacity的数量是10;     Limit是10;  positon:0;     标记未定义;



2:  缓冲区API
      Buffer是一个抽象类,定义了缓冲区基本操作;

3:  存取
     对应的函数式get和put,应为参数和返回值类型的不统一,所以在子类中实现这写方法;
使用举例:
buffer.put((byte)'H').put((byte)'E').put((byte)'L').put((byte)'L').put((byte)'O');
执行后缓冲区数据排列是  HELLO

我们想要把E变成M,然后再末尾添加字母A
buffer.put(1,(byte)'M').put((byte)'A');


4:  缓冲区翻转
我们往缓冲区写数据的时候,只要写一个数据,position就会往后移动1位,这样当我们写到最后也就是缓冲区的上界的时候,position就在末尾;这样的话当我们把缓冲区传给一个通道的时候,通道或利用缓冲区的get方法读取数据,但是现在position还是指向缓冲区末尾的,所以这里我们设计上界的目的就是判断缓冲区是否已经到达最大限度了,而我们呢还应该有一个缓冲区翻转的功能,就是把position移到0,也就是首位;这就会说缓冲区的翻转,Buffer已经实现,函数为flip;所以我们当接受到一个外来的缓冲区的时候,需要将其翻转;

5:  clear函数
这个函数可以把缓冲区清空,但是并不是把缓冲区的数据清空,而只是把上界设为容量的值,并且把position设为起始值;这样我们执行put方法的时候会覆盖原来的数据;

一个利用缓冲区进行填充释放操作

package com.mushu.NIO;

import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;

import javax.swing.text.Position;
//这个程序创建一个缓冲区,目的是把数组string打出来;他每拿到一个数组字符串,都放入缓冲区,通过缓冲区取出来然后print;
//然后把缓冲区重置,继续上述操作第二个字符串;以此类推;

public class BufferTest {
public static void main(String[] args) {
CharBuffer buffer = CharBuffer.allocate(100);
while (fillBuffer(buffer)) {
buffer.flip();
drainBuffer(buffer);
buffer.clear();

}

}

public static void drainBuffer(CharBuffer buffer) {
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
System.out.println("");

}

// 查看缓冲区是否已满
public static boolean fillBuffer(CharBuffer buffer) {
if (string.length <= index) {
return false;
}
// 此处,index初始为0,也就是数组第一个位置,每执行一次,index+1,
// 档index的值大于或者等于数组的长度的时候,
// 也就是数组遍历完了也就是说缓冲区被填满了
String data = string[index++];
for (int i = 0; i < data.length(); i++) {
buffer.put(data.charAt(i));
}

return true;

}

public static String[] string = { "asd", "assssss", "qqqwww", "qqqqqq",
"ffffffff" };
public static int index = 0;

}





6:  压缩缓冲区
比如有一个缓冲区ABCDEFG;现在position指在D处,也就是前面ABC都被处理过了,这时候我们想要丢弃前面处理过的数据达到重复利用缓冲区空间的目的,我们可以使用compact函数,这个函数执行后会把DEFG一一对应复制到1234的位置,从而变成DEFGEFG,这样最后三位是可以利用的有效空间;这样就是压缩函数的作用

7: Mark标记函数
现在我们有一个缓冲区ABCDEF,现在position为0,当我们执行buffer.position(2).mark().position(4);
首先mark标记为2,而position指4;

package com.mushu.NIO;

import java.nio.CharBuffer;

public class Mark {
public static void main(String[] args){
CharBuffer buffer=CharBuffer.allocate(100);
buffer.put('A');
buffer.put('B');
buffer.put('C');
buffer.put('D');
buffer.put('E');
buffer.put('F');
buffer.flip();
while(buffer.hasRemaining()){
System.out.print(buffer.get());
}
System.out.println();
//-----------------------------------------------------------------------
System.out.println("现在position的位置:"+buffer.position());
System.out.println("buffer.position(2)后position的位置:"+buffer.position(2).position());
System.out.println("mark后mark的值:"+buffer.mark());
System.out.println("buffer.position(4):"+buffer.position(4).position());
System.out.println("执行reset后positon的位置(注意现在position的位置将会等于mark之前positon的位置):"+buffer.reset().position());
}

}







8:比较

缓冲区有比较两个缓冲区是否相等的函数  equals
两个缓冲区被认为相等的充要条件:
1:两个缓冲区的数据类型是相同的,数据类型不相同的缓冲区不可能相等;
2: 两个缓冲区对象都剩余相同的元素。缓冲区的容量不需要相同,但是缓冲区剩余元素的数据必须相同,而且一一对应相等;
3:   每个缓冲区执行get函数后返回的剩余数据元素序列应该一致;

如果不满足以上条件,就会返回false;



9: 批量移动
为什么会有批量移动,因为缓冲区设计的 目的就高效处理数据,我们用缓冲区一次处理一个字符数据,那就是杀鸡用牛刀;
API非常详细清楚


10: 创建缓冲区
NIO包中关于缓冲区的8个类都是抽象类,类里面有静态工厂,用来创建新的实例;

创建缓冲区的两种方法:
分配:用内部静态工厂创建实例并分配给类里面一个已有的私有变量空间
包装:用参数传递传进来一个数组作为缓冲区的基础存储区域

分配:
CharBuffer buffer=CharBuffer.allocate(100); 分配一个100字符的缓冲区
包装:提供自己的数组作为缓冲区的备份存储器;
char[] arr=new char[100];
CharBuffer buffer=CharBuffer.wrap(arr);//注意这样做的隐患在于如果你改变了数组的内部,那么缓冲区将会改变;


11:复制缓冲区
视图缓冲器:视图就是一个数据映射,比如A里面用data数据,根据A里面的data数据创建B,B就是一个视图缓冲器。A和B共同影响了data数据;


12: 字节缓冲区
字节缓冲区有其特殊的性质,因为操作系统的IO操作也是面向字节的,与JVM一样;
关于字节顺序:一般字节是操作系统的单位,c语言中char类型是1个字节,那么系统存储char数据的时候就按照字节一个字节一个字节读取写入。因为一个字节对应一个字符,这样很是轻松;但是java中比如int4个字节,我们要存储一个int类型数据,就需要4个字节空间,但是这个4个字节怎么存储呢?  是正序存还是倒叙存。如果高字节位于内存低位值,就是大端字节;如果低位字节位于内存低位,高端字节位于内存高位,这种符合人们逻辑的就是小端字节序;     在设计网络协议的时候,面对不同系统的通信和数据读取,需要辨别不同系统采取的是大端字节序还是小端字节序,这样读取数据才不会错乱;

字节缓冲区的byteorder函数可以指定我们是采用大端字节还是小端字节;


13:  直接缓冲区

14:  视图缓冲区


15:  处理无符号数据

16:  内存映射缓冲区




第三章  通道

1: 什么是通道
提供与IO服务的直接连接,chanel用于在字节缓冲区和位于通道的另一侧的实体(文件或者套接字)直接的有效数据传输;

2:通道只能接受字节缓冲区,因为操作系统IO操作时面向字节的;

3:通道分类
通道时IO的导管,IO广义上分为两大类:FIle IO和StreamIO,相应的有两种通道:文件通道和套接字通道;

4: 打开通道

5:关闭通道

6:  矢量IO

7:  文件通道
文件通道不能直接创建,只能通过在一个打开的file对象上创建;

8: Socket通道

9:  管道



第四章  选择器

1: 阻塞IO和非阻塞IO
TCP是面向连接的传输层协议,一个socket必然保持着一个连接。
传统::我们可以这样理解:在单线程模型下,服务器端开启服务端口来处理客户端服务,也就是说通往服务器端的路只有一条,而却有成千上万个客户端想要通往(连接)服务器,而且服务器这条路同时同刻只能由一个人在上面,如果一个A客户端连接了服务器,在单线程模型下,服务器着呢工资啊处理A客户端请求,比如IO,那么这个时候服务器段就会阻塞,代码停止,那么其他客户端就得等待,而且A客户端还在服务器上进行readIO操作读取服务器数据,这个时候其他客户端就得等更长的时间,服务器的资源这个时候就被A客户端独占了,况且A  read的时候,服务器就得阻塞等待IO操作完成(为什么会阻塞?  当客户端和服务器建立连接后,服务器代码就开始执行read方法读取要从客户端读取数据,但是CPU的执行速度显然要比网络快几个级别,放大的说,cpu用0.5秒执行到了read方法,客户端那边从发送数据经过 线缆路由等复杂关口,等达到服务器段解析接受到数据之间花费2秒,这2秒内服务器就阻塞在read方法,什么事情都干不了,就算有其他连接过过来,他也执行不了;),这样的客户端服务器设计明显不能胜任高并发服务器设计;

改进一步:  要解决这个问题,在服务器段就启用多线程机制,当服务器端接受到A客户端连接请求后,就立即新开一个线程把A交给这个线程处理(这样就解决的主线程的阻塞问题,主线程可以把任务交给子线程处理,当然等待漫长的网络准备也有子线程负责,主线程只复杂接受连接);这样就可以解决并发问题,但是这样做的坏处是:
当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些CPU时间
阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。
在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,我们使用BIO的时候往往会为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可伸缩性。

再改进一步:利用非阻塞IO
我们知道一般socket编程的几步是这样的:收到客户端连接,接受客户端连接请求(2),read客户端发来的数据(3),向客户端发送数据,错误处理
既然传统socket编程中程序因为网络等多种原因会在23处阻塞,那么我们采用一种事件处理机制,类似于观察者模式,就是你们不用在这等着网络准本好接受数据,我去帮你们监听网络数据是否准备好,你们谁想要知道网络数据是否准备好,就到我这里注册一下,等网络数据准备好了,我就通知你们执行;
事件名对应值服务端接收客户端连接事件SelectionKey.OP_ACCEPT(16)客户端连接服务端事件SelectionKey.OP_CONNECT(8)读事件SelectionKey.OP_READ(1)写事件SelectionKey.OP_WRITE(4)

由一个专门的线程来处理所有的IO事件,并负责分发;
NIo的选择器非阻塞IO类似于观察者模式;
selector类似于一个观察者,我们需要把自己需要探知数据的通道注册到观察者上,并且指定观察者说一旦有这个事件操作的话就通知我,所以当观察者发现有注册到他身上的事件的话,就会通知,返回一组key,读到这些key的时候就会得到刚才注册的通道,然后就可以用这个通道读取数据了;如果没有事件,那么select就会阻塞;
NIO设计的是单线程的,也就是说一直只有一个线程处理操作,所以NIO面向的是处理快速数据的,比如read,accept;这样聊天软件比较适合;如果NIO要处理write事件,那么写时候比较耗费时间,这样服务器就阻塞了,所以一般对于写事件,都是新开线程单独处理;


NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。

Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。

对于熟悉于系统调用的C/C++程序员来说,一个阻塞在select上的线程有以下三种方式可以被唤醒:

1) 有数据可读/写,或出现异常。

2) 阻塞时间到,即time out。

3) 收到一个non-block的信号。可由kill或pthread_kill发出。







问题:  Selector.select();  此时如果没有事件来,那么线程会阻塞到这里;那么如果有事件来,这里怎么被唤醒呢?  

当我们创建一个selector的时候,java在操作系统或创建一个selector指向自己的连接,当操作系统发现有事件来的时候,会利用这个连接发送数据就会唤醒selecotr;






NIO事件机制:

相关事件定义 在这个模型中,我们定义了一些基本的事件:

(1)onAccept:

当服务端收到客户端连接请求时,触发该事件。通过该事件我们可以知道有新的客户端呼入。该事件可用来控制服务端的负载。例如,服务器可设定同时只为一定数量客户端提供服务,当同时请求数超出数量时,可在响应该事件时直接抛出异常,以拒绝新的连接。

(2)onAccepted:

当客户端请求被服务器接受后触发该事件。该事件表明一个新的客户端与服务器正式建立连接。

(3)onRead:

当客户端发来数据,并已被服务器控制线程正确读取时,触发该事件。该事件通知各事件处理器可以对客户端发来的数据进行实际处理了。需要注意的是,在本模型中,客户端的数据读取是由控制线程交由读线程完成的,事件处理器不需要在该事件中进行专门的读操作,而只需将控制线程传来的数据进行直接处理即可。

(4)onWrite:

当客户端可以开始接受服务端发送数据时触发该事件,通过该事件,我们可以向客户端发送回应数据。在本模型中,事件处理器只需要在该事件中设置 。

(5)onClosed:

当客户端与服务器断开连接时触发该事件。

(6)onError:

当客户端与服务器从连接开始到最后断开连接期间发生错误时触发该事件。通过该事件我们可以知道有什么错误发生。



原创粉丝点击