前言
这两天比较空闲,准备研究下netty,看的掘金小册Netty 入门与实战:仿写微信 IM 即时通讯系统,文中开始就用java nio做演示,看完后意识到对java nio还不够理解,因此决定先研究下java的nio.
java nio
关于io模式可以参考我写过的另一篇文章IO模式和IO多路复用,java中的nio对于不同平台实现也是不同的,参见java 和netty epoll实现
重要组成
- Channel 通道,和阻塞io中的stream很类似,只不过Channel是双向的
- Buffer 缓冲区,用来缓冲数据
- Selectors 选择器 参见IO模式和IO多路复用中nio原理部分
三者的简要说明
这部分主要参考Java NIO Tutorial
Channel
主要有下面四种Channel
- FileChannel 对应文件IO
- DatagramChannel 对应UDP网络IO
- SocketChannel 对应TCP网络IO
- ServerSocketChannel 服务端使用,监听TCP连接
Buffer 缓冲区
这部分基本复制自理解Java NIO.
缓冲区对象有四个基本属性:
- 容量Capacity:缓冲区能容纳的数据元素的最大数量,在缓冲区创建时设定,无法更改
- 上界Limit:缓冲区的第一个不能被读或写的元素的索引
- 位置Position:下一个要被读或写的元素的索引
- 标记Mark:备忘位置,调用mark()来设定mark=position,调用reset()设定position=mark
这四个属性总是遵循这样的关系:0<=mark<=position<=limit<=capacity,Buffer对象中很多方法都是对这几个属性做设置,如flip
,rewind
,clear
,reset
等等. 下图是新创建的容量为10的缓冲区逻辑视图
buffer.put((byte)'H').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');
复制代码
现在缓冲区满了,我们必须将其清空。我们想把这个缓冲区传递给一个通道,以使内容能被全部写出,但现在执行get()无疑会取出未定义的数据。我们必须将 posistion设为0,然后通道就会从正确的位置开始读了,但读到哪算读完了呢?这正是limit引入的原因,它指明缓冲区有效内容的未端。这个操作在缓冲区中叫做翻转对应的方法是buffer.flip()
flip方法的源码如下:
/** * Flips this buffer. The limit is set to the current position and then the position is set to zero. If the mark is defined then it is discarded. After a sequence of channel-read or put operations, invoke this method to prepare for a sequence of channel-write or relative get operations.
*/
public final Buffer flip() { limit = position; position = 0; mark = -1; return this;}
复制代码
随便提一下rewind操作,它一般用来重复读取buffer,与flip相似,但是不影响limit。
Selectors
一图胜千言
Channel和Buffer
Channel和Buffer的关系
Buffer和Channel的关联如下图
Channel和Buffer的基本操作
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
复制代码
从上例可以看出nio中读取写入的一般逻辑:
- 打开一个Channel对象c
- 创建Buffer对象b(,如果是写入到Channel,这里会对Buffer进行写入)
- 从c中循环
读取到
到b,或者从b写入到
到c
一般从Channel写入到Buffer,调用的是Channel.read(Buffer b),而从Buffer写入到Channel调用的是Channel.write(Buffer b),笔者之前这个地方经常搞混,实质在于这里的读写是站在Channel的角度来说的
Selector详解
在阻塞IO中,一个线程对应一个连接,对于成千上万的连接显然是hold不住(另外线程切换的消耗也是很大的).nio中的Selector就是为了解决这个问题出现的:一个Selector对应并处理多个连接,也就是单个线程对应多个连接.
# 注册到selector
Selector selector = Selector.open();
//使用selector时,channel必须设置为非阻塞IO,由于FileChannel只能是阻塞的,所以也肯定不能为FileChannel
channel.configureBlocking(false);
//将该连接注册到Selector **1**
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
# selector死循环等待事件到来
while(true) {
//如果没有可*处理*的channel,继续循环 **3**
int readyChannels = selector.select();
if(readyChannels == 0) continue;
//拿到可处理channel列表 **2**
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//这里根据事件类型来分别处理
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
// **4*
keyIterator.remove();
}
}
复制代码
-
注册channel到selector时,需要选定该channel感兴趣的事件类型,这里可以使用
或操作 |
- connect 连接事件
- accept 连接接受事件
- read 读事件就绪
- write 写事件就绪
-
SelectionKey 这个对象有以下几个属性
- interestOps() 该key感兴趣的事件类型列表
- readyOps 准备就绪的事件类型列表
- attach(theObject) 附加Obj到这个key上,可以是用来标识这个key或者存储其他信息
-
selector.select()返回就绪的channel数量,一共有三种形式
- select() 等待直到至少一个channel就绪才会返回
- select(long timeout),等待指定时间或至少一个channel就绪才会返回
- selectNow() 非阻塞,立刻返回,实现上复用了2 ,即select(0)
结合IO模式和IO多路复用关于阶段的说明,对于1,2第一阶段是阻塞的,对于3第一阶段是非阻塞的 selector.selectedKeys()返回所有就绪的key,可以根据事件类型对这些key进行处理
-
selector.selectedKeys()返回内部变量publicSelectedKeys,
但是在遍历publicSelectedKeys中的key后,这些key不会自动移除,这里需要自己手动删除,如果一个key k1对应的事件已经被处理了但是没有手动移除,那么下次调用selector.selectedKeys()它还会被返回(在k1再次变为准备状态这段时间,就相当于对一个不可能触发的key做无用遍历)
总结
本文简单介绍了java nio的基本概念,Channel,Buffer,Selector等的作用.写完这篇也扫清了自己对Selector的疑问.希望能对大家有点帮助.