学习笔记:Java I/O的工作机制

资料来源:《深入分析Java Web技术内幕》第二章

Java的I/O操作类在java.io包下,大概有将近80个类,大概可以分成如下四组:

  • 基于字节操作的I/O接口:InputStream和OutputStream;
  • 基于字符操作的I/O接口:Writer和Reader;
  • 基于磁盘操作的I/O接口:File;
  • 基于网络操作的I/O接口:Socket;(不在java.io包下)

1、基于字节操作的I/O:
有两点值得说明,一是操作数据的方式可以组合使用;二是必须指定流最终写到什么地方,要么写到磁盘,要么写到网络中。

2、基于字符操作的I/O:
无论是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以I/O操作的都是字节而不是字符。因为在开发中通常操作的数据都是字符型式的,为了方便操作才有的字符I/O接口。
Writer类提供了一个抽象方法write(char cbuf[],int off,int len);Reader的操作接口是 int read(char cbuf[],int off,int len),返回读到的n个字节数
无论是Writer还是Reader类,他们都只定义了读取或写入的数据字符的方式(怎么读或写),不规定数据写到哪里
2.1、字节与字符的转化接口:
数据持久化或网络传输都是以字节进行的,所以必须要进行转化。
InputStreamReader类是从字节到字符的转化桥梁;OutputStreamWriter类是从字符到字节的编码过程。

3、Java序列化技术:
Java序列化就是将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。需要持久化,对象必须继承java.io.Serializable接口。反序列化是相反的过程,既是将字节数组再重新构造成对象。
但是在实际开发中,如果是纯Java环境下,Java序列化能够很好地工作。但是在多语言环境下,其他语言很难还原出结果,所以尽量存储通用的数据结构,比如Json或者xml结构数据

4、NIO的工作方式
4.1 BIO:催生出NIO操作方式
BIO既阻塞I/O,不管是磁盘I/O还是网络I/O,数据在写入OutputStream或者从InputStream读取时都有可能会阻塞,一旦有阻塞,线程或失去CPU的使用权,当前有一些解决办法,比如一个客户端对应一个处理县城,出现阻塞也只是一个线程阻塞不会影响其他线程工作,然后采用线程池减少线程创建和回收的成本。
但是,在需要大量HTTP长连接的情况(Web聊天),但并不是每时每刻这些链接都在传输数据,所以不可能同时创建这么多线程来保持链接。基于以上的问题,需要一种新的I/O操作方式:NIO-同步非阻塞的I/O模型

4.2 NIO的工作机制
NIO相关的类有几个非常关键:Channel、Selector(前俩最为关键)、Buffer。
以现实中的城市交通来比喻NIO的工作方式:
Channel:比作某种交通工具(公交、高铁等);
Selector:调度室。控制车辆(Channel)的运行调度系统,监控每辆车的当前运行状态,是进站了、在路上等等(Selector轮询每个Channel的状态);
Buffer:车上的座位

在上车之前,你不知道座位什么样,也不知道车上是否有座位,更不知道自己上的什么车,因为这些你都不能选择,但是这些信息被封装在了运输工具(Socket,比Channel更具体,代表了某一辆交通工具)里面。NIO通过这种方式把信息具体化,让程序员可以控制他们。比如在Buffer中可以控制Buffer的容量、是否扩容以及如何扩容。
上一段经典的NIO代码:

public void selector() throws IOException{
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    //创建一个selector选择器
    Selector selector = Selector.open();
    //创建一个服务端的Channel
    ServerSocketChannel ssc = ServerSocketChannel.open();
    //设置为非阻塞方式
    ssc.configureBlocking(false);
    //将Channel绑定一个Socket对象
    ssc.socket().bind(new InetSocketAddress(8080));
    //注册监听事件
    ssc.register(selector,SelectionKey.OP_ACCEPT);
    while(true){
        //取得所有Key集合
        Set selectedKeys = selector.selectedKeys();
        Interator it = selectedKeys.interator();
        while(it.hasNext()){
            SelectedKey key = (SelectedKey) it.next();
            if(key.readyOps() & SelectedKey.OP_ACCEPT == SelectedKey.OP_ACCEPT){
            ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
            //接收到服务端的请求
            SocketChannel sc = ssChannel.accept();
            sc.configureBlocking(false);
            sc.register(selector,SelectionKey.OP_READ);
            it.remove();
            }else if(key.readyOps() & SelectedKey.OP_ACCEPT == SelectedKey.OP_READ){
            //已经注册在这个选择器上的所有通信信道有需要的事件发生
            SocketChannel sc = (SocketChannel) key.channel();
            while(true){
                buffer.clear();
                //读取数据
                int n = sc.read(buffer);
                if(n<=0){
                    break;
                }
                buffer.flip();
            }
            it.remove();
        }
    }
}
}

代码解释:
调用Selector的静态工厂创建一个选择器,创建一个服务端的Channel,绑定到一个Socket对象,并把这个通信信道注册到选择器上,把这个同信息到设置为非阻塞模式。然后就可以调用Selector的selectedKeys方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生,如果有某个事件发生,将会返回所有的SelectionKey,通过这个对象的Channel方法就可以去的这个通信信道对象,从而都去通信的数据,而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲器。
在上面的代码中,是将Server短的监听连接请求的时间和处理请求的时间放在一个线程中,但是在实践应用中,通常会把他们分开,放在俩线程中:一个专门负责监听客户端的连接请求,已阻塞方式执行;另一个专门负责处理请求,只有这个处理请求的线程才会采用NIO方式,像Web服务器Tomcat和Jetty都是使用这个处理方式。

你可能感兴趣的:(学习笔记)