IO工作机制

一,磁盘IO工作机制

1.1 访问文件的方式

1.标注访问:通过系统调用read和write函数;从磁盘复制到内核空间,在复制到用户空间,非常耗费时间,因此在内核空间中存在一个缓存机制。

2.直接iO方式:应用程序直接访问磁盘,而不经过内核空间缓存区。如果程序缓存中没有,在访问内核空间的缓存,这样速度非常低。通常是直接和异步相结合,会取得比较好的性能。

3.同步访问文件的方式:就是数据的读写是同步的,与方式一不同的是,只有数据被成功写入磁盘时才返回给应用成功的标志。这种方式性能较差,一般用于安全性比较高的场合。

4.异步访问方式:当访问数据的线程放出请求后,会接着处理其他的事情,不会阻塞等待,当返回数据时才会继续处理接下来的操作

5.内存映射的方式:操作系统将一块内存区域与磁盘中的文件关联起来,当腰访问内存中的一段数据时转换为访问文件的某一段数据。目的还是减少内核空间到用户空间数据的复制次数,因为这两个空间时共享的

1.2 java访问磁盘文件

在java中File通常并不代表一个真实存在的文件,当你指定一个路径描述符是,返回一个代表这个路径的虚拟对象,这可能是一个真实存在的文件或者包含多个文件的目录。

1.3 对象序列化技术

java序列化就是指讲一个对象转换为一串二进制表示的字节数组,通过保存或者转义这些字节数据来达到持久化的目的。

在纯java环境下,java序列化能够很好的进行工作,但是在多语言环境下,用java序列化存储后,很难用其他语言还原出来,因此应该存储通用的数据结构,如json或者xml

二,网络IO工作机制

2.1 TCP状态转化

2.2 影响网络传输的因素:网络带宽,传输距离,TCP拥塞控制

2.3 Java Socket的工机制

Socket比作城市之间的交通工具。大部分使用的是流套接字,是一种稳定的通信协议。通过Socket唯一代表一个主机上的应用程序的通信链路了

2.4 建立通信链路

2.5 传输数据

三,NIO的工作方式

3.1 BIO : 即阻塞IO,不管是磁盘或者网络IO,数据在写入OutputStream或者从InputStream读取时,都有可能会阻塞,一旦阻塞就会失去CPU的使用权,这在当前的大规模访问量和有性能要求的情况下,是不能接受的,因此需要新的方式

3.2 NIO的工作机制

Channel和Selector是NIO的两个核心概念。可以把Channel比作某种具体的交通工具,而Selector负责键控每个交通工具的当前运行状态,也及时说它可以轮询每个Channel的状态。

Channel 通道主要用于传输数据,从缓冲区的一侧传到另一侧的实体(如文件、套接字...),反之亦然;通道是访问IO服务的导管,通过通道,我们可以以最小的开销来访问操作系统的I/O服务;缓冲区是通道内部发送数据和接收数据的端点;

另外,关于通道Channel接口的定义,很简单,只有两个方法,判断通道是否打开和关闭通道;

复制代码
public interface Channel extends Closeable {

    public boolean isOpen();

    public void close() throws IOException;

}
复制代码

 

  1. 创建通道:

  通道主要分为两大类,文件(File)通道和套接字(socket)通道;涉及的类有FileChannel类和三个socket通道类:SocketChannel、ServerSocketChannel和DatagramChannel;

  下面分别看下这几个通道是如何创建的:

  创建FileChannel通道

  FileChannel通道只能通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel( )方法来获取,如下所示:

        RandomAccessFile raf = new RandomAccessFile ("somefile", "r"); 
        FileChannel fc = raf.getChannel( );

  创建SocketChannel通道

        SocketChannel sc = SocketChannel.open( ); 
        sc.connect (new InetSocketAddress ("somehost", someport));

  创建ServerSocketChannel通道

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(somelocalport));

  创建DatagramChannel通道

        DatagramChannel dc = DatagramChannel.open( );

  2. 使用通道:

  在使用通道的时候,我们通常都将通道的数据取出存入ByteBuffer对象或者从ByteBuffer对象中获取数据放入通道进行传输;

  在使用通道的过程中,我们要注意通道是单向通道还是双向通道,单向通道只能读或写,而双向通道是可读可写的;

  如果一个Channel类实现了ReadableByteChannel接口,则表示其是可读的,可以调用read()方法读取;

  如果一个Channel类实现了WritableByteChannel接口,则表示其是可写的,可以调用write()方法写入;

  如果一个Channel类同时实现了ReadableByteChannel接口和WritableByteChannel接口则为双向通道,如果只实现其中一个,则为单向通道;

  如ByteChannel就是一个双向通道,实际上ByteChannel接口本身并不定义新的API方法,它是一个聚集了所继承的多个接口,并重新命名的便捷接口;

  如下是一个使用通道的例子,展示了两个通道之间拷贝数据的过程,已添加了完整的注释:

