Loading

Java NIO中的Channel类

Channel

Channel翻译成通道,Channel的角色和OIO中的Stream(流)是类似的,在OIO(OIO的操作是阻塞的,而NIO的操作是非阻塞的)中,同一个网络连接会关联到两个流:一个是输入流(Input Stream),另一个是输出流(Output Stream),Java应用程序通过这两个流,不断地进行输入和输出的操作;

在NIO中,一个网络连接使用一个Channel(通道)表示,所有的NIO的IO操作都是通过连接Channel完成的,Channel的数据流向是双向的,既可以用来进行读操作,又可以用来进行写操作;

注:

Channel和Stream的一个显著的不同,Stream的数据流向是单向的,如InputStream是单向的只读流,OutputStream是单向的只写流;而Channel的数据流向是双向,既可以用来进行读操作,又可以用来进行写操作;

 

在Unix和类Unix计算机操作系统中,文件描述符(file descriptor)是文件或其他输入/输出资源(例如管道或网络套接字)的进程唯一标识符(句柄),即一切设备皆文件;

对于底层的物理链路,操作系统会为应用创建一个文件描述符(file descriptor),用于标识这种底层的物理链路;

一个Java对象有内存的数据结构和内存地址, 那么一个文件描述符(file descriptor)也有一个内核的数据结构和一个进程内的唯一编号来表示;然后操作系统会把这个文件描述提供给应用层,应用层通过对这个文件描述符(file descriptor)去对传输链路进行数据的读取和写入;

参考:https://en.wikipedia.org/wiki/File_descriptor

   https://www.computerhope.com/jargon/f/file-descriptor.htm

 

Channel的理解

TCP/IP四层模型包含:应用层,传输层,网际层,网络接口层;其中,网络接口层,通常也称为数据链路层或数据链路,是TCP/IP模型的最底层;

参考:https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E6%8E%A5%E5%8F%A3%E5%B1%82%E5%AE%89%E5%85%A8

 

NIO中的传输通道,实际上就是对底层的传输链路所对应的文件描述符(file descriptor)的一种封装,以SocketChannel为例,如下:

sun.nio.ch.SocketChannelImpl

SocketChannelImpl成员属性fd为文件描述符对象;

 

java.io.FileDescriptor

FileDescriptor成员属性fd为文件描述符的进程内的唯一编号;

 

对于两个Java应用通过NIO建立双向的连接(传输链路),它们各自都会有一个内部的文件描述符(file descriptor),代表这条连接的各自的应用;如下图:

 

NIO中的Channel的主要实现

NIO中的Channel的主要实现,如下:

  • FileChannel

文件通道,用于文件的数据读写;

  • DatagramChannel

数据报通道,用于UDP协议的数据读写;

  • SocketChannel

套接字通道,用于Socket套接字TCP连接的数据读写;

  • ServerSocketChannel

服务器套接字通道(或服务器监听通道),允许应用监听TCP连接请求,为每个监听到的请求,创建一个 SocketChannel套接字通道;

 

FileChannel文件通道

获取FileChannel通道

通过文件的输入流、输出流获取FileChannel文件通道,示例如下:

// 创建一个文件输入流
FileInputStream fis = new FileInputStream(srcFile);
// 获取文件流的通道
FileChannel inChannel = fis.getChannel();
// 创建一个文件输出流
FileOutputStream fos = new FileOutputStream(destFile);
// 获取文件流的通道
FileChannel outchannel = fos.getChannel();

 

通过RandomAccessFile文件随机访问类,获取FileChannel文件通道实例,示例如下:

// 创建 RandomAccessFile 随机访问对象
RandomAccessFile rFile = new RandomAccessFile("filename.txt", "rw");
// 获取文件流的通道(可读可写)
FileChannel channel = r File.getChannel();

 

读取FileChannel通道

在大部分应用场景,从通道读取数据都会调用通道的java.nio.channels.FileChannel#read(java.nio.ByteBuffer)方法,它从通道读取到数据写入到 ByteBuffer缓冲区,并且返回读取到的数据量;

示例如下:

RandomAccessFile aFile = new RandomAccessFile(fileName, "rw");
// 获取通道(可读可写)
FileChannel channel=aFile.getChannel();
// 获取一个字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(CAPACITY);
int length = -1;
// 调用通道的read方法,读取数据并买入字节类型的缓冲区
while ((length = channel.read(buf)) != -1) {
    // buf中的数据处理
}

示例中channel.read(buf)读取通道的数据时,虽然对于通道来说是读取模式,但是对于ByteBuffer缓冲区来说是写入数据,此时ByteBuffer 缓冲区处于写入模式;

 

写入FileChannel通道

写入数据到通道,在大部分应用场景都会调用通道的write方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源;

java.nio.channels.FileChannel#write(java.nio.ByteBuffer)

该方法的作用是从 ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数;

注:该方法的入参是需要从其中读取数据写入到通道中,所以入参必须处于读取模式,不能处于写入模式;

 

示例如下:

// 如果buf处于写入模式(如刚写完数据),需要flip翻转buf,使其变成读取模式
buf.flip();
int outlength = 0;
// 调用write方法,将buf的数据写入通道
while ((outlength = outchannel.write(buf)) != 0) {
    // buf中的数据处理
}

 

 关闭通道

