通道(Channel)

Posted AoTuDeMan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通道(Channel)相关的知识,希望对你有一定的参考价值。

层次结构图

从上图可以看出,Channel是所有类的父类,它定义了通道的基本操作。从Channel引申出的其他接口都是面向字节的子接口,这也意味着通道只能在字节缓冲区(ByteBuffer)上操作。

Channel和Buffer

Channel和Buffer之间的关系,如下图所示:

Channel中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

通道基础

  先来看一下基本的Channel接口,下面代码是Channel接口的完整源码:

 1 public interface Channel extends Closeable {
 2 
 3     /**
 4      * Tells whether or not this channel is open.  </p>
 5      *
 6      * @return <tt>true</tt> if, and only if, this channel is open
 7      */
 8     public boolean isOpen();
 9 
10     /**
11      * Closes this channel.
12      *
13      * <p> After a channel is closed, any further attempt to invoke I/O
14      * operations upon it will cause a {@link ClosedChannelException} to be
15      * thrown.
16      *
17      * <p> If this channel is already closed then invoking this method has no
18      * effect.
19      *
20      * <p> This method may be invoked at any time.  If some other thread has
21      * already invoked it, however, then another invocation will block until
22      * the first invocation is complete, after which it will return without
23      * effect. </p>
24      *
25      * @throws  IOException  If an I/O error occurs
26      */
27     public void close() throws IOException;
28 
29 }

  和缓冲区不同,Channel的API主要由接口来指定。不同的操作系统上通道的实现会有根本性的差异,所以通道API仅仅描述了可以做什么,因此很自然的,通道实现经常使用操作系统的本地代码,通道接口允许开发者以一种受控且可移植的方式来访问底层的I/O服务。从上面最基础的源代码可以看出,所有的通道只有两种共同的操作:检查一个通道是否打开 isOpen() 方法和关闭一个打开了的通道 close()方法,其余所有的东西都是那些实现Channel接口以及它的子接口的类。

  从最基础的Channel引申出的其他接口都是面向字节的子接口:在Channel的众多实现中,有一个SelectableChannel实现,其表示可被选择的通道。任何一个SelectableChannel都可以将自己注册到一个Selector中,这样,这个Channel就能被Selector(如果对Selector不了解,可看文章I/O 模型中Selector部分)所管理,而一个Selector可以管理多个SelectableChannel。当这个SelectableChannel的数据准备好时,Selector就会接到通知,去获取那些准备好的数据。而SocketChannel就是SelectableChannel的一种。

  同时,通道只能在字节缓冲区上操作。层次接口表明其他数据类型的通道也可以从Channel接口引申而来。这是一种很好的映射,不过非字节实现是不可能的,因为操作系统都是以字节的形式实现底层I/O接口的。

Channel的主要实现

  FileChannel:用于读取、写入、映射和操作文件的通道。

  DatagramChannel:通过UDP读写网络中的数据通道。

  SocketChannel:通过tcp读写网络中的数据。

  ServerSocketChannel:可以监听新进来的tcp连接,对每一个连接都创建一个SocketChannel。

获取通道的方式

  (1)通过getChannel()方法获取;

    前提是该类支持该方法。支持该类的方法有:FileInputStream、FileOutputStream、RandomAccessFile、Socket、ServerSocket、DatagramSocket。

  (2)通过静态方法open();

  (3)通过JDK1.7中的Files的newByteChannel()方法;

Scatter(分散)/Gather(聚集)

  ❤ 分散:从Channel中读取是指在读操作时将读取的数据写入多个Buffer中,因此,Channel将从Channel中读取的数据分散到多个Buffer中;

  ❤ 聚集:指将数据写入到Channel中时将多个Buffer的数据写入同一个Channel,因此,Channel将多个Buffer中的数据聚集后发送到Channel。

下面例子是分散:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。

 认识了解Channel

看一下基本的Channel接口:

ByteChannel:

1 public interface ByteChannel
2     extends ReadableByteChannel, WritableByteChannel
3 {
4 
5 }

 WritableByteChannel:

public interface WritableByteChannel
    extends Channel
{
    public int write(ByteBuffer src) throws IOException;

}

 ReadableByteChannel :