复制代码
package nio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

public class Main
{

    public static void main(String[] args) throws IOException
    {
        ReadableByteChannel source = Channels.newChannel(System.in);
        WritableByteChannel dest = Channels.newChannel(System.out);
        channelCopy1(source, dest);
        // channelCopy2 (source, dest);
        source.close();
        dest.close();

    }

    private static void channelCopy1(ReadableByteChannel src, WritableByteChannel dest)
        throws IOException
    {
        ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
        while (src.read(buffer) != -1)
        {
            // 切换为读状态
            buffer.flip();
            // 不能保证全部写入
            dest.write(buffer);
            // 释放已读数据的空间,等待数据写入
            buffer.compact();
        }
        // 退出循环的时候,由于调用的是compact方法,缓冲区中可能还有数据
        // 需要进一步读取
        buffer.flip();
        while (buffer.hasRemaining())
        {
            dest.write(buffer);
        }
    }

    private static void channelCopy2(ReadableByteChannel src, WritableByteChannel dest)
        throws IOException
    {
        ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
        while (src.read(buffer) != -1)
        {
            // 切换为读状态
            buffer.flip();
            // 保证缓冲区的数据全部写入
            while (buffer.hasRemaining())
            {
                dest.write(buffer);
            }
            // 清除缓冲区
            buffer.clear();
        }
        // 退出循环的时候,由于调用的是clear方法,缓冲区中已经没有数据,不需要进一步处理
    }

}
复制代码

  3. 关闭通道

  可以通过调用close()方法来关闭通道;

  一个打开的通道代表与一个特定I/O服务的特定连接,并封装该连接的状态。当通道关闭时,这个连接会丢失,然后通道将不再连接任何东西。

  可以通过isOpen()方法来判断通道是否打开,如果对关闭的通道进行读写等操作,会导致ClosedChannelException异常;

  另外,如果一个通道实现了InterruptibleChannel接口,那么,当该通道上的线程被中断时,通道会被关闭,且该线程会抛出ClosedByInterruptException异常;

Selector一般称为选择器,也可以翻译为多路复用器,主要功能是用于检查一个或者多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个Channel(通道),当然也可以管理多个网络连接。使用Selector的好处在于,可以使用更少的线程来处理更多的通道,相比使用更多的线程,避免了线程上下文切换带来的开销等。

  1.创建:通过调用静态工厂方法Selector.open()方法创建一个Selector对象,open()方法实际上是向SPI1发出请求,通过默认的SelectorProvider对象获取一个新的Selector实例,如:

Selector selector = Selector.open();
  2.注册Channel到Selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

  代码的第一句就是让这个Channel(通道)是非阻塞的。它是SelectableChannel抽象类里的方法,用于使通道处于阻塞模式或非阻塞模式,false表示非阻塞,true表示阻塞

  要想Channel注册到Selector中,那么这个Channel必须是非阻塞的。所以FileChannel不适合Selector,因为FileChannel不能切换为非阻塞模式,更准确的说是因为FileChannel没有继承SelectableChannel。但是SocketChannel可以正常使用。

  代码的第二行,register()方法就是将通道注册到Selector中,并且让Selector监听感兴趣的事件(第二个参数)。

  着重讲一下第二个参数,它是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:Connect、Accept、Read、Write。

  ❤ Connect:成功连接到另一个服务器称为“连接就绪”;

  ❤ Accept:ServerSocketChannel准备好接收新进入的连接称为“接收就绪”;

  ❤ Read:有数据可读的通道称为“读就绪”;

  ❤ Write:等待写数据的通道称为“写就绪”;

  上面这四种事件用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操作。

  (1)key.interestOps():

           通过这个方法来判断Selector是否对Channel的某种事件感兴趣;

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

  (2)key.readyOps():

           ready set 是通道已经准备就绪的操作的集合。在一次选Selection之后,你应该会首先访问这个ready set。Java中定义了以下几个方法来检查这些操作是否就绪:

   //创建ready集合的方法
    int readySet = selectionKey.readyOps();
    //检查这些操作是否就绪的方法
    selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();

  (3)key.attachment():

           可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个特定的通道。例如,可以附加与通道一起使用的Buffer,或者包含聚集数据的某个对象。如:

    key.attach(theObject);
    Object attachedObj = key.attachment();

    还可以在register()方法使用的时候(即Selector注册Channel的时候)附加对象:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

  (4)key.channel()和key.selector() :

           取出SelectionKey关联的Channel和Selector;

    Channel channel = key.channel();
    Selector selector = key.selector();

  4.Selector中的Channel

  选择器维护注册过的通道,这种选择器与通道的注册关系被封装在SelectionKey中

public abstract class Selector
{
    ...
    public abstract Set keys();
    public abstract Set selectedKeys();
    public abstract int select() throws IOException;
    public abstract int select(long timeout) throws IOException;
    public abstract int selectNow() throws IOException;
    public abstract void wakeup();
    ...   
}

  Selector维护的三种类型SelectionKey集合:

  (1)已注册的键的集合(Registered key set)

      所有与选择器关联的通道所生成的键的集合称为已注册键的集合。这个集合通过keys()方法返回,并且有可能是空的。

      注意:并不是所有注册过的键都有效。同时已注册键的集合是不可以直接修改的,若这么做的话,将会抛出ava.lang.UnsupportedOperationException 异常。

  (2)已选择键的集合(Selected key set)

      已注册键的集合的子集,这个集合的每个成员都是相关的通道被选择器判断为已经准备好的并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(有可能是空的)。

      注意:这些键可以直接从这个集合中移除,但是不能添加。若这么做的话将会抛出java.lang.UnsupportedOperationException异常。

  (3)已取消键的集合(Cancelled key set)

      已注册键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但他们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

      注意:当键被取消(可以通过isValid()方法来判断)时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用select()方法时(或者一个正在进行的select()调用结束时),已取消的键的·集合中的被取消的键将会被清理掉,并且相应的注销也将会完成。通道会被注销,新的SelectionKey将被返回。当通道关闭时,所有相关的键会自动取消(一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相应的键将立即被无效化(取消),一旦键被无效化,调用它的与相关的方法就将抛出CancelledKeyException 异常。

  5.select()方法

  在刚初始化的Selector对象中,上面讲述的三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector重载的几个select()方法:

    ❤ int select():阻塞,至少有一个通道在你注册的事件上就绪了;

    ❤ int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒;

    ❤ int selectNow():非阻塞,执行就绪检查过程,但不阻塞,如果当前没有通道就绪,立刻返回0;

  select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在调用select()时进入就绪的通道不会在本次调用中被计入,

  而在前一次select()调用进入就绪但现在已经不在于就绪状态的通道也不会被计入。例如:首次调用select()方法,如果有一个通道变成了就绪状态,返回了1,若再次调用select()方法,

  如果一个另一个通道就绪了,它会再次返回1.如果对第一个就绪的Channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

  一旦调用了select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键的集合。如下:

 1 Set selectedKeys = selector.selectedKeys();
 2 Iterator keyIterator = selectedKeys.iterator();
 3 while(keyIterator.hasNext()) {
 4     SelectionKey key = keyIterator.next();
 5     if(key.isAcceptable()) {
 6         // a connection was accepted by a ServerSocketChannel.
 7     } else if (key.isConnectable()) {
 8         // a connection was established with a remote server.
 9     } else if (key.isReadable()) {
10         // a channel is ready for reading
11     } else if (key.isWritable()) {
12         // a channel is ready for writing
13     }
14     keyIterator.remove();
15 }

  请注意keyIterator.remove()每次迭代结束时的呼叫。在Selector删除SelectionKey作为自己选择的关键实例,当你完成处理后,你必须这样做。这样的话才能在通道下一次变为“就绪”时,Selector将再次将其添加到所选的键集合。

  6.停止选择

  选择器执行选择的过程,系统底层会一次询问每个通道是否就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有一下二种方式来唤醒在Select()方法中阻塞的线程。

  (1)wakeup()方法:一个线程调用select()方法的那个对象上调用Selector.wakeup()方法。阻塞在select()方法上的线程会立马返回。如果有其它线程调用了wakeup()方法,

      但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。

  (2)close()方法:该方法使得任何一个在选择操作中阻塞的线程都被唤醒,用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。

以下是一段典型的NIO代码:

public void selector() throws IOException {
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        Selector selector=Selector.open();
        ServerSocketChannel ssc=ServerSocketChannel.open();
        ssc.configureBlocking(false);//设置为非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));
        ssc.register(selector, SelectionKey.OP_ACCEPT);//注册监听的时间
        while(true) {
            Set selectKeys=selector.selectedKeys();//获取所有的key集合
            Iterator it=selectKeys.iterator();
            while(it.hasNext()) {
                SelectionKey key=(SelectionKey)it.next();
                if((key.readyOps()&SelectionKey.OP_ACCEPT)==SelectionKey.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()&SelectionKey.OP_READ)==SelectionKey.OP_READ) {
                    SocketChannel sc=(SocketChannel)key.channel();
                    while(true) {
                        buffer.clear();
                        int n=sc.read(buffer);
                        if(n<0) {
                            break;
                        }
                        buffer.flip();
                    }
                    it.remove();
                }
            }
        }
        
    }

 在上面这段程序中,将server端的监听链接请求的事件和处理请求的事件放在一个线程终,但是在事件应用中,我们通常会把他放在两个线程终,一个线程负责监听客户端的连接请求,而且以阻塞方式进行的,另一个线程负责处理请求,这个线程才会真正的采用NIO的方式,就像Tomcat和jetty

3.3 Buffer的工作方式

  selector检测到Channel信道有数据传输时,通过select()取得SocketChannel,读取或者写入Buffer缓存区,可以把BUffer理解称为一组基本数据类型的元素列表,通过几个变量来保存这个数据的当前位置:

capacity 缓存区数组的总长度
position 下一个要操作的数据元素的位置
limit  下一个不可操作的数据元素的位置
Mark 记录position的前一个位置

3.4 NIO的数据访问方式

提供了比传统的文件访问方式更好的方法:FileChannel.transferTO,FileChannel.transferFrom 和FileChannel.map两种

  1.FileChannel.transferXX可以减少数据从内核到用户空间的复制,数据直接在内存空间移动,在Linux系统中使用sendfile系统调用

  2.FileChannel.map将文件按照一定大小映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,这用同样较少了从内核到用户空间的损耗。这种方式对大文件的只读操作。但是这种方式与操作系统的底层IO实现是相关的,如下面代码所示:

public static void map() {
        int BUFFER_SIZE=1024;
        String filename="Buffer.txt";
        long fileLength=new File(filename).length();
        int buffercount=1+(int)(fileLength/BUFFER_SIZE);
        MappedByteBuffer[] buffers=new MappedByteBuffer[buffercount];
        long remaining=fileLength;
        for(int i=0;i<buffercount;i++) {
            RandomAccessFile file;
            try {
                file=new RandomAccessFile(filename,"r");
                buffers[i]=file.getChannel().map(FileChannel.MapMode.READ_ONLY, i*BUFFER_SIZE, (int)Math.min(remaining,BUFFER_SIZE));
            }catch(Exception e) {
                e.printStackTrace();
            }
            remaining-=BUFFER_SIZE;
        }
    }
原文地址:https://www.cnblogs.com/QianYue111/p/13290684.html