当通道使用完成后,必须将其关闭,调用close方法即可;

示例如下:

// 关闭通道
channel.close( );

 

强制刷新到磁盘

在将缓冲区写入通道时,出于性能原因,操作系统不可能每次都实时将写入数据刷到磁盘,完成最终的数据保存;

如果在将缓冲数据写入通道时,需要保证数据能落地写入到磁盘,可以在写入后调用一下FileChannel#force方法;

示例如下:

// 强制刷新到磁盘
channel.force(true);

 

FileChannel文件复制的示例

查看代码
public class FileChannelCopyDemo {
    private final static Logger logger = LoggerFactory.getLogger(FileChannelCopyDemo.class);

    /**
     * 演示程序的入口函数
     *
     * @param args
     */
    public static void main(String[] args) {
        //演示复制资源文件
        nioCopyResourceFile();
    }

    /**
     * 复制两个资源目录下的文件
     */
    public static void nioCopyResourceFile() {
        String sourcePath = "/filename.txt";
        String srcPath = getResourcePath(sourcePath);
        logger.info("srcPath:{}", srcPath);

        String destPathName = "copy";
        String destPath = builderResourcePath(destPathName);
        logger.info("destPath:{}", destPath);
        String destFileName = "copy.txt";

        nioCopyFile(srcPath, destPath, destFileName);
    }