public interface ReadableByteChannel extends Channel {
    public int read(ByteBuffer dst) throws IOException;

}

   通道可以是单向的也可以是双向的。一个Channel类可能只实现了定义read() 方法的 ReadableByteChannel接口,而另一个Channel类也许只是实现了定义write() 方法的 WritableChannel接口,那么实现这两种接口之一的类都是单向的,就只能在一个方向上传输数据。如果一个类同时实现了这两个接口,那么这个类它就是双向的,可以进行双向的传输数据,就像上面的ByteChannel。

  通道不仅可以单向双向,也可以是阻塞和非阻塞的,非阻塞模式的通道永远不会让调用的线程休眠,请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式(例如:从SelectableChannel引申而来的类可以和支持选择的选择器(Selector)一起使用)。

文件通道

由于我们在开发中文件I/O用到的地方比较多,所以对于文件通道必须要详细了解。

  通道是访问I/O服务的导管,I/O可以广义的分为两大类:File I/O和Stream I/O。那么相应的,通道也可以广义上的分外两种类型,分别是文件(File)Channel和套接字(Socket)通道。文件通道指的是 FileChannel,套接字通道则有三个,分别是SocketChannel、ServerSocketChannel和DatagramChannel。

  通道可以通过多种方式创建。Socket通道可以通过Socket通道的工厂方法直接创建,但是一个FileChannel对象却只能通过一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel()方法来获取,开发者不能直接创建一个FileChannel。

接下来通过UML图来了解一下文件通道的类层次关系:

文件通道总是阻塞的,因此不能被置于非阻塞模式下。

  前面提到过,FileChannel对象不能直接创建,一个FileChannel实例只能通过一个打开的File对象(RandomAccessFile、FileInputStream或FileOutputStream)上调用getChannel()方法获取,通过调用getChannel()方法会返回一个连接到相同文件的FileChannel对象且该FileChannel对象具有与File对象相同的访问权限,然后就可以使用通道对象来利用强大的FileChannel API了。

  FileChannel对象是线程安全的,多个线程可以在同一个实例上并发调用方法而不会引起任何问题,不过并非所有操作都是多线程的。影响通道位置或者影响文件的操作都是单线程的,如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他想尝试进行此类操作之一的线程必须等待,并发行为也会受到底层操作系统或者文件系统的影响。

使用文化通道

  下面通过使用文件通道,读取文件中的数据:

 1 public static void main(String[] args) throws Exception{
 2         File file = new File("D:/ceshi.txt");
 3         FileInputStream fis = new FileInputStream(file);
 4         FileChannel fc = fis.getChannel();
 5         ByteBuffer bb = ByteBuffer.allocate(35);
 6         fc.read(bb);
 7         bb.flip();
 8         while (bb.hasRemaining())
 9         {
10             System.out.print((char)bb.get());
11         }
12         bb.clear();
13         fc.close();
14     }

输出结果

Hello !
    FileChannel.

这是最简单的操作,前面讲过文件通道必须通过一个打开的RandomAccessFile、FileInputStream、FileOutputStream获取到,因此这里使用FileInputStream来获取FileChannel。接着只要使用read方法将内容读取到缓冲区内即可,缓冲区内有了数据,就可以使用前文对于缓冲区的操作读取数据了。

接着看一下使用文件通道写数据:

 1 public static void main(String[] args) throws Exception{
 2         File file = new File("D:/ceshi.txt");
 3         RandomAccessFile raf = new RandomAccessFile(file, "rw");
 4         FileChannel fc = raf.getChannel();
 5         ByteBuffer bb = ByteBuffer.allocate(60);
 6         String str = "Hello,FileChannel!";
 7         bb.put(str.getBytes());
 8         bb.flip();
 9         fc.write(bb);
10         bb.clear();
11         fc.close();
12     }

输出结果

这里使用了RandomAccessFile去获取FileChannel,然后操作其实差不多,write方法写ByteBuffer中的内容至文件中,注意写之前还是要先把ByteBuffer给flip一下。

可能有人觉得这种连续put的方法非常不方便,但是没有办法,之前已经提到过了:通道只能使用ByteBuffer

参考:https://www.cnblogs.com/xrq730/p/5080503.html

以上是关于通道(Channel)的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 协程Channel 通道 ① ( Channel#send 发送数据 | Channel#receive 接收数据 )

Kotlin 协程Channel 通道 ① ( Channel#send 发送数据 | Channel#receive 接收数据 )

Java新IO_通道(Channel)代码

[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础

Kotlin 协程Channel 通道 ② ( Channel 通道容量 | Channel 通道迭代 | 使用 iterator 迭代器进行迭代 | 使用 for in 循环进行迭代 )

Kotlin 协程Channel 通道 ② ( Channel 通道容量 | Channel 通道迭代 | 使用 iterator 迭代器进行迭代 | 使用 for in 循环进行迭代 )