Java NIO:Selector详解以及在网络编程中的应用

概述

Selector <---> Channel <---> Buffer,三者都支持双向的数据传递

在NIO网络编程中,Selector&Channel&Buffer三者的关系是十分紧密的,Buffer从Channel中读写,Channel注册在Selector中。在以往的网络编程中,通常都是通过创建一个线程来维护一个socket通讯,在业务量较小时,是可以很好的完成工作的,但是一旦客户端增多,创建的线程也随之增多,对硬件的开销是非常大的。这时候NIO的Selector就体现出了价值:

Selector在Java NIO中可以检测到一个或者多个Channel,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个Channel,从而管理多个网络连接。这样的单个线程管理管理多个Channel可以极大的减少线程间切换的开销

示例

package com.leolee.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Set;

/**
 * @ClassName SelectorTest
 * @Description: NIO socket编程demo,用于理解Selector
 * @Author LeoLee
 * @Date 2020/9/22
 * @Version V1.0
 **/
public class SelectorTest {

    //端口数组,用于和多个客户端建立连接后分配端口
    int[] ports = null;

    //起始端口
    int tempPort = 5000;

    //构造器初始化 端口数组ports,并从起始端口tempPort开始分配[size]个端口号
    public SelectorTest (int size) {
        this.ports = new int[size];
        for (int i = 0; i < size; i++) {
            this.ports[i] = tempPort + i;
        }
    }


    public void selectorTest () throws IOException {

        Selector selector = Selector.open();

        //windows系统下是sun.nio.ch.WindowsSelectorProvider,如果是linux系统,则是KQueueSelectorProvider
        //由于Selector.open()的源码涉及 sun 包下的代码,是非开源代码,具体实现不得而知
//        System.out.println(SelectorProvider.provider().getClass());//sun.nio.ch.WindowsSelectorProvider
//        System.out.println(sun.nio.ch.DefaultSelectorProvider.create().getClass());//sun.nio.ch.WindowsSelectorProvider

        for (int i = 0; i < ports.length; i++) {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);//非阻塞模式
            ServerSocket serverSocket = serverSocketChannel.socket();
            //绑定端口
            InetSocketAddress address = new InetSocketAddress("127.0.0.1", ports[i]);
            serverSocket.bind(address);

            //注册selector
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("[step1]监听端口:" + ports[i]);
        }

        //阻塞代码,始终监听来自客户端的连接请求
        while (true) {
            //获取我们“感兴趣的时间”已经准备好的通道,上面代码感兴趣的是SelectionKey.OP_ACCEPT,这里获取的就是SelectionKey.OP_ACCEPT事情类型准备好的通道
            //number为该“感兴趣的事件“的通道数量
            int number = selector.select();
            System.out.println("number:" + number);
            if (number > 0) {
                //由于selector中会有多个通道同时准备好,所以这里selector.selectedKeys()返回的是一个set集合
                Set<SelectionKey> selectionKeys =  selector.selectedKeys();
                System.out.println("[step2]selectionKeys:" + selectionKeys);
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    //由于我们”感兴趣“的是SelectionKey.OP_ACCEPT,所以如下判断
                    if (selectionKey.isAcceptable()) {
                        //selectionKey.channel()返回是ServerSocketChannel的爷爷类SelectableChannel,所以做强制类型转换
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);//非阻塞模式

                        //重点重点重点重点重点重点重点重点重点重点
                        //将接收到的channel同样也注册到Selector上,Selector<--->channel<--->buffer,三者是双向的
                        socketChannel.register(selector, SelectionKey.OP_READ);//这时候”感兴趣的事件“是读操作,因为要接收客户端的数据了
                        //重点重点重点重点重点重点重点重点重点重点
                        //当以上代码执行完毕后,已经建立了服务端与客户端的socket连接,这时候就要移除Set集合中的selectionKey,以免之后重复创建该selectionKey对应的通道
                        iterator.remove();

                        System.out.println("[step3]成功获取客户端的连接:" + socketChannel);
                    } else if (selectionKey.isReadable()) {//判断selectionKey可读状态
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        int byteRead = 0;
                        while (true) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
                            byteBuffer.clear();
                            int read = socketChannel.read(byteBuffer);
                            //判断数据是否读完
                            if (read <= 0) {
                                socketChannel.register(selector, SelectionKey.OP_READ);
                                break;
                            }

                            //写回数据,这里为了简单:读取什么数据,就写回什么数据
                            byteBuffer.flip();
                            socketChannel.write(byteBuffer);
                            byteRead += read;
                        }
                        System.out.println("[step4]读取:" + byteRead + ",来自与:" + socketChannel);

                        //重点重点重点重点重点重点重点重点重点重点
                        //当以上代码执行完毕后,已经完成了对某一个已经“读准备好”通道的读写操作,这时候就要移除Set集合中的selectionKey,以免之后重复读写该selectionKey对应的通道
                        iterator.remove();
                    }
                }
            }
        }
    }


    /*
     * 功能描述: <br> 使用nc命令连接服务端:nc 127.0.0.1 5000
     * 〈〉
     * @Param: [args]
     * @Return: void
     * @Author: LeoLee
     * @Date: 2020/9/23 12:59
     */
    public static void main(String[] args) throws IOException {

        SelectorTest selectorTest = new SelectorTest(5);
        selectorTest.selectorTest();
    }
}

基本思路:

  1. 通过构造方法定义5个监听端口
  2. 创建Selector,并将已经初始化完成的ServerSocketChannel注册在Selector上,Selector开始监听Channel
  3. 构造while死循环(阻塞代码),始终监听来自客户端的请求,通过判断Selector注册通道之后返回的SelectionKey集合中每一个SelectionKey状态,来处理不同的操作(建立连接、读、写)

”感兴趣的事件“是一个需要特别注意的概念:

主要分为四种,在SelectionKey类中定义为了四个常量

  1. Connect
  2. Accept
  3. Read
  4. Write

Channel向Selector注册的时候都要给定一个 int 类型的 参数 [ops],代表了监听“感兴趣”的通道类型,说人话就是之后返回的SelectionKey的状态。可以是复合状态:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

这是通过一种事件的形式,当某些事件完成后,标识了Channel的状态随之改变,所以SelectionKey所代表的Channel的状态也发生了改变,通过区分判断不同的状态,我们知道应该对这些Channel做对应的操作(建立连接、读、写)。

运行

运行服务端demo,服务端监听了5个端口

[step1]监听端口:5000
[step1]监听端口:5001
[step1]监听端口:5002
[step1]监听端口:5003
[step1]监听端口:5004

使用nc命令来连接服务端

服务端5000端口监听到客户端建立连接的请求并建立连接:

[step2]selectionKeys:[sun.nio.ch.SelectionKeyImpl@179d3b25]
[step3]成功获取客户端的连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]

客户端发送消息到服务端,当服务端收到消息后,将消息内容原封不动的返回给了客户端

[step2]selectionKeys:[sun.nio.ch.SelectionKeyImpl@20ad9418]
[step4]读取:13,来自与:java.nio.channels.SocketChannel[connected local=/127.0.0.1:5000 remote=/127.0.0.1:56614]

PS.

可以尝试多建立几个客户端,连接不同的端口来感受一下代码思路

需要代码的来这里拿嗷:demo项目地址

猜你喜欢

转载自blog.csdn.net/qq_25805331/article/details/108757585