理解Java的NIO
同步与阻塞
同步和异步是针对应用程序和内核的交互而言的。
-
同步:执行一个操作之后,进程触发IO操作并等待(阻塞)或者轮询的去查看IO的操作(非阻塞)是否完成,等待结果,然后才继续执行后续的操作。
-
异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。
非阻塞是针对于进程在访问数据的时候,根据IO口的状态返回不同的状态值。阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
-
阻塞:进程给CPU传达一个任务之后,一直等待CPU处理完成,然后才执行后面的操作。
-
非阻塞:进程给CPU传达任我后,继续处理后续的操作,隔断时间再来询问之前的操作是否完成。(轮询)
同步异步是结果,阻塞非阻塞是手段。
Java IO的各种流是同步阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 当然,他还有一个更加重要的特性是,多路复用IO。
Java NIO的同步非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
之前使用futrueModel的就是类似异步IO模型。异步一定是非阻塞的。
概述
Java的NIO有三个核心部分:Channels、Buffers、Selectors
Channel 和 Buffer
Channel有四个类型:
文件 | UDP | TCP-client | TCP-server |
---|---|---|---|
FileChannel | DatagramChannel | SocketChannel | ServerSocketChannel |
Buffer有八个实现类型,对应八种基本数据类型除了布尔型的七种,以及一个映射类型:
ByteBuffer | IntBuffer | CharIntBuffer | FloatIntBuffer |
---|---|---|---|
ShortBuffer | LongIntBuffer | DoubleIntBuffer | MappedByteBuffer |
可以将一个Channel看作一个流,而buffer和channel联合起来就是一个带有缓冲区域的流。和stream不同的是,stream的方向是单向的,但是buffer和channel之间的数据传输是双向的,数据既可以从channel到buffer,也可以从buffer到channel。
Selector
Selector是一个管理器,可以管理多个channel。在线程中使用Selector,将Channel注册到Selector中,在channel中有事件就绪,就会将调用select方法,获取事件,响应事件。
Channel
Channel 是一个接口,其常用的实现类有下面四个:
文件 | UDP | TCP-client | TCP-server |
---|---|---|---|
FileChannel | DatagramChannel | SocketChannel | ServerSocketChannel |
一个Channel的使用实例。
package com.xzj;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
// @ author :zjxu time:2019/1/8
public class Main {
final static String PATH = "/Users/thisxzj/IDEAProject/Revise/NIO/src/index.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile(PATH, "rw");
FileChannel fileChannel = file.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
int length = fileChannel.read(byteBuffer);
if (length != -1) {
System.out.println("length = " + length );
byteBuffer.flip(); //反转buffer
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
//byteBuffer.rewind();
//while (byteBuffer.hasRemaining()) {
// System.out.print((char) byteBuffer.get());
//}
byteBuffer.clear();
}
fileChannel.close();
}
}
运行结果:
length = 12
Hello World!
Buffer
Buffer是一个抽象类,他有有八个实现类型,对应八种基本数据类型除了布尔型的七种,以及一个映射类型:
ByteBuffer | IntBuffer | CharIntBuffer | FloatIntBuffer |
---|---|---|---|
ShortBuffer | LongIntBuffer | DoubleIntBuffer | MappedByteBuffer |
buffer的基本用法
- 写入数据到Buffer
- 调用
flip()
方法 - 从Buffer中读取数据
- 调用
clear()
方法或者compact()
方法
buffer的内部
buffer内部是一个对应类型的数组,这个数字有四个index,这四个index在数据的存取过程中有自己作用。属性解释:
- capacity:缓冲区数字的总长度。
- pasition:下一个要操作的数据元素的位置。
- limit:缓冲区不可操作的下一个位置的位置。
- mark:记录position前一个位置,default:-1(也就是不存在)。
使用方法:
- 创建方法:ByteBuffer.allocate(n) //创建一个长度为n的Byte的缓冲区。这个时候的 capacity 和 limit的大小都是数组的长度;position的大小是0,数组的首端。
- 在写入了数据之后,position会变为数组没有存储数据的位置的一个位置。
- 使用byteBuffer.flip方法,limit = position,position = 0,然后就可以正确的读取这鞋数据,并且将数据发送出去。
- 然后使用 byteBuffer.clear方法,将会到刚刚创建的状态。
- 关于 mark 方法 ,使用mark方法的时候,mark会记录position - 1 的数据大小。当再次使用reset的时候,position将不会还原,会回到mark的值。
几个方法:
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
······
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
······
public Buffer rewind() {
position = 0;
mark = -1;
return this;
}
······
public final Buffer mark() {
mark = position;
return this;
}
······
public Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
flip方法
反转buffer,反转缓冲区。首先将限制设置为当前位置,然后将位置设置为 0。通常情况下,在从写入状态转向读取状态的时候调用flip方法。
clear、compact方法
清除方法,清除整个缓冲区,相当于是再次初始化。和这个类似的还有一个compact方法。clear的作用是将全部缓冲区域清除,如果缓冲区中的数据全部使用结束,那么使用clear是没有问题的。
如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么就使用compact()方法。compact()方法将所有未读的数据拷贝搬移Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。也就是将没有使用过的数据搬移到初始端,然后,在原先数据的后面接着写入新的数据。
rewind 方法
无论是在读还是写的模式下,都从头开始。如果是读的模式,使用这个方法,写入新的数据将会覆盖原来数据的。如果是读取的话,将会重复的将原先读过的数据在读一遍。
如将channel中的注释打开,那么执行的结果就是:
length = 12
Hello World!Hello World!
这就体现了在读取的时候rewind的作用。
mark()与reset()方法
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
例如,先mark一下,然后经过读或者是写,之后,使用reset可以将position返回到使用mark的时候的那个mark值。但是flip、clear、compact、rewind、reset都会将mark标记回归负一,也就是取消掉。在使用reset的时候,必须要有一个mark,否则会exception。
buffer的使用
创建
使用Buffer抽象类的实现类的静态方法来创建,例如:
//创建一个对应的Buffer的对象,其最大容量为128。
XXXBuffer xxxBuffer = XXXBuffer.allocate(128);
写数据
从channel中:
int readLength = inChannel.read(xxxBuffer);
使用channel的read方法,将channel中的数据传入xxxBuffer中。
使用put方法:
xxxBuffer.put(new byte[]{' ', 'x', ' ', 'z', ' ', 'j', ' ', '!'});
使用buffer对象的put方法,将put方法的参数,传入的到buffer中。这个参数可以是这个buffer的类型的值、数组。还可以是另一个同类型的buffer,这个参数的本质也是一个数组。
读取数据
从Buffer读取数据到Channel:
int writeLength = inChannel.write(xxxBuffer);
使用get方法获取buffer中的数据:
xxxBuffer.get(); //获取的是一个xxx类型的数据
其他
可以使用equare 和 compareTo来比较两个buffer。
多buffer操作
使用多个buffer,对应一个channel。
scatter
使用buffer的数组从channel中获取信息的时候,将会按照数组中buffer的顺序,将channel中的信息,保存在buffer中。
package com.xzj;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
// @ author :zjxu time:2019/1/8
public class Main {
final static String PATH = "/Users/thisxzj/IDEAProject/Revise/NIO/src/index.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile(PATH, "rw");
FileChannel fileChannel = file.getChannel();
ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(100);
ByteBuffer[] byteBuffer = new ByteBuffer[]{byteBuffer1, byteBuffer2};
long length = fileChannel.read(byteBuffer);
if (length != -1) {
System.out.println("length = " + length);
byteBuffer2.flip(); //反转buffer
while (byteBuffer2.hasRemaining()) {
System.out.print((char) byteBuffer2.get() );
}
byteBuffer2.clear();
}
fileChannel.close();
}
}
index.txt中保存的文本:
Hello World!Hello World!
程序的输出:
length = 24
d!Hello World!
很明显,第一个长度为10的数组中,保存了前面十个字符。后面长度为100的保存了后面的十四位。在移动下一个buffer前,必须填满当前的buffer。
gather
同样的,channel在获取数字的时候,可以有这样的数组的机制。但是需要注意的是,只有position和limit之间的数字会被填充进去。所以和前面的一样,需要使用filp更改来将数据刷新进去。在满的时候,或者是将队尾的空格输入进去的时候,使用rewind。
package com.xzj;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
// @ author :zjxu time:2019/1/8
public class Main {
final static String PATH = "/Users/thisxzj/IDEAProject/Revise/NIO/src/index.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile(PATH, "rw");
FileChannel fileChannel = file.getChannel();
ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(10);
byteBuffer1.put("0123456789".getBytes());
byteBuffer1.rewind();
byteBuffer2.put("ABCDEFGHIJ".getBytes());
byteBuffer2.rewind();
ByteBuffer[] byteBuffer = new ByteBuffer[]{byteBuffer1, byteBuffer2};
long length = fileChannel.write(byteBuffer);
System.out.println(length);
fileChannel.close();
}
}
index.txt中的文本:
0123456789ABCDEFGHIJ
控制台信息:
20
通道间通信
transferFrom
将from中文本信息复制到to中,如果to中文本信息长度比from长,多余的一部分保留。
package com.xzj;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
// @ author :zjxu time:2019/1/8
public class Main {
final static String FROM = "/Users/thisxzj/IDEAProject/Revise/NIO/src/fromIndex.txt";
final static String TO = "/Users/thisxzj/IDEAProject/Revise/NIO/src/toIndex.txt";
public static void main(String[] args) throws IOException {
RandomAccessFile fromFile = new RandomAccessFile(FROM, "rw");
RandomAccessFile toFile = new RandomAccessFile(TO, "rw");
FileChannel fromChannel = fromFile.getChannel();
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
fromChannel.close();
toChannel.close();
}
}
运行结果:
fromIndex:abcdbfghijkrmnopqrstuvwxyz
toIndex:1234567890123456789012345678901234567890
运行之后,from中文本不变,to中的文本变化如下:
toIndex:abcdbfghijkrmnopqrstuvwxyz78901234567890
transferTo
将上面的程序的第21行改为:
fromChannel.transferTo(position, count, toChannel);
其余的逻辑,方法都不变。
SelectableChannel
和selector配套使用的是selectable接口的实现类,通常是网络接口。所以,NIO是和网络紧密相关的一个IO机制。
- 有关UDP协议的:DatagramChannel。
- 有关SCTP协议的:SctpChannel、SctpMultiChannel、SctpServerChannel。
- 有关TCP协议的:ServerSocketChannel、SocketChannel。
- 有关管道的:SinkChannel、SourceChannel这两个抽象类定义在java.nio.channels.Pipe类中。
- Socket和ServerSocket是位于java.net下的两个类。
- SocketChannel和ServerSocketChannel是位于java.nio.channels下的两个类。
连接关系:
- 服务器必须先建立ServerSocket或者ServerSocketChannel 来等待客户端的连接。
- 客户端必须建立相对应的Socket或者SocketChannel来与服务器建立连接。
- 服务器接受到客户端的连接,再生成一个Socket或者SocketChannel与此客户端通信。
SocketChannel
打开方法
//客户端
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
连接方法
//客户端
socketChannel.configureBlocking(false); //非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
写入方法
//客户端
ByteBuffer writeBuffer = ByteBuffer.allocate(64);
writeBuffer.clear();
writeBuffer.put((new Date().toString()).getBytes());
writeBuffer = writeBuffer.flip();
socketChannel.write(writeBuffer);
读取方法
//客户端
readBuff.clear();
int length = socketChannel.read(readBuff);
readBuff.flip();
System.out.println("received: " +
new String(readBuff.array()).substring(0, length));
关闭方法
//客户端
socketChannel.close();
ServerSocketChannel
打开方法
//服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
ssc.configureBlocking(false);
连接方法
//服务端
ssc.configureBlocking(false); //非阻塞模式
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
监听连接
通过 ServerSocketChannel.accept() 方法监听新进来的连接。当 accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。
通常不会仅仅只监听一个连接,在while循环中调用 accept()方法。如下面的例子:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
当然,也可以在while循环中使用除了true以外的其它退出准则。
读取方法
//使用一个新建的Socket连接,在这个连接上使用read和write
readBuff.clear();
int length = socketChannel.read(readBuff);
readBuff.flip();
System.out.println("received: " +
new String(readBuff.array()).substring(0, length));
关闭方法
//服务端
serverSocketChannel.close();
TCP的使用示例
WebServer
package com.xzj;
// @ author :zjxu time:2019/1/9
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class WebServer {
public static void main(String[] args) throws IOException {
//创建一个channel并且设定为非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
serverSocketChannel.configureBlocking(false);
//创建selector
Selector selector = Selector.open();
//将channel和selector关联起来,并且指定感兴趣的事件是 Accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//创建读写buffer
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(128);
writeBuff.put("enter:".getBytes());
writeBuff.flip();
while (true) {
int nReady = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
//轮询
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
// 创建新的连接,并且把连接注册到selector上,而且
// 声明这个channel只对读操作感兴趣
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
int length = socketChannel.read(readBuff);
readBuff.flip();
System.out.println("received: " + new String(readBuff.array()).substring(0, length-1));
key.interestOps(SelectionKey.OP_WRITE);
} else if (key.isWritable()) {
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuff);
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
WebClient
package com.xzj;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Date;
// @ author :zjxu time:2019/1/9
public class WebClient {
public static void main(String[] args) throws IOException {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
//创建读写buffer
ByteBuffer writeBuffer = ByteBuffer.allocate(64);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
while (true) {
writeBuffer.clear();
writeBuffer.put((new Date().toString()).getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
readBuffer.clear();
socketChannel.read(readBuffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
DatagramChannel
打开方法
DatagramChannel channel = DatagramChannel.open();
连接方法
channel.socket().bind(new InetSocketAddress(8000));
收发方法
ByteBuffer sendBuffer = ByteBuffer.allocate(64);
ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
//接收方法
receiveBuffer.clear();
channel.receive(receiveBuffer);
String string =
new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date());
//发送方法
sendBuffer.clear();
sendBuffer.put(string.getBytes());
sendBuffer.flip();
channel.send(sendBuffer,new InetSocketAddress(8001));
可以将DatagramChannel“连接”到网络中的特定地址的。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。
这里有个例子:
`channel.connect(``new` `InetSocketAddress(``"jenkov.com"``, ``80``));`
当连接后,也可以使用read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证。这里有几个例子:
int bytesRead = channel.read(buf);
int bytesWrite = channel.write(buf);
UDP的使用示例
package com.xzj;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Date;
import java.util.Iterator;
import java.util.Scanner;
// @ author :zjxu time:2019/1/9
public class DatagramTest {
public void send() throws IOException {
DatagramChannel dChannel = DatagramChannel.open();
dChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String string = scanner.next();
buffer.put((new Date().toString()+">>"+string).getBytes());
buffer.flip();
dChannel.send(buffer, new InetSocketAddress("127.0.0.1", 8989));
buffer.clear();
}
dChannel.close();
}
public void receive() throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.bind(new InetSocketAddress(8989));
Selector selector = Selector.open();
datagramChannel.register(selector, SelectionKey.OP_READ);
while(selector.select()>0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = (SelectionKey) iterator.next();
if (selectionKey.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
datagramChannel.receive(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
}
}
iterator.remove();
}
}
}
Pipe
JavaNIO中的pipe是两个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
创建管道
Pipe pipe = Pipe.open();
写入
SinkChannel sinkChannel = pipe.sink();
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
String string =
new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date());
byteBuffer.clear();
byteBuffer.put(string.getBytes());
byteBuffer.flip();
sinkChannel.write(byteBuffer);
读取
SourceChannel sourceChannel = pipe.source();
ByteBuffer readBuffer = ByteBuffer.allocate(64);
readBuffer.clear();
int length = sourceChannel.read(readBuffer);
System.out.println(new String(readBuffer.array()).substring(0, length - 1));
sourceChannel.close();
Pipe使用示例
package com.xzj;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Pipe;
import java.nio.channels.Pipe.SinkChannel;
import java.nio.channels.Pipe.SourceChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
// @ author :zjxu time:2019/1/9
public class Test {
public static void main(String[] args) throws IOException {
Pipe pipe = Pipe.open();
SinkChannel sinkChannel = pipe.sink();
ByteBuffer writeBuffer = ByteBuffer.allocate(64);
String string =
new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date()) + "\n";
writeBuffer.clear();
writeBuffer.put(string.getBytes());
writeBuffer.flip();
sinkChannel.write(writeBuffer);
sinkChannel.close();
SourceChannel sourceChannel = pipe.source();
ByteBuffer readBuffer = ByteBuffer.allocate(64);
readBuffer.clear();
int length = sourceChannel.read(readBuffer);
System.out.println(new String(readBuffer.array()).substring(0, length - 1));
sourceChannel.close();
}
}
运行结果:
20190109 12:34:14
Selector
使用scelector的好处是,只需要更少的线程来维护通道。,可以使用以恶搞线程处理所有的通道。
创建方法
Selector selector = Selector.open();
//selector.isOpen();判断是否打开
//selector.close();关闭
注册通道
channel.configureBlocking(false);
SelectionKey readKey = channel.register(selector,SelectionKey.OP_READ);
第二个参数,表示监听的动作:
有四个类型:
事件 | 符号 | key常量 |
---|---|---|
连接就绪 | Connect | SelectionKey.OP_CONNECT |
接收就绪 | Accept | SelectionKey.OP_ACCEPT |
读就绪 | Read | SelectionKey.OP_READ |
写就绪 | Write | SelectionKey.OP_WRITE |
Key
使用key常量将selector和channel中的动作关联起来,并返回一个SelectionKey对象。只要是channel向selector中注册了事件,selector就会跟踪检测这个事件是否发生。这些的key对象就是检测这些事件的句柄。
获取keys
//所有注册到selector的key
Set<SelectionKey> keys = selector.keys();
//相关事件被捕获的key
Set<SelectionKey> selectedKeys = selector.selectedKeys();
如果关闭了与SelectionKey对象关联的Channel对象,或者调用了SelectionKey对象的cancel方法,这个SelectionKey对象就会被加入到cancelled-keys集合中,表示这个SelectionKey对象已经被取消。
cancelled-keys没有对应的方法被获取。
interest集合
int interestSet = key.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
用来检测对那些事件感兴趣。其中:
操作类型 | 值 | 描述 |
---|---|---|
OP_READ | 1 << 0 | 读操作 |
OP_WRITE | 1 << 2 | 写操作 |
OP_CONNECT | 1 << 3 | 连接socket操作 |
OP_ACCEPT | 1 << 4 | 接受socket操作 |
使用interestOps();方法获取的是一个多位二进制数,通过逻辑操作,可以判断是不是在key上有对应的操作。
ready集合
int readySet = key.readyOps();
boolean isAcceptable = (readySet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isConnectable = (readySet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
boolean isReadable = (readySet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
boolean isWritable = (readySet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
和上面的获取机制一模一样。此外还可以使用直接获取的方式,获取ready状态。
key.isAcceptable();
key.isConnectable();
key.isReadable();
key.isWritable();
附加对象
通过key生成的channel和seletor:
Channel keyChannel = key.channel();
Selector keySelector = key.selector();
将一个对象添加到key上:
StringBuilder stringBuilder = new StringBuilder("xzj");
//添加上去
key.attach(stringBuilder);
//从key获取
StringBuilder getter = (StringBuilder) key.attachment();
select()
向一个selector中注册多个channel之后,可以使用selector的select方法,该方法的返回值是一个int型,表示的是通道就绪的数量。使用的select()方法是阻塞等待至少一个通道准备就绪,其中可以向其中添加一个最长等待时间,也可以使用selectNow()来表示立即返回。
如下:
selector.select();
selector.select(1000);
selector.selectNow();
遍历
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator selectedKeysIterator = selectedKeys.iterator();
while (selectedKeysIterator.hasNext()) {
SelectionKey key = (SelectionKey) selectedKeysIterator.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
}
//将刚刚访问过的元素删除
selectedKeysIterator.remove();
}
weakUp方法
在某一个线程在使用select()方法被阻塞,而无法返回的时候,在同一个select对象上使用weakUp()方法,会使阻塞的select方法返回。