Java中Socket的用法(二) NioSocket的用法

从JDK1.4开始,Java新增了新的IO模式——nio(new IO),nio再底层采用了新的处理方式,极大地提高了IO的效率。我们使用的Socket也属于IO的一种,nio提供了相应的ServerSocketChannel和SocketChannel,他们分别对应原来的ServerSocket和Socket。

要理解NioSocket的使用必须先理解三个概念:

  • Buffer
  • Channel
  • Selector

为了让人理解,我们来看一个例子:

有个同学批发了很多方便面、饮料和别的日用品在宿舍卖,而且提供送过上门的服务,只要宿舍楼里有发消息买东西,他就送过去、收钱、返回,然后等下一个,这种模式就想到与普通Socket处理请求的模式。如果请求不是很多,这是没有问题的,当请求多起来的时候,这种模式就应付不过来了,如果现在电商网站也用这种配送方式,效果大家可想而知,所以电商网站必须采用新的配送模式,这就是现在的快递的模式(也许以后还会有更好的模式)。快递并不会一件一件的送,而是将多件货一起拿去送,而且在中转站都有专门的分拣员负责按配送范围把货物分给不同的送货员,这样效率就提高了很多。这种模式就相当于NioSocket的处理模式,Buffer就是所要送的货物,Channel就是送货员(或者开往某个区域的配货车),Selector就是中转站的分拣员

NioSocket使用中首先要创建ServerSocketChannel,然后注册Selector,接下来就可以用Selector接收请求并处理了。

ServerSocketChannel可以使用自己的静态工厂方法open创建。每个ServerSocketChannel对应一个ServerSocket,可以调用其socket方法来获取,不过直接使用获取到的ServerSocket来监听请求,那还是原来的处理模式,一般使用获取到的ServerSocket来绑定端口。ServerSocketChannel可以通过configureBlocking方法来设置是否采用阻塞模式,如果要采用非阻塞模式可以用configureBlocking(false)来设置,设置了非阻塞模式之后就可以调用register方法注册Selector来使用了(阻塞模式不可以使用Selector)。

Selector可以通过其静态工厂方法open创建,创建后通过Channel的register方法注册到ServerSocketChannel或者SocketChannel上,注册完之后Selector就可以通过select方法来等待请求,select方法有一个long类型的参数,代表最长等待时间,如果再这段时间里接收到了相应操作的请求则返回可以处理的请求的数量,否则在超时后返回0,程序继续往下走,如果传入的参数为0或者调用无参数的重载方法,select方法会采用阻塞模式直到有相应操作的请求出现。当接收到请求后Selector调用selectedKeys方法返回SelectionKey的集合。

SelectionKey保存了处理当前请求的Channel和Selector,并且提供了不同的操作类型。Channel在注册Selector的时候可以通过register的第二个参数选择特定的操作,这里的操作就是在SelectionKey中定义的,一共有4种

  • SelectionKey.OP_ACCEPT (请求操作)
  • SelectionKey.OP_CONNECT (连接操作)
  • SelectionKey.OP_READ (读操作)
  • SelectionKey.OP_WRITE (写操作)

只有在register方法中注册了相应的操作Selector才会关心相应类型操作的请求。

Channel和Selector并没有谁属于谁的关系,就好像一个分拣员可以为多个地区分拣货物而每个地区也可以有多个分拣员来分拣一样,他们就好像数据库里的多对多的关系,不过Selector这个分拣员分拣的更细,它可以按照不同的类型来分拣,分拣后的结果保存在SelectionKey中,可以分别通过SelectionKey的channel方法和selector方法来获取对应的Channel和Selector,而且还可以通过isAcceptable、isConnectable、isReadable和isWritable方法来判断是什么类型的操作。

NioSocket中服务端的处理过程可以分为5步:

  1. 创建ServerSocketChannel并设置相应参数
  2. 创建Selector并注册到ServerSocketChannel上
  3. 调用Selector的select方法等待请求
  4. Selector接收到请求后使用selectedKeys返回SelectionKey集合
  5. 使用SelectionKey获取到Channel、Selector和操作类型并进行具体操作

我们来写个例子,使用nio方式进行处理的NIOServer

