通道(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 接收数据 )
[Go] 通过 17 个简短代码片段,切底弄懂 channel 基础
Kotlin 协程Channel 通道 ② ( Channel 通道容量 | Channel 通道迭代 | 使用 iterator 迭代器进行迭代 | 使用 for in 循环进行迭代 )
Kotlin 协程Channel 通道 ② ( Channel 通道容量 | Channel 通道迭代 | 使用 iterator 迭代器进行迭代 | 使用 for in 循环进行迭代 )