网络编程NIO:BIO和NIO详解,看不懂你打我( 三 )

选择器(Selector)在NIO程序中 , 可以用选择器Selector实现一个选择器处理多个通道 , 即一个线程处理多个连接 。 只要把通道注册到Selector上 , 就可以通过Selector来监测通道 , 如果通道有事件发生 , 便获取事件通道然后针对每个事件进行相应的处理 。 这样 , 只有在通道(连接)有真正的读/写事件发生时 , 才会进行读写操作 , 大大减少了系统开销 , 并且不必为每个连接创建单独线程 , 就不用去维护过多的线程 。
选择器的相关类在java.nio.channels包和其子包下 , 顶层类是Selector , 它是一个抽象类 , 它的常用方法有:
网络编程NIO:BIO和NIO详解,看不懂你打我文章插图
通道的注册在ServerSocketChannel和SocketChannel类里都有一个注册方法 register(Selector sel, int ops) , sel为要注册到的选择器 , ops为该通道监听的操作事件的类型 , 可以通过该方法将ServerSocketChannel或SocketChannel注册到目标选择器中 , 该方法会返回一个SelectionKey(真正实现类为SelectionKeyImpl)储存在注册的Selector的publicKeys集合属性里 。 SelectionKey储存了通道的事件类型和该注册的通道对象 , 可以通过SelectionKey.channel()方法获取SelectionKey对应的通道 。
网络编程NIO:BIO和NIO详解,看不懂你打我文章插图
每个注册到选择器的通道都需定义需进行的操作事件类型 , 通过查看SelectionKey类的属性可以知道操作事件的类型有4种:
public static final int OP_READ = 1 << 0; //读操作public static final int OP_WRITE = 1 << 2; //写操作public static final int OP_CONNECT = 1 << 3; //连接操作public static final int OP_ACCEPT = 1 << 4; //接收操作选择器的检查我们可以通过选择器的检查方法 , 如select()来得知发生事件的通道数量 , 当该数量大于为0时 , 即至少有一个通道发生了事件 , 就可以使用selectedKeys()方法来获取所有发生事件的通道对应的SelectionKey , 通过SelectionKey中的方法来判断对应通道中需处理的事件类型是什么 , 在根据事件做出相应的处理 。
public final boolean isReadable() { //判断是否是读操作return (readyOps() }public final boolean isWritable() { //判断是否是写操作return (readyOps() }public final boolean isConnectable() { //判断是否是连接操作return (readyOps() }public final boolean isAcceptable() { //判断是否是接收操作return (readyOps() }
网络编程NIO:BIO和NIO详解,看不懂你打我文章插图
NIO实现简单的聊天群//服务器端public class GroupChatSever {private final static int PORT = 6666;//监听端口private Selector selector;//选择器private ServerSocketChannel serverSocketChannel;public GroupChatSever(){try{selector = Selector.open();//开启选择器serverSocketChannel = ServerSocketChannel.open();//开启通道serverSocketChannel.configureBlocking(false);//将通道设为非阻塞状态serverSocketChannel.socket().bind(new InetSocketAddress(PORT));//通道绑定监听端口serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//将通道注册到选择器上 , 事件类型为接收listen();}catch (IOException e){e.printStackTrace();}}//对端口进行监听public void listen(){try {while (true){//检查注册通道是否有事件发生 , 检查时长为2秒int count = selector.select(2000);if (count > 0){//如果注册通道有事件发生则进行处理//获取所有发生事件的通道对应的SelectionKeyIterator keyIterator = selector.selectedKeys().iterator();while (keyIterator.hasNext()){SelectionKey key = keyIterator.next();if (key.isAcceptable()){//判断该key对应的通道是否需进行接收操作//虽然accept()方法是阻塞的 , 但是因为对通道进行过判断 ,//可以确定是有客户端连接的 , 所以此时调用accept并不会阻塞SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);//接收后 , 将获取的客户端通道注册到选择器上 , 事件类型为读socketChannel.register(selector, SelectionKey.OP_READ);System.out.println(socketChannel.getRemoteAddress() + "上线!");}if (key.isReadable()){//判断该key对应的通道是否需进行读操作readFromClient(key);}//注意当处理完一个通道key时 , 需将它从迭代器中移除keyIterator.remove();}}}}catch (IOException e){e.printStackTrace();}}/*** 读取客户端发来的消息* @param key 需读取的通道对应的SelectionKey*/public void readFromClient(SelectionKey key){SocketChannel socketChannel = null;try{//通过SelectionKey获取对应通道socketChannel = (SocketChannel)key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int read = socketChannel.read(byteBuffer);if (read > 0){String message = new String(byteBuffer.array());System.out.println("客户端: " + message);sendToOtherClient(message, socketChannel);}}catch (IOException e){//这里做了简化 , 将所有异常都当做是客户端断开连接触发的异常 , 实际项目中请不要这样做try{System.out.println(socketChannel.getRemoteAddress() + "下线");key.cancel();//将该SelectionKey撤销socketChannel.close();//再关闭对应通道}catch (IOException e2){e2.printStackTrace();}}}/*** 将客户端发送的消息转发到其他客户端* @param message 转发的消息* @param from 发送消息的客户端通道* @throws IOException*/public void sendToOtherClient(String message, SocketChannel from) throws IOException{System.out.println("消息转发中......");for (SelectionKey key : selector.keys()){//遍历选择器中所有SelectionKeyChannel channel = key.channel();//根据SelectionKey获取对应通道//排除掉发送消息的通道 , 将消息写入到其他客户端通道if (channel instanceof SocketChannelByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());socketChannel.write(byteBuffer);}}}public static void main(String[] args) {GroupChatSever groupChatSever = new GroupChatSever();}}