    /**
     * 复制文件
     *  @param srcPath
     * @param destPath
     * @param destFileName
     */
    public static void nioCopyFile(String srcPath, String destPath, String destFileName) {

        File srcFile = new File(srcPath);
        File destDir = new File(destPath);
        File destFile = new File(destDir, destFileName);

        try {
            if (!destDir.exists()) {
                destDir.mkdir();
            }

            //如果目标文件不存在,则新建
            if (!destFile.exists()) {
                destFile.createNewFile();
            }

            long startTime = System.currentTimeMillis();

            FileInputStream fis = null;
            FileOutputStream fos = null;
            FileChannel inChannel = null;
            FileChannel outchannel = null;
            try {
                fis = new FileInputStream(srcFile);
                fos = new FileOutputStream(destFile);
                inChannel = fis.getChannel();
                outchannel = fos.getChannel();

                ByteBuffer buf = ByteBuffer.allocateDirect(1024);
                //从输入通道读取到buf
                while (inChannel.read(buf) != -1) {

                    //翻转buf,变成成读模式
                    buf.flip();

                    int outlength = 0;
                    //将buf写入到输出的通道
                    while ((outlength = outchannel.write(buf)) != 0) {
                        System.out.println("写入字节数:" + outlength);
                    }
                    //清除buf,变成写入模式
                    buf.clear();
                }

                //强制刷新磁盘
                outchannel.force(true);
            } finally {
                outchannel.close();
                fos.close();
                inChannel.close();
                fis.close();
            }
            long endTime = System.currentTimeMillis();
            logger.info("复制毫秒数:{}", (endTime - startTime));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String getResourcePath(String resName) {
        URL url = FileChannelCopyDemo.class.getResource(resName);
        String path = null;
        if (null == url) {
            path = FileChannelCopyDemo.class.getResource("/").getPath() + resName;
        } else {
            path = url.getPath();

        }
        String decodePath = null;
        try {
            decodePath = URLDecoder.decode(path, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        if (isWin()) {
            return decodePath.substring(1);
        }
        return decodePath;
    }

    private static boolean isWin() {
        String os = System.getProperty("os.name");
        if (os.toLowerCase().startsWith("win")) {
            return true;
        }
        return false;
    }

    /**
     * 构建当前类路径下的 resName资源的完整路径
     * url.getPath()获取到的路径被utf-8编码了
     * 需要用URLDecoder.decode(path, "UTF-8")解码
     *
     * @param resName 需要获取完整路径的资源,需要以/打头
     * @return 完整路径
     */
    public static String builderResourcePath(String resName) {
        URL url = FileChannelCopyDemo.class.getResource("/");
        String path = url.getPath();
        String decodePath = null;
        try {
            decodePath = URLDecoder.decode(path, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return decodePath + resName;
    }

}

示例中新建的ByteBuffer是写入模式才可以作为inChannel的read方法的参数,read方法将从inChannel读到的数据写入到ByteBuffer,之后需要调用缓冲区的flip方法,将ByteBuffer从写入模式切换成读取模式,这才能作为outchannel的write方法的参数,以便从ByteBuffer读取数据,最终写入到outchannel输出通道;

 

SocketChannel套接字通道

在NIO中,涉及网络连接的通道有两个:一个是 SocketChannel负责连接的数据传输,另一个是 ServerSocketChannel负责连接的监听(即TCP协议交互);其中NIO中的SocketChannel传输通道,与OIO中的Socket类对应;NIO中的 ServerSocketChannel监听通道,对应于OIO中的ServerSocket类;

ServerSocketChannel仅仅应用于服务器端,而SocketChannel则同时处于服务器端和客户端;

 

TCP协议服务端与客户端的API交互如下:

无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式;通过调用AbstractSelectableChannel#configureBlocking方法设置是否阻塞;

// 设置为非阻塞模式
socketChannel.configureBlocking(false);

// 设置为阻塞模式
socketChannel.configureBlocking(true);

在阻塞模式下,SocketChannel通道的connect连接、read读、write写操作都是同步的和阻塞式的,在效率上与Java旧的OIO的面向流的阻塞式读写操作相同;

 

获取 SocketChannel传输通道

在客户端,先通过SocketChannel的open方法获得一个套接字传输通道;然后将socket套接字设置为非阻塞模式;最后通过connect方法对服务器的IP和端口发起连接;示例如下:

// 获得一个套接字传输通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
// 对服务器的 IP 和端口发起连接
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));

 

非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect方法就返回了,因此需要不断地自旋,检查当前是否是连接到了主机,示例如下:

while (!socketChannel.finishConnect()){
    // 不断地自旋、等待,或者做一些其他的事情
}

 

在连接建立的事件到来时,服务器端的ServerSocketChannel能成功地查询出这个新连接事件,并且通过调用服务器端 ServerSocketChannel监听套接字的accept方法,来获取新连接的套接字通道,示例如下:

// 新连接事件到来,首先通过事件,获取服务器监听通道
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获取新连接的套接字通道
SocketChannel socketChannel = server.accept();
// 设置为非阻塞模式
socketChannel.configureBlocking(false);

 

读取SocketChannel传输通道

当SocketChannel传输通道可读时,可以从SocketChannel读取数据,具体方法与前面的文件通道读取方法是相同的,调用read方法,将数据读入缓冲区 ByteBuffer;示例如下:

ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buf);

 

写入到SocketChannel传输通道

大部分应用场景,数据写入到SocketChannel都会调用通道的write方法;示例如下:

// 写入前需要读取缓冲区,要求ByteBuffer是读取模式
buffer.flip();
socketChannel.write(buffer);

 

关闭SocketChannel传输通道

在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次shutdownOutput终止输出方法,向对方发送一个输出的结束标志,然后调用SocketChannel的close方法,关闭套接字连接;示例如下:

// 调用终止输出方法,向对方发送一个输出的结束标志
socketChannel.shutdownOutput();
// 关闭套接字连接
socketChannel.close();

 

DatagramChannel数据报通道

在Java中使用UDP协议传输数据,比TCP协议更加简单;与Socket套接字的TCP传输协议不同,UDP协议不是面向连接的协议;使用UDP协议时,只要知道服务器的 IP和端口就可以直接向对方发送数据;在Java NIO中,使用DatagramChannel数据报通道来处理 UDP协议的数据传输;

 

UDP协议客户端和服务端的API交互如下:

获取DatagramChannel数据报通道

获取数据报通道的方式很简单,调用DatagramChannel类的 open静态方法即可,然后调用configureBlocking方法,设置是否阻塞;示例如下:

// 获取DatagramChannel数据报通道
DatagramChannel channel = DatagramChannel.open();
// 设置为非阻塞模式
datagramChannel.configureBlocking(false);

 

不论是TCP还是UDP,对于服务端,还需要调用bind方法绑定一个数据报的监听端口,bind方法将IP地址相关信息与套接字关联起来;

创建socket时指定了它的地址族,但是没有指定使用该地址中的哪个具体socket地址,因此需要将一个socket和socket地址绑定;将一个socket与socket地址绑定称为socket命名;在服务器程序中,通常需要命名socket,只有命名后客户端才能知道该如何连接它;客户端通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址;

实例如下:

// 调用bind方法绑定一个数据报的监听端口
channel.socket().bind(new InetSocketAddress(10000));

 

读取DatagramChannel数据报通道数据

当DatagramChannel通道可读时,可以从 DatagramChannel读取数据,与SocketChannel读取方式不同,这里不调用read方法,而是调用receive方法将数据从DatagramChannel读入,再写入到 ByteBuffer缓冲区中;示例如下:

// 创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 从DatagramChannel读入,再写入到ByteBuffer缓冲区
SocketAddress clientAddr= datagramChannel.receive(buf);

通道读取receive方法虽然读取了数据到buf缓冲区,但是其返回值是SocketAddress类型,表示返回发送端的连接地址(包括IP和端口);

 

写入DatagramChannel数据报通道

向DatagramChannel发送数据与向SocketChannel发送数据的方法是不同的,DatagramChannel这里不是调用write方法,而是调用send方法;示例如下:

// 把缓冲区翻转到读取模式
buffer.flip();
// 调用send方法,把数据发送到目标IP+端口
dChannel.send(buffer, new InetSocketAddress( 127.0.0.1",10000));
// 清空缓冲区,切换到写入模式
buffer.clear();

由于UDP是面向非连接的协议,因此在调用 send方法发送数据的时候,需要指定接收方的地址(IP和端口);

 

关闭 DatagramChannel数据报通道

直接调用close方法,即可关闭数据报通道;

dChannel.close();

 

posted @ 2022-11-20 02:13  街头卖艺的肖邦  阅读(166)  评论(0编辑  收藏  举报