/**
 * @Description: 使用NIO的服务端
 * @Author: gradual
 * @Date: 2018-10-24 上午11:25
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel,监听8080端口
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(8080));
        //设置为非阻塞模式
        ssc.configureBlocking(false);
        //为ssc注册选择器
        Selector selector = Selector.open();
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        //创建处理器
        Handler handler = new Handler(1024);
        while(true) {
            //等待请求,每次阻塞3s,超过3s后线程继续往下运行,如果传入0或者不传参数将一直阻塞
            if (selector.select(10000) == 0) {
                System.out.println("等待请求超时");
                continue;
            }
            System.out.println("处理请求");
            //获取待处理的SelectionKey
            Iterator keyIterator = selector.selectedKeys().iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                try {
                    //接收到连接请求时
                    if (key.isAcceptable()) {
                        handler.handleAccept(key);
                    }
                    //读数据
                    if (key.isReadable()) {
                        handler.handleRead(key);
                    }
                } catch (Exception e) {
                    keyIterator.remove();
                    continue;
                }
                //处理完后,从待处理的SelectionKey迭代器中移除当前所使用的key
                keyIterator.remove();
            }
        }
    }

    private static class Handler {
        private int bufferSize = 1024;
        private String localCharset = "UTF-8";

        public Handler() {}

        public Handler(int bufferSize) {
            this(bufferSize, null);
        }

        public Handler(String localCharset) {
            this(-1, localCharset);
        }

        Handler(int bufferSize, String localCharset) {
            if (bufferSize > 0) {
                this.bufferSize = bufferSize;
            }
            if (localCharset != null) {
                this.localCharset = localCharset;
            }
        }

        public void handleAccept(SelectionKey key) throws IOException {
            SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
            sc.configureBlocking(false);
            sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
        }

        public void handleRead(SelectionKey key) throws IOException {
            //获取channel
            SocketChannel sc = (SocketChannel) key.channel();
            //获取buffer并重置
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            //没有读到内容则关闭
            if (sc.read(buffer) == -1) {
                sc.close();
            } else {
                //将buffer转换为读状态
                buffer.flip();
                //将Buffer中接收到的值按localCharset格式编码后保存到receivedString
                String receivedString = Charset.forName(localCharset)
                                                .newDecoder()
                                                .decode(buffer)
                                                .toString();
                System.out.println("received from client : " + receivedString);

                //返回数据给客户端
                String sendString = "received data: " + receivedString;
                buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
                sc.write(buffer);
                //关闭socket
                sc.close();
            }
        }
    }
}

上面的处理过程都做了注释,main方法启动监听,当监听到请求时根据SelectionKey的状态交给内部类Handler进行处理,Handler可以通过重载的构造方法设置编码格式和每次读取数据的最大值。Handler处理过程中用到了Buffer,Buffer是java.nio包中的一个类,专门用于存储数据。

Buffer里有4个属性非常重要,他们分别是:

  • Capacity: 容量,也就是Buffer最多可以保存元素的数量,在创建时设置,使用过程中不可以改变
  • Limit:可以使用的上限,开始创建时limit和capacity的值相同,如果给limit设置一个值之后,limit就成了最大可以访问的值,其值不可以超过capacity。比如,一个Buffer的容量capacity为100,表示最多可以保存100个数据,但是现在只往里面写了20个数据然后要读取,在读取的时候limit就会设置为20。
  • Position:当前所操作元素所在的索引位置,position从0开始,随着get和put方法自动更新。
  • Mark:用来暂时保存position的值,position保存到mark后就可以修改并进行相关的操作,操作完后可以通过reset方法将mark的值恢复到position。比如,Buffer中的一共保存了20个数据,position的位置是10,现在想读取15到20之间的数据,这时就可以调用Buffer#mark()将当前的position保存到mark中,然后调用Buffer#position(15)将position指向第15个元素,这时就可以读取了,读取完之后调用Buffer#reset()就可以将position回复到10。Mark默认值为-1,而且其值必须小于position的值,如果调用Buffer#position(int newPosition)时传入newPosition比mark小则会将mark设置为-1。

这四个属性的大小关系是:mark<=position<=limit<=capacity

理解了这四个属性,Buffer就容易理解了。我们这里的NioServer用到clear和flip方法,clear的作用是重新初始化limit、position和mark三个属性,让limit=capacity、position=0、mark=-1。flip方法的作用是这样的:在保存数据时保存一个数据position加1,保存完了之后如果想读出来就需要将position的位置设置给limit,然后将position设置为0,这样就可以读取所保存的数据了,flip方法就是做这个用的,

这两个方法的代码如下:

public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
}

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}

NioSocket就写到这里,当然所举的例子只是为了更好理解NioSocket使用的方法,实际使用中一般都会采用多线程的方式来处理,不过使用单线程更容易理解。

 

你可能感兴趣的:(Java)