Channel简介
在Java NIO中,主要有三大基本的组件:Buffer、Channel和Selector,前面两篇文章我们具体介绍了Selector和Buffer,老规矩,就让我们继续慢慢地揭开Channel的神秘面纱吧!
在Java NIO的世界中,Selector是中央控制器
,Buffer是承载数据的容器
,而Channel可以说是最基础的门面
,它是本地I/O设备、网络I/O的通信桥梁
,只有搭建了这座桥梁,数据才能被写入Buffer,连接才能被Selector控制,
Channel这座桥梁分别为本地I/O设备和网络I/O提供了以下实现,并且和Java IO体系的类是一一对应的:
- 网络I/O设备:
- DatagramChannel:读写UDP通信的数据,对应DatagramSocket类
- SocketChannel:读写TCP通信的数据,对应Socket类
- ServerSocketChannel:监听新的TCP连接,并且会创建一个可读写的SocketChannel,对应ServerSocket类
- 本地I/O设备:
- FileChannel:读写本地文件的数据,不支持Selector控制,对应File类
其类继承结构如下图:
- 从上图中我们可以看出前面讲述的四个类都是被定义为抽象的,这些类中只是声明了可操作的接口;主要是在不同的操作系统当中,其实际操作本地I/O和网络I/O在实现上会有根本性的差异,就拿Windows和Unix来说,两者的文件系统管理是不一致的(想了解三者I/O架构上的区别可参考Unix,Linux,Windows的IO架构)
- Channel接口实现了Closeable接口,并且本身还定义
isOpen()
方法,标识所有的Channel都是可以被主动关闭 InterruptibleChannel
接口声明了Channel是可以被中断的SelectableChannel
接口声明了Channel是可以被选择的(即支持Selector控制),而FileChannel是没有实现该接口的WritableByteChannel
和ReadableByteChannel
接口分别提供了写操作和读操作的API,且是基于Buffer的ScatteringByteChannel
和GatheringByteChannel
接口允许您委托操作系统来完成辛苦活:将读取到的数据分开存放到多个存储桶(bucket)或者将不同的数据区块合并成一个整体。这是一个巨大的成就,因为操作系统已经被高度优化来完成此类工作了。它节省了您来回移动数据的工作,也就避免了缓冲区拷贝和减少了您需要编写、调试的代码数量。其分别定义了write(ByteBuffer[] srcs, int offset, int length)
和read(ByteBuffer[] dsts, int offset, int length)
SeekableByteChannel
接口用于控制本地文件的positionNetworkChannel
接口标识了该Channel是属于网络I/O
ServerSocketChannel
让我们从最简单的ServerSocketChannel
来开始对socket通道类的讨论。以下是ServerSocketChannel
的完整 API:
public abstract class ServerSocketChannel extends AbstractSelectableChannel {
public static ServerSocketChannel open() throws IOException
public abstract ServerSocket socket();
public abstract SocketChannel accept() throws IOException;
//支持的SelectionKey类型,返回OP_ACCEPT
public final int validOps()
}
ServerSocketChannel
与ServerSocket
一样是socket监听器,其主要区别前者可以运行在非阻塞模式下运行;
// 创建一个ServerSocketChannel,将会关联一个未绑定的ServerSocket
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
ServerSocketChannel
的创建也是依赖底层操作系统实现,其实现类主要是ServerSocketChannelImpl
,我们来看看其构造方法
class ServerSocketChannelImpl extends ServerSocketChannel implements SelChImpl {
private final FileDescriptor fd;
private int fdVal;
// 这里忽略一些变量
.....
private int state = -1;
ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
// 创建一个文件操作符
this.fd = Net.serverSocket(true);
// 得到文件操作符是索引
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
}
新建一个ServerSocketChannelImpl
其本质是在底层操作系统创建了一个fd(即文件描述符),相当于建立了一个用于网络通信的通道,通过这个通道我们可以和外部网络进行通信;
当然上述操作只是抢占了一个通道,它是无法和外部通信的;我们知道,在实际网络交互中,必须通过端口才能通信,所以呢,下一步我们来看看如何绑定端口
ServerSocketChannel
貌似没有bind()
方法来绑定端口,上面我们提到它在创建时会新建一个fd,其本质对应了ServetSocket
对象,我们看ServerSocketChannel
的API能看到通过socket()
对象能获取到ServetSocket
,此时我们只要调用socket的bind()
方法绑定即可
ServerSocketChannel#socket#bind(InetSocketAddress)
ServerSocketChannel
最主要的作用就是用于监听TCP连接,其API中也有相应的accept()
方法来获取TCP连接
public SocketChannel accept() throws IOException {
// 忽略一些校验及无关代码
....
SocketChannelImpl var2 = null;
// var3的作用主要是说明当前的IO状态,主要有
/**
* EOF = -1;
* UNAVAILABLE = -2;
* INTERRUPTED = -3;
* UNSUPPORTED = -4;
* THROWN = -5;
* UNSUPPORTED_CASE = -6;
*/
int var3 = 0;
// 这里本质也是用fd来获取连接
FileDescriptor var4 = new FileDescriptor();
// 用来存储TCP连接的地址信息
InetSocketAddress[] var5 = new InetSocketAddress[1];
try {
// 这里设置了一个中断器,中断时会将连接关闭
this.begin();
// 这里当IO被中断时,会重新获取连接
do {
var3 = this.accept(this.fd, var4, var5);
} while(var3 == -3 && this.isOpen());
}finally {
// 当连接被关闭且accept失败时或抛出AsynchronousCloseException
this.end(var3 > 0);
// 验证连接是可用的
assert IOStatus.check(var3);
}
if (var3 < 1) {
return null;
} {
// 默认连接是阻塞的
IOUtil