欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaIO

程序员文章站 2022-06-13 21:21:05
...

什么是IO

在学习IO之前,我们先要了解什么是IO,有什么用,怎么去使用。

I即是input,O则是output,也就是输入和输出。我们在学习和使用中经常会提到io流其实是一种抽象的概念,表示的是一种数据的无结构化传递。
那么这种数据的无结构化传递的应用场景又是什么呢?顾名思义,我们在需要进行数据传递的时候就会使用IO。比如需要将文件或者说数据传输到另一个地方。打个比方,我在运行Java时需要将C盘下的一个叫test.txt的文件复制并将他送到D盘根目录下,这时候就需要使用IO。当然IO不仅应用于本地磁盘,IO一般是应用于网络传输。

IO的体系

这里我有一张IO体系中常用的流的图
JavaIO

IO流中操作的类虽然很多,但实际我们经常使用的核心也不过是File、InputStream、OutputStream、Reader和Writer。从核心出发,IO流又可以被划分为字节流和字符流。其中字节流操作的数据单元是8位的字节,InputStream、OutputStream作为抽象基类,字符流操作的数据单元是字符,以Writer、Reader作为抽象基类。

网络IO

IO的数据源只能来自于硬盘、键盘、内存和网络。而大部分的情况下,我们要和网络IO打交道。
网络上通信,在Java里需要拿到套接字。在服务端某个端口上注册serversocket进行监听,如果有哪个socket根据对应的ip地址和端口号连接进来,就会建立一个socket实例进行连接。连接之后就可以调用socket实例去调用输入输出流来输入或者输出数据,通信过程如下图所示。
JavaIO
看上去非常简单,但对于底层操作系统来讲并不是这样的。对于底层的操作系统来说,我们发送数据的操作需要先将数据从用户空间的缓冲区复制到内核空间的内核缓冲区,最后才能发送出去。收取数据的时候也是这样的,先内核拿到数据放到内核缓冲区,再复制一份到用户空间的缓冲区。
JavaIO

零拷贝

在上述的一次IO过程里,我们可以发现有一些步骤是很冗余的,比如明明我通过磁盘控制器DMA读取数据到内存来,为什么还要复制一份到用户空间里来,复制的这一份数据也占用了一块和内核空间不同的物理内存,尽管两者的数据是一样的。因此就引申出零拷贝的概念来。
我们用两张图来表现普通的IO和零拷贝的不同。
JavaIO
JavaIO

在JavaNIO里和核心组件channel中有一个方法transferTo,通过这个方法即可实现零拷贝。这里贴一段我写的示例代码。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
/**
 * @author Goddran
 * @version 1.0
 * @date 2020/10/9{TIME}
 */
public class ZeroCopyClient {

    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));
        FileChannel fileChannel = new FileInputStream("F:\\test\\Big.zip").getChannel();
        long position = 0;
        long size = fileChannel.size();
        while(size>0){
            long tf = fileChannel.transferTo(position, size, socketChannel);
            if (tf>0){
                position+=tf;
                size-=tf;
            }
        }
        System.out.println("传输的字节数"+position);
        socketChannel.close();
        fileChannel.close();
    }
}

NIO

NIO是从JDK1.4以来,为了弥补原先IO性能不足而提出的更高效的方式。他的核心组件有三个,channel,selector和Buffer。
NIO对数据的操作都是基于channel和buffer的,channel是一个新的原始IO对象,而buffer为所有原始类型提供缓冲操作。基本操作就是从channel读取数据到缓冲区,或者从缓冲区写数据到channel。在这一过程里,NIO是非阻塞的,也就是说在线程从通道读取数据到缓冲区时,线程依旧可以做其他事情。而selector是用于监听多个通道的事件,例如连接打开、数据到达等,一个selector可以监听多个通道。
IO与NIO具体的区别我用下面这张表来显示。
JavaIO

Channel

关于channel,以我的水平能写的不多,详细部分还请看源码来理解,这里我简单举几个channel的实现。

FileChannel: 从文件中读写数据
DatagramChannel: 通过UDP协议读写网络中的数据
SocketChannel: 通过TCP协议读写网络中的数据
ServerSocketChannel: 监听一个TCP连接,对于每一个新的客户端连接都会创建一个SocketChannel。

Buffer

关于buffer,它是一个对象,包含了需要写入或者刚刚读出的数据,其本质是一块可以写入数据或者读取数据的内存,也是一个byte[]数据,只是在NIO中被封装成了NIO Buffer对象,并提供了一组方法来访问这个内存块。
Buffer三个最重要的数据分别是capacity、position和limit。
例如在最开始我调用allocate(8)方法,给buffer的capacity赋值为8个字节,那么对于buffer对象,他的初始值limit和capacity都是8,而position为0。当我从通道读取了四个字节的数据之后,position的值变成了4。此时如果我们想要将buffer中的数据输出出来只需要简单的这样做就可以

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      socketChannel.read(byteBuffer);
      System.out.println(new String(byteBuffer.array()));

但如果想继续使用这个缓冲区并将这个缓冲区的数据写入通道内,就必须调用flip方法,将buffer的读模式改为写模式。调用filp方法后,limit的值等于当前position的值,在我们这个例子里是4,而position的值变为0。这之后再调用socketChannel.write(buffer)方法,每写入一个字节的数据position的值就会+1直至position等于limit。如果不想继续使用buffer内的数据则需要调用clear方法,将position的值设置为0,limit的值等于capacity,这样再一次写入数据的时候,后写入的数据就会覆盖掉原先的数据,而没有被覆盖掉的数据又会因为下一次flip方法修改limit的值导致没有被覆盖的数据不会被读出。

Selector

Selector多路复用器是JavaNIO中能够检测一个或多个NIO通道是否为诸如读写、连接等事件做好准备的组件,这样调用线程去执行selector就可以一个单独的线程管理多个channel从而管理网络连接。
selector的工作原理是ServerSocketChannel在selector上进行注册,selector就可以监听是否有连接事件发生。然后在循环里调用select方法,这个方法是一个阻塞方法,除非有事件已经准备好了,否则阻塞。如果select不再阻塞,说明有事件已经准备好了,就可以创建set集合去存储selector.selectedKeys()返回的集合。创建迭代器去迭代set集合,查看其中的selectionKey是什么,然后交给对应的方法去处理。示例代码如下

while(true){
   selector.select();//阻塞,可以创建一个线程来管理,实现一对多管理连接
   Set<SelectionKey> selectionKeys = selector.selectedKeys();
   Iterator<SelectionKey> iterator = selectionKeys.iterator();
      while(iterator.hasNext()){
         SelectionKey selectionKey = iterator.next();
         iterator.remove();//将轮询器中的已经被拿出来的selectionKey删除
         if (selectionKey.isAcceptable()){//处理连接事件     进入该条件需要客户端要连接过来
         	handleAccept(selectionKey);
         }else if (selectionKey.isReadable()){//处理读事件        说明客户端有数据写过来
            handleRead(selectionKey);
         }
   }
}

csdn上有很多关于select方法和epoll方法的文章,这里就不再讨论他们的源码,只说结论。
进入selector的请求数量不多的情况下,优先使用select方法,此时遍历整个数组的代价并不算高,这个数量不多大概是指1000个连接以下。
epoll方法则在连接数量高且连接活跃度不高的情况下性能比较优越。

相关标签: Java学习 java