NIO的骚操作二、SocketChannel

本篇继https://blog.csdn.net/HouXinLin_CSDN/article/details/104263694之后。

Java NIO SocketChannel是连接TCP网络套接字的通道,和Socket一样,SocketChannel通常用于客户端请求,可以使用以下方式创建,但是要注意的是:
SocketChannel在阻塞情况下,调用connect如果连接成功,则返回true,否则阻塞或抛出异常。
在非阻塞模式下,如果立即连接成功,则返回true,如果没有立即完成,则返回false,但是返回false后的几秒内,有可能连接成功,这时候读写还是会发生错误,后续需要调用finishConnect来确保完成连接,如果调用finishConnect时已经连接成功则返回true,否则尝试连接,失败则抛出IO异常。

两种模式切换可以调用configureBlocking来完成,传入false代表非阻塞模式。

SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

Java NIO ServerSocketChannel是一个服务器通道,可以侦听请求的TCP连接,和ServerSocket一样。

SocketChannel和ServerSocketChannel又不同于旧版,旧版是阻塞式的,而NIO中可以是非阻塞式,也就是说,旧版的ServerSocket在调用accept会阻塞当前线程,直到有连接请求,而NIO在非阻塞式下,调用accept如果没有请求,则立即返回null,那怎么拿到客户端的连接?答案是Selector。通过把指定事件注册到Selector中,当有指定事件发生时,Selector会有所反应,根据这个事件在做出对应的响应逻辑。

这里的事件包括如下图,在SelectionKey中定义,有读、写、连接,同意连接事件。
在这里插入图片描述

代码示例

首先是服务端,编写一个ServerSocketChannel需要以下几步:
1.创建Selector。
2.创建ServerSocketChannel,并设置非阻塞模式。
3.将感兴趣的事件注册到Selector。
4.轮询取出Selector要发生的事件,并做出响应。

1、2、3步代码如下:

Selector selector =Selector.open();
ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8000));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

接着轮询,通过select方法取得Selector中已经准备好的IO操作数量,注意这个方法是阻塞式的。并通过selectedKeys取得所有SelectionKey,SelectionKey表示SelectableChannel 在 Selector 中的注册的标记,无论是ServerSocketChannel还是SocketChannel都继承SelectableChannel,当二者其中指定事件发生时,判断对象是哪个事件发生,做出相应逻辑。

在serverSocketChannel调用register后,返回一个SelectionKey对象,也就是说,同意连接的事件会发生在这个对象,为了验证,在其中服务accept事件下判断了next ==serverSelectKey,它永远等于true。

 public static void main(String[] args) throws InterruptedException, IOException {
     Selector selector =Selector.open();
     ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();
     serverSocketChannel.bind(new InetSocketAddress(8000));
     serverSocketChannel.configureBlocking(false);
	 SelectionKey serverSelectKey = serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
	  System.out.println("服务端启动成功");
     for(;;){
         int select = selector.select();
         Set<SelectionKey> selectionKeys = selector.selectedKeys();
         Iterator<SelectionKey> iterator = selectionKeys.iterator();
         while (iterator.hasNext()){
             SelectionKey next = iterator.next();
             iterator.remove();
             if (next.isAcceptable()){
                 System.out.println("isAcceptable=="+(next ==serverSelectKey));
                 acceptHandler(serverSocketChannel,selector);
             }
             if (next.isReadable()){
                 readHandler(next,selector);
             }
         }
     }
 }

对于不同事件,可通过SelectionKey对象的isXXXX判断.

如果是同意连接,则调用acceptHandler处理。如下。同意之后,也要对SocketChannel设置非阻塞模式,在注册到Selector中,准备接收这个客户端的写操作,也就是注册时指明监听事件为SelectionKey.OP_READ,并向客户端输出一句欢迎来到祖安

 private static void acceptHandler(ServerSocketChannel serverSocketChannel,Selector selector) throws IOException{
     SocketChannel accept = serverSocketChannel.accept();
     accept.configureBlocking(false);
     accept.register(selector,SelectionKey.OP_READ);
     accept.write(Charset.forName("utf-8").encode("欢迎来到祖安"));
 }

如果是客户端正在写事件,则通过readHandler方法处理,这其中客户端关闭后read就成-1,要关闭连接,不然服务端会空轮询。

 private static  void readHandler(SelectionKey selectionKey) {
     try {
         SocketChannel channel = (SocketChannel)selectionKey.channel();
         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
         String request="";
         int read = channel.read(byteBuffer);
         if (read==-1){
             channel.close();
             return;}
         while (read>0){
             byteBuffer.flip();
             request +=Charset.forName("utf-8").decode(byteBuffer);
             read=channel.read(byteBuffer);
         }
         System.out.println("客户端数据:"+request);
     }catch (IOException e){
     }
 }

客户端测试,通过线程池提交10个线程去连接服务端,并输出6次数据。

客户端要接收数据,同样在Selector中注册read事件,通过轮询再次判断。

public class Main5NIOClient implements Runnable {
    class ClientRecvMsg extends Thread {
        private Selector selector;
        public ClientRecvMsg(Selector selector) {
            selector = selector;
        }
        @Override
        public void run() {
            super.run();
            for (; ; ) {
                try {
                    selector.select();
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey next = iterator.next();
                        if (next.isReadable()) {
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            ((SocketChannel) next.channel()).read(buffer);
                            buffer.flip();
                            System.out.println("服务端:" + Charset.forName("utf-8").decode(buffer));
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    @Override
    public void run() {
        SocketChannel socketChannel = null;
        try {
            //创建Selector
            Selector selector = Selector.open();
            //创建SocketChannel
            socketChannel = SocketChannel.open();
            //非阻塞模式
            socketChannel.configureBlocking(false);
            //连接,没有立即完成则返回false
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
            if (socketChannel.finishConnect()) {
                socketChannel.register(selector,SelectionKey.OP_READ);
                new ClientRecvMsg(selector).start();
                for (int i = 0; i < 6; i++) {
                    String info = "我是祖安" + i + "号";
                    socketChannel.write(Charset.forName("utf-8").encode(info));
                    TimeUnit.SECONDS.sleep(1);
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if (socketChannel != null) {
                    socketChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Main5NIOClient());
        }
    }
}

运行之后,单线程服务端轻松处理多个并发请求。

如有错误,请多多指出

发布了42 篇原创文章 · 获赞 7 · 访问量 7745

猜你喜欢

转载自blog.csdn.net/HouXinLin_CSDN/article/details/104274688
今日推荐