选择器结合SelectableChannel实现了非阻塞的效果。
选择器实现了I/O通道的多路复用。
5.1 选择器与I/O多路复用
Selector选择器是NIO技术中的核心组件,可以将通道注册进选择器中,其主要作用就是使用1个线程来对多个通道中的已就绪通道进行选择,然后就可以对选择的通道进行数据处理,属于1对多的关系,也就是使用1个线程来操作多个通道,这种机制在NIO技术中称为“I/0多路复用”。它的优势是可以节省CPU资源,CPU不需要在不同的线程间进行上下文的切换。线程上下文切换是非常耗时的动作,减少切换对设计高性能服务器具有很重要的意义。
学习I/O多路复用时一定要明白,就是在使用I/O多路复用时,这个线程不是以for循环的方式来判断每个通
道是否有数据要进行处理,而是以操作系统底层作为“通知器”,来“通知JVM中的线程”哪个通道中的数据要
进行处理,这点一定要注意。当不使用for循环的方式来进行判断,而是使用通知的方式时,这就大大提高了
程序运行的效率,不会出现无限期的for循环迭代空运行了。
5.2 核心类Selector SelectionKey SelectableChannel的关系
Selector类是抽象类,它是SelectableChannel对象的多路复用器。这句话的意思是只有SelectableChannel通道对象才能被Selector.java选择器所复用?原因是只有SelectableChannel类才具有register(Selector sel,int ops)方法,该方法是将当前的SelectableChannel通道注册到指定的选择器中,参数sel也说明了这个问题
SelectionKey类的作用是一个标识,这个标识代表SelectableChannel类已经向Selector类注册了。
5.3 通道类AbstractInterruptibleChannel与接口InterruptibleChannel
AbstractInterruptibleChannel类实现了InterruptibleChannel接口,该接口的主要作用,是使通道能以异步的方式进行关闭与中断。
如果通道实现了asynchronously和closeable特性,那么,当一个线程在一个能被中断的通道上出现了阻塞状态,其他线程调用这个通道的close()方法时,这个呈阻塞状态的线程将收到AsynchronousCloseException异常。
如果通道实现了asynchronously和closeable,并且还实现了interruptible特性,那么,当一个线程在一个能被通道上出现了阻塞状态,其他线程调用这个阻塞线程的interrupt()方法时,通道将被关闭,这个阻塞的线程将收到ClosedByInterruptException异常,这个阻塞线程的状态一直是中断状态。
AbstractInterruptibleChannel类是抽象类,主要作用是提供了一个可以被中断的通道基本实现类。此类封装了能使通道实现异步关闭和中断所需要的最低级别的机制。在调用有可能无限期阻塞的I/0操作的之前和之后,通道类必须分别调用begin()和end()方法。
5. 4 通道类SelectableChannel的介绍
AbstractInterruptibleChannel类的子类就包含抽象类SelectableChannel和FileChannel。
在SelectableChannel在选择器里注册后,通道在注销之前将一直保持注册状态。需要注意的是,不能直接注销通道,而是通过调用SelectionKey类的cancel()方法显示的取消,这将在选择器的下一个选择select()操作期间去注销通道。无论是通过调用通道的close()方法,还是中断阻塞与该通道上I/O操作中的线程来关闭该通道,都会隐式的取消该通道的所有SelectionKey.
SelectableChannel在多线程环境下是安全的。
SelectableChannel要么处于阻塞模式,要么处于非阻塞模式。
5.5 通道类AbstractSelectableChannel的介绍
SelectableChannel类的子类包含抽象类AbstractSelectableChannel.
抽象类AbstractSelectableChannel是可选择通道的基本实现类。此类定义了处理通道注册、注销和关闭机制的各种方法。它会维持此通道的当前阻塞模式及其当前的选择键集SelectionKey.它执行实现SelectableChannel规范所需的所有同步。此类中所定义的抽象保护方法的实现不必与同一操作中使用的其它线程同步。
5.6 通道类ServerSocketChannel与接口NetworkChannel的介绍
抽象类AbstractSelectableChannel的子类包含抽象类ServerSocketChannel
从继承的结构信息来看,抽象类ServerSocketChannel实现了1个新的接口NetworkChannel。
ServerSocketChannel类是针对面向流的侦听套接字的可选择通道。ServerSocketChannel不是侦听网络套接字的完整抽象,必须通过调用socket()方法所获得的关联ServerSocket对象来完成对套接字选项的绑定和操作。不可能为任意的已有ServerSocket创建通道,也不可能指定与ServerSocketChannel关联的ServerSocket所使用的SocketImpl对象。
通过调用此类的open()方法创建ServerSocketChannel。可通过调用相关ServerSocket的某个bind()方法来绑定ServerSocketChannel.
NetworkChannel:
一个NetworkChannel代表连接到Socket的网络通道。实现此接口的通道就是网络套接字通道。bind()方法用于将套接字绑定到本地地址,getLocalAddress()方法返回套接字绑定到的地址,setOption()和getOption()方法分别用于设置和查询套接字选项。
5.7 ServerSocketChannel类、Selector和SelectionKey的使用
5.7.1 获得ServerSocketChannel与ServerSocket socket对象
ServerSocketChannel类是抽象的,不能直接实例化,API提供了public static ServerSocketChannel open()方法来创建ServerSocketChannel类的实例。open()方法是静态的,作用是打开服务器套接字通道。新通道的套接字最初是未绑定的;可以接受连接之前,必须通过它的某个套接字的bind()方法将其绑定到具体的地址。
通过调用open()方法创建ServerSocketChannel类的实例后,可以调用它的public abstract ServerSocket socket()方法来返回ServerSocket类的对象,然后与客户端套接字进行通信。socket()方法的作用是获取与此通道关联的服务器套接字ServerSocket类的对象。
public final void close()方法的作用是关闭此通道
5.7.2 执行绑定操作
serverSocket.bind(new InetSocketAddress(“localhost”,8888));将ServerSocket类绑定到指定的地址,而ServerSocketChannel类也有bind()方法,该方法public final ServerSocketChannel bind(SocketAddress local)的作用是将通道的通道的套接字绑定到本地地址并侦听连接。
public class ServerSocketChannelTest {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//如果使用ServerSocketChannel进行了绑定
//那么就不再使用ServerScoket进行bind()绑定
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
ServerSocket serverSocket = serverSocketChannel.socket();
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
char [] charArray = new char[1024];
int readLength = inputStreamReader.read(charArray);
while(readLength!=-1) {
String newString = new String(charArray,0,readLength);
System.out.println(newString);
readLength = inputStreamReader.read(charArray);
}
inputStreamReader.close();
inputStream.close();
socket.close();
serverSocket.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.7.3 执行绑定操作与设置backlog
public abstract ServerSocketChannel bind(SocketAddress local,int backlog)方法的作用是将通道的套接字绑定到本地地址并侦听连接,通过使用参数backlog来限制客户端连接的数量。
5.7.4 阻塞与非阻塞以及accept()方法的使用效果
public abstract SocketChannel accept()方法的作用是接受此通道套接字的连接。如果此通道处于非阻塞模式,那么在不存在挂起的连接时,此方法直接返回null.否则,在新的连接可用或者发生IO错误之前会无限期的阻塞它。无论此通道的阻塞阻塞模式如何,此方法返回的套接字通道将处于阻塞模式。
如何切换ServerSocketChannel通道的阻塞与非阻塞的执行模式呢?调用ServerSocketChannel的public final SelectableChannel configureBlocking(boolean block)方法即可。
public final SelectableChannel configureBlocking(boolean block)方法的作用是调整此通道的阻塞模式,传入true是阻塞模式,传入false是非阻塞模式。
先看阻塞模式:
public class BlockServerTest {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
System.out.println(serverSocketChannel.isBlocking());
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
System.out.println("begin"+System.currentTimeMillis());
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("end"+System.currentTimeMillis());
socketChannel.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
true
begin1558946407394
非阻塞模式:
public class NotBlockServerTest {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
System.out.println(serverSocketChannel.isBlocking());
//调用非阻塞方法
serverSocketChannel.configureBlocking(false);
System.out.println(serverSocketChannel.isBlocking());
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
System.out.println("begin"+System.currentTimeMillis());
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("end"+System.currentTimeMillis());
socketChannel.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
true
false
begin1558946557057
end1558946557057
Exception in thread "main" java.lang.NullPointerException
at cn.yu.serversocketchannel.NotBlockServerTest.main(NotBlockServerTest.java:23)
在非阻塞模式下,accept()方法在没有客户端连接时,返回null值。
5.7.5 SocketChannel accept()方法结合ByteBuffer来获取数据
public class ServerSocketChannel2 {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer byteBuffer = ByteBuffer.allocate(2);
int readLength = socketChannel.read(byteBuffer);
while(readLength!=-1) {
String newString = new String(byteBuffer.array(),"utf-8");
System.out.println(newString);
byteBuffer.flip();
readLength = socketChannel.read(byteBuffer);
}
socketChannel.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
----------------------------
public class ServerSocketChannelClient2 {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8888);
OutputStream outStream = socket.getOutputStream();
String s = new String("我是一个Hero".getBytes(),"utf-8");
outStream.write(s.getBytes());
outStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.7.6 获得Selector对象
上文一直在使用ServerSocketChannel类进行Socket服务端和客户端的通信,并没有涉及高性能的IO多路复用。
Selector:
由于Selector类是抽象的,不能直接实例化,因此需要调用open()方法获得Selector对象。
public static Selector open()方法的作用是打开一个选择器,使SelectableChannel能将自身注册到这个选择器上。
5.7.7 执行注册操作与获得SelectionKey对象
SelectableChannel类的public final SelectionKey register(Selector sel,int ops)方法的作用是向给定的选择器注册此通道,返回一个选择键(SelectionKey)
参数sel代表要向其注册此通道的选择器,参数ops代表register()方法的返回值SelectionKey的可操作集,操作集是在SelectionKey类中以常量的形式进行提供的。
public class SelectorTest {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//必须将ServerSocketChanel设置成非阻塞的
//不然会出现java.io.channels.IllegalBlockingModeException异常
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress("localhost",8888));
//核心代码
Selector selector = Selector.open();
SelectionKey key = serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("selector="+selector);
System.out.println("key="+key);
serverSocket.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
运行结果:
selector=sun.nio.ch.WindowsSelectorImpl@6ae40994
key=channel=sun.nio.ch.ServerSocketChannelImpl[/127.0.0.1:8888], selector=sun.nio.ch.WindowsSelectorImpl@6ae40994, interestOps=16, readyOps=0
5.7.8 判断注册状态
SelectableChannel 类的public final boolean isRegistered()方法的作用是判断此通道当前是否已向任何选择器进行了注册。
5.7.9 使用configureBlock(false)方法解决异常
如果想把通道注册到选择器中,就必须将通道设置成非阻塞模式。
5.7.10 获得阻塞锁对象
public final Object blockingLock()方法的作用是获取其configureBlocking()和register()方法实现同步的对象,防止重复注册。
5.7.11 获得支持的SocketOption列表
Set<SocketOption<?> supportedOptions()方法的作用是返回通道支持的Socket option.
public abstract <T> ServerSocketChannel setOption(SocketOption<T> name,T value)方法的作用是设置Socket Option值。
T getOptions(SocketOption <T> name) 方法的作用是获取Socket option值
5.7.12 获得SocketAddress对象
public abstract SocketAddress getLocalAddress()方法的作用是获取绑定的SocketAddress对象
5.7.13 根据Selector找到对应的SelectKey
public final SelectionKey keyFor(Selector sel)方法的作用是获取通道向给定选择器注册的SelectionKey.
同一个SelectableChannel通道可以注册到不同的选择器对象,然后返回新创建的SelectionKey对象,可以使用public final SelectionKey keyFor(Selector sel)方法来取得当前通道注册在指定选择器上的SelectionKey对象。
5.7.14 获得SelectorProvider对象
public final SelectorProvider provider()方法 的作用是返回创建此通道的SelectorProvider
SelectorProvider类的作用是用于选择器和可选择通道的服务提供者类。选择器提供者的实现类是SelectorProvider类的一个子类,它具有零参数的构造方法,并实现了抽象方法。给定的对Java虚拟机的调用维护了单个系统级的默认提供者实例,它由provider()方法返回。
在第一次调用该方法时,将查找一下指定的默认提供者。系统级的默认提供者由DatagramChannel、pipe、Selector、ServerSocketChannel和SocketChannel类的静态open()方法使用。
public class SelectorProviderTest {
public static void main(String[] args) {
SelectorProvider provider1 = SelectorProvider.provider();
System.out.println(provider1);
ServerSocketChannel serverSocketChannel = null;
try {
serverSocketChannel = serverSocketChannel.open();
SelectorProvider provider2 = serverSocketChannel.provider();
System.out.println(provider2);
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
sun.nio.ch.WindowsSelectorProvider@5b2133b1
sun.nio.ch.WindowsSelectorProvider@5b2133b1
5.7.15 通道注册与选择器
1.相同的通道注册到不同的选择器,返回的SelectionKey不是同一个对象
public class SelectorTest2 {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
serverSocketChannel.configureBlocking(false);
Selector selector1 = Selector.open();
Selector selector2 = Selector.open();
SelectionKey selectionKey1 = serverSocketChannel.register(selector1,SelectionKey.OP_ACCEPT);
System.out.println("selctionKey1="+selectionKey1.hashCode());
SelectionKey selectionKey2 = serverSocketChannel.register(selector2,SelectionKey.OP_ACCEPT);
System.out.println("selctionKey2="+selectionKey2.hashCode());
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
selctionKey1=999661724
selctionKey2=1793329556
从上述输出的结果来看,证实了结论:相同的通道可以注册到不同的选择器,返回的Selectionkey不是同一个对象。
2.不同的通道注册到相同的选择器,返回不是同一个对象
3.不同的通道注册到不同的选择器,返回不是同一个对象。
4.相同的通道注册到相同的选择器,返回的SelectionKey是同一个对象
5.7.16 返回此通道所支持的操作
public final int validOps()方法的作用是返回一个操作集,标识此通道所支持的操作。因为服务器套接字通道仅支持接受新的连接,所以此方法返回SelectionKey.OP_ACCEPT.
public class TestMethod_validOps {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketChannel socketChannel = SocketChannel.open();
int value1 = serverSocketChannel.validOps();
int value2 = socketChannel.validOps();
System.out.println("value1"+value1);
System.out.println("value2"+value2);
System.out.println();
System.out.println("ServerSocketChannel只支持OP_ACCEPT,值为0表示支持");
System.out.println(SelectionKey.OP_ACCEPT&~serverSocketChannel.validOps());
System.out.println(SelectionKey.OP_CONNECT&~serverSocketChannel.validOps());
System.out.println(SelectionKey.OP_READ&~serverSocketChannel.validOps());
System.out.println(SelectionKey.OP_WRITE&~serverSocketChannel.validOps());
System.out.println("SocketChannel 支持 OP_CONNECT OP_READ OP_WRITE,值为0表示支持");
System.out.println(SelectionKey.OP_ACCEPT&~socketChannel.validOps());
System.out.println(SelectionKey.OP_CONNECT&~socketChannel.validOps());
System.out.println(SelectionKey.OP_READ&~socketChannel.validOps());
System.out.println(SelectionKey.OP_WRITE&~socketChannel.validOps());
socketChannel.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
value116
value213
ServerSocketChannel只支持OP_ACCEPT,值为0表示支持
0
8
1
4
SocketChannel 支持 OP_CONNECT OP_READ OP_WRITE,值为0表示支持
16
0
0
0
5.7.17 执行Connect连接操作
public abstract boolean connect(SocketAddress remote)方法的作用是连接到远程通道的Socket.
如果此通道处于非阻塞模式,则此方法的调用将启动非阻塞连接操作。
如果通道呈阻塞模式,则立即发起连接;如果呈非阻塞模式,则不是立即发起连接,而是在随后的某个时间才发起连接。
如果连接是立即建立的,说明通道是阻塞模式,当连接成功时,则此方法返回true,连接失败出现异常。
如果连接不是立即建立的,说明通道是非阻塞模式,则此方法返回false,并且以后必须通过调用finishConnect()方法来验证连接是否完成。
5.7.18 判断此通道上是否正在进行连接操作
public abstract boolean isConnectionPending()方法的作用是判断此通道上是否正在进行连接操作。
5.7.19 完成套接字通道的连接过程
public abstract boolean finishConnect()方法的作用是完成套接字通道的连接过程。通过将套接字通道置于非阻塞模式,然后调用其connect()方法来发起非阻塞连接操作。
5.7.20 类FileChannel中的long transferTo(position,count,WritableByteChannel)的方法的使用
方法transferTo()的作用是试图读取此通道文件中给定position处开始的count个字节,并将其写入目标通道中,但是此方法不一定传输所有请求的字节,是否传输取决于通道的性质与状态。
5.7.21 方法public static SocketChannel open(SocketAddress remote)与SocketOption的执行顺序
如果先调用public static SocketChannel open(SocketAddress remote)方法,然后设置SocketOption,则不会出现预期的效果,因为在public static SocketChannel open(SocketAddress remote)方法中已经自动执行了connect()方法。
5.7.22 传输大文件
public class BigFileServer {
public static void main(String[] args) {
try {
ServerSocketChannel channel1 = ServerSocketChannel.open();
channel1.configureBlocking(false);
channel1.bind(new InetSocketAddress("localhost",8888));
Selector selector = Selector.open();
channel1.register(selector, SelectionKey.OP_ACCEPT);
boolean isRun = true;
while(isRun == true) {
selector.select();
Set<SelectionKey> set = selector.selectedKeys();
Iterator<SelectionKey> iterator = set.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()) {
SocketChannel socketChannel = channel1.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_WRITE);
}
if(key.isWritable()) {
SocketChannel socketChannel = (SocketChannel)key.channel();
FileInputStream file = new FileInputStream("");
FileChannel fileChannel = file.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(524288000);
while(fileChannel.position()<fileChannel.size()) {
fileChannel.read(byteBuffer);
byteBuffer.flip();
while(byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
byteBuffer.clear();
System.out.println(fileChannel.position()+" "+fileChannel.size());
}
System.out.println("结束写操作");
socketChannel.close();
}
}
}
channel1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
----------------------------------------
public class BigFileClient {
public static void main(String[] args) {
try {
SocketChannel channel1 = SocketChannel.open();
channel1.configureBlocking(false);
channel1.connect(new InetSocketAddress("localhost",8088));
Selector selector = Selector.open();
channel1.register(selector, SelectionKey.OP_CONNECT);
boolean isRun = true;
while(isRun == true) {
System.out.println("begin selector");
if(channel1.isOpen() == true) {
selector.select();
System.out.println("end selector");
Set<SelectionKey> set= selector.selectedKeys();
Iterator<SelectionKey> iterator = set.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if(key.isConnectable()) {
while(!channel1.finishConnect()) {
}
channel1.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(50000);
int readLength = channel1.read(byteBuffer);
byteBuffer.flip();
long count = 0;
while(readLength!=-1) {
count = count+readLength;
System.out.println("count="+count+"readLength="+readLength);
readLength = channel1.read(byteBuffer);
byteBuffer.clear();
}
System.out.println("读取结束");
channel1.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.7.23 read和write方法是非阻塞的
执行代码configureBlocking(false)代表当前的IO为非阻塞的,NIO就是同步非阻塞模型,所以read和write方法也呈现此特性。
public class WriteNoBlockServerTest {
public static void main(String[] args) {
try {
ServerSocketChannel channel1 = ServerSocketChannel.open();
channel1.configureBlocking(false);
channel1.bind(new InetSocketAddress("localhost",7077));
Selector selector = Selector.open();
channel1.register(selector, SelectionKey.OP_ACCEPT);
selector.select();
Set<SelectionKey> selectedKeysSet = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeysSet.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(Integer.MAX_VALUE/10);
System.out.println("byteBuffer.limit()="+byteBuffer.limit());
System.out.println("begin"+System.currentTimeMillis());
socketChannel.write(byteBuffer);
System.out.println("end"+System.currentTimeMillis()+"byteBuffer.position()="+byteBuffer.position());
}
channel1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
----------------------
public class WriteNoBlockClientTest {
public static void main(String[] args) {
try {
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("localhost",7077));
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
byteBuffer.limit()=214748364
begin1558962281698
end1558962281887byteBuffer.position()=131071
方法write并没有将全部的214748634字节传输到对端,只传输了131071个字节,说明write具有非阻塞特性。
5.8 Selector类的使用
Selector类的主要作用是作为SelectableChannel对象的多路复用器。
可通过调用Selector类的open()方法创建选择器,该方法将使用系统的默认SelectorProvider创建新的选择器。也可通过调用自定义选择器提供者的openSelector()方法来创建选择器。在通过选择器的close()方法关闭选择器之前,选择器一直保持打开状态。
通过SelectionKey对象来表示SelectableChannel到选择器的注册。选择器维护了3种SelectionKey-Set(选择键集):
1.键集:包含的键表示当前通道到此选择器的注册,也就是通过某个通道的register()方法注册该通道时,所带来的影响是想选择器的键集中添加了一个键。此集合由keys()方法返回。键集本身是不可直接修改的。
2.已选择键集:在首先调用select()方法选择操作期间,检测每个键的通道是否已经至少为该键的相互操作集所标识的一个操作准备就绪,然后调用selectedKeys()方法返回已就绪键的集合。已选择键集始终是键集的一个子集。
3.已取消键集:标识已被取消但其通道尚未注销的键的集合。不可直接方法此集合。已取消键集始终是键集的一个子集。在select()方法选择操作期间,从键集中移除已取消的键。
选择器本身可由多个并发线程安全使用,但是其键集并非如此。
阻塞在select()或select(long)方法中的某个线程可能被其他线程以下列3种方式之一中断:
1.通过调用选择器的wakeup()方法
2.通过调用选择器的close()方法。
3.再通过调用已阻塞线程的interrupt()方法的情况下,将设置其中中断状态并且将调用该选择器的wakeup()方法。
一般情况下,选择器的键和已选择键集由多个并发线程使用是不安全的。如果这样的线程可以直接修改这些键之一,那么应该通过对该键集本身进行同步来控制访问。
5.8.1 验证public abstract int select()方法具有阻塞性
public abstract int select()方法的作用是选择一组键,其相应的通道已为I/O操作准备就绪。此方法执行处于阻塞模式的选择操作。仅在至少选择一个通道、调用此选择器的wakeup()方法,或者当前的线程已中断后,此方法才返回。返回值代表添加到就绪操作集的键的数目,该数目可能为0,为0代表就绪操作集中的内容并没有添加新的键,保持内容不变。
5.8.2 Select()方法不阻塞的原因和解决办法
public class TestMethod_Select_Server {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
serverSocketChannel.configureBlocking(false);
Selector selector1 = Selector.open();
SelectionKey selectionKey1 = serverSocketChannel.register(selector1, SelectionKey.OP_ACCEPT);
boolean isRun = true;
while(isRun == true){
int keyCount = selector1.select();
Set<SelectionKey> set1 = selector1.keys();
Set<SelectionKey> set2 = selector1.selectedKeys();
System.out.println("keyCount="+keyCount);
System.out.println("set1 size="+set1.size());
System.out.println("set2 size="+set2.size());
System.out.println();
}
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
--------------------------
public class TestMethod_Select_client {
public static void main(String[] args) {
try {
Socket socket = new Socket("localhost",8888);
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
keyCount=1
set1 size=1
set2 size=1
keyCount=0
set1 size=1
set2 size=1
keyCount=0
set1 size=1
set2 size=1
keyCount=0
set1 size=1
set2 size=1
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
出现死循环的原因是在客户端连接服务器时,服务端中的通道对accept事件并未处理,导致accept事件一直存在,也就是select()方法一直检测有准备好的通道要对accept事件进行处理,但一直未处理,就会出现死循环。解决方法:将accept()事件消化处理。
public class TestMethod_Select_Server {
public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost",8888));
serverSocketChannel.configureBlocking(false);
Selector selector1 = Selector.open();
SelectionKey selectionKey1 = serverSocketChannel.register(selector1, SelectionKey.OP_ACCEPT);
boolean isRun = true;
while(isRun == true){
int keyCount = selector1.select();
Set<SelectionKey> set1 = selector1.keys();
Set<SelectionKey> set2 = selector1.selectedKeys();
System.out.println("keyCount="+keyCount);
System.out.println("set1 size="+set1.size());
System.out.println("set2 size="+set2.size());
System.out.println();
Iterator<SelectionKey> iterator = set2.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
ServerSocketChannel channel =(ServerSocketChannel)key.channel();
channel.accept();//使用方法accept()将事件处理掉
}
}
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.8.3 出现重复消费的情况
如果两个不同的通道注册到相同的选择器,极易出现重复消费的情况。
造成这样的原因是变量set2在每一次循环中使用的是底层提供的的同一对象,一直在往set2里面添加已就绪的SelectionKey,一个是关联7777端口的SelectionKey,另一个是关联8888端口的SelectionKey.在这期间,从未从set2中删除SelectionKey,因此,set2的size值为2,在使用while(iterator.hasNext())对set2循环2此,就导致了重复消费。解决重复消费问题的方法就是remove()方法删除set2中处理过后的SelectionKey.
5.8.4 验证产生的set1和set2关联的各自对象一直是同一个
从程序结果看:
Set <SeletionKey> set1 = selector1.keys();//键集数目
Set <SeletionKey> set2 = selector1.selectedKeys();//已选中键集数目
变量set1的hash值一直不变。
变量set2的hash值一直不变。
5.8.5 int selector.select()方法返回值的含义
int selector.select()方法返回值的含义就是已更新其准备就绪操作集的键的数目。
5.9 SelectionKey类的使用
Selection类表示SelectableChannel在选择器中的注册的标记。
在每次向选择器注册通道时,就会创建一个选择键。
选择键包含两个集,是表示为整数值的操作集,其中每一位都表示该键通道所支持的一类可选择通道操作。
1.interest集。确定了下一次调用某个选择器的select()方法时,将测试哪类操作的准备就绪信息。
2.ready集。标识了这样一类操作,即某个键的选择器检测到该键的通道已为此类操作准备就绪。
5.9.1 判断是否允许连接SelectableChannel对象
public final boolean isAcceptable()方法的作用是测试此键的通道是否已准备好接受新的套接字连接。
public final boolean isConnectable()方法的作用是测试此键的通道是否已完成其套接字连接操作。
public abstract SelectableChannel channel() 方法的作用是返回为之创建此键的通道。
5.10 DatagramChannel类的使用
DatagramChannel类是针对面向DatagramSocket的可选择通道。DatagramChannel不是DatagramSocket的完整抽象。
5.10.1 使用DatagramChannel类实现UDP通信
public class Server {
public static void main(String[] args) {
try {
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress("localhost",8888));
Selector selector = Selector.open();
SelectionKey selectionKey1 = channel.register(selector, SelectionKey.OP_READ);
boolean isRun = true;
while(isRun = true) {
selector.select();
Set<SelectionKey> selectedKeysSet = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeysSet.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isReadable()) {
channel = (DatagramChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1000);
channel.receive(buffer);
System.out.println(new String(buffer.array(),0,buffer.position()));
}
iterator.remove();
}
}
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
----------------------------------------------
public class Client {
public static void main(String[] args) {
try {
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey selectionKey1 = channel.register(selector, SelectionKey.OP_WRITE);
int keyCount = selector.select();
Set<SelectionKey> selectedKeySet = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeySet.iterator();
while(iterator.hasNext()) {
SelectionKey key = iterator.next();
if(key.isWritable()) {
ByteBuffer buffer = ByteBuffer.wrap("我是客户端!".getBytes());
channel.send(buffer, new InetSocketAddress("localhost",8888));
channel.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.10.2 连接操作
public abstract DatagramChannel connect(SocketAddress remote)方法的作用是连接此通道的套接字。
5.11 Pipe.SinkChannel和Pipe.SourceChannel类的使用
Pipe.SinkChannel类表示Pipe的可写入结尾的通道
Pipe.SourceChannel类表示Pipe的可读取结尾的通道。
Pipe类实现单向管道传送的通道类。
管道由一对通道组成:一个可写入的sink通道和一个可读取的source通道。一旦将某些字节写入接收器通道,就可以按照与写入时完全相同的顺序从源通道中读取这些字节。
public class PipeTest {
public static void main(String[] args) throws InterruptedException {
try {
Pipe pipe = Pipe.open();
SinkChannel sinkChannel = pipe.sink();
SourceChannel sourceChannel = pipe.source();
Thread t1 = new Thread() {
public void run() {
try {
Thread.sleep(1000);
for(int i = 0;i<5;i++) {
sinkChannel.write(ByteBuffer.wrap(("我是1号"+(i+1)+"\r\n").getBytes()));
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
};
t1.start();
Thread t2 = new Thread() {
public void run() {
try {
Thread.sleep(1000);
for(int i = 0;i<5;i++) {
sinkChannel.write(ByteBuffer.wrap(("我是2号"+(i+1)+"\r\n").getBytes()));
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
};
t2.start();
Thread.sleep(3000);
sinkChannel.close();
ByteBuffer readBuffer = ByteBuffer.allocate(1000);
int readLength = sourceChannel.read(readBuffer);
while(readLength!=-1) {
System.out.println(new String(readBuffer.array(),0,readLength));
readLength = sourceChannel.read(readBuffer);
}
sourceChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.12 SelectorProvider类的使用
SelectorProvider是用于选择器和可选择通道的服务提供者类。选择器提供者实现类是SelectorProvider类的一个子类,它具有零参数的构造参数,并实现了一下指定的抽象方法。给定的对Java虚拟机的调用维护了单个系统级的默认提供者实例,它由provider()方法返回。
public class SelectorProviderTest {
public static void main(String[] args) throws IOException {
SelectorProvider provider = SelectorProvider.provider();
System.out.println("provider="+provider.getClass().getName());
Selector selector = provider.openSelector();
DatagramChannel datagramChannel1 = provider.openDatagramChannel();
DatagramChannel datagramChannel2 = provider.openDatagramChannel(StandardProtocolFamily.INET);
DatagramChannel datagramChannel3 = provider.openDatagramChannel(StandardProtocolFamily.INET6);
Pipe pipe = provider.openPipe();
ServerSocketChannel serverSocketChannel = provider.openServerSocketChannel();
SocketChannel socketChannel = provider.openSocketChannel();
Channel channel =provider.inheritedChannel();
System.out.println("openSelector()="+selector.getClass().getName());
System.out.println("openDatagramChannel()="+datagramChannel1.getClass().getName());
System.out.println("openDatagramChannel2(StandardProtocolFamily.INET)="+datagramChannel2.getClass().getName());
System.out.println("openDatagramChannel3(StandardProtocolFamily.INET6)="+datagramChannel3.getClass().getName());
System.out.println("openPipe()="+pipe.getClass().getName());
System.out.println("openServerSocketChannel()="+socketChannel.getClass().getName());
System.out.println("inheritedChannel()="+channel);
}
}
运行结果:
provider=sun.nio.ch.WindowsSelectorProvider
openSelector()=sun.nio.ch.WindowsSelectorImpl
openDatagramChannel()=sun.nio.ch.DatagramChannelImpl
openDatagramChannel2(StandardProtocolFamily.INET)=sun.nio.ch.DatagramChannelImpl
openDatagramChannel3(StandardProtocolFamily.INET6)=sun.nio.ch.DatagramChannelImpl
openPipe()=sun.nio.ch.PipeImpl
openServerSocketChannel()=sun.nio.ch.SocketChannelImpl
inheritedChannel()=null