NIO同步非阻塞Selector轮询实现网络socket

NIO同步非阻塞,详细原理见代码注释

  • 服务器端代码
package niodemo.niochat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

//如下的代码就实现了服务器仅仅通过1个线程(没有创建新的线程就是主线程)监听多个客户端的请求,可以写客户端测试,也可以用telnet
public class NIOSelectorServer {
    private static Charset charset = Charset.forName("unicode");

    public static void main(String[] args) throws IOException {

        //1. 创建ServerSocketChannel,通过open()获得channel实例
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //2.绑定ip
        InetSocketAddress inetSocketAddress = new InetSocketAddress(6666);
        serverSocketChannel.bind(inetSocketAddress);

        //3. 将serverSocketChannel设置成非阻塞的
        serverSocketChannel.configureBlocking(false);

        //4. 创建Selector
        Selector selector = Selector.open();

        //5. 将ServerSocketChannel注册到Selector上:注意:首次注册都是注册到selector上的所有的SelectionKey集合上
        //【Selector上注册的都是channel,在服务器端就一个channel就是ServerSocketChannel】
        //注册是以事件的方式注册到selector上,serverSocketChannel这个服务器端通道关注的事件是accept
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        /*
         * (1) SelectionKey类是Selector类上的集合,Selector上有3个SelectionKey,分别是:所有的SelectionKey,
         *     已选择的SelectionKey,和废弃的SelectionKey。
         * (2) SelectionKey上有4中事件类型(注册到selector上的channel都是通过事件注册的)分别是:
         *     OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE
         * (3) 其中,服务器端的通道是ServerSocektChannel就一个,关注的事件是OP_ACCEPT
         *     服务器接收客户端请求的通道是SocketChannel是多个的,一个客户端一个SocketChannel,关注的事件是OP_READ和OP_WRITE
         *     写客户端的时候,客户端的SocketChannel关注的事件是OP_CONNECT
         * */

        //6. 在selector上使用select()方法轮询所有的SelectionKey集合
        System.out.println("select()自身以阻塞的方式等待客户端连接......");
        while (selector.select() > 0) {    //select()的返回值大于0,说明所有的SelectionKey集合上有事件发生
            //有事件发生的就将其从selector上所有的SelectionKey集合转移到已选择的SelectionKey集合上。
            //7. 遍历已选择的SelectionKey集合,遍历是方法:迭代器,for循环

            //需要注意的是:selector.selectedKeys()方法返回的是已选择的SelectionKey集合,当有事件发生的时候遍历的就是已选择的SelectionKey集合
            //而selector.select()轮询的是所有的SelectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //使用迭代器遍历已选择的SelectionKey集合
            Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
            while (selectionKeyIterator.hasNext()) {
                SelectionKey key = selectionKeyIterator.next();
                try {
                    //key是拿到的每个selectionKey集合上的曾经注册的channel
                    //8. 情况1:如何已选择集合SelectionKey上发生的事件是accept,说明是一个新的客户端连接请求事件
                    if (key.isAcceptable()) {
                        System.out.println("有新的客户端连接!");
                        //9. 新的连接事件要为该客户端创建一个新的专属于这个客户端连接的channel
                        //客户端的channel通过SocketChannel创建,服务器的channel通过ServerSocketChannel创建
                        //就像客户端的socket通过Socket创建,服务器的socket通过ServerSocket创建
                        SocketChannel socketChannel = serverSocketChannel.accept(); //通过serverSocketChannel.accept()获得实例
                        //10. 将channel设置为非阻塞模式,否则使用NIO无意义【每一个channel在注册到selector之前都要设置为非阻塞模式】
                        socketChannel.configureBlocking(false);

                        //11. 将channel注册到selector的所有SelectionKey上,等待被轮询
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }

                    //12. 情况2:如果已选择集合SelectionKey上发生的事件是read, 说明是原来就已经在selector上的channel又有事件发生了
                    if (key.isReadable()) {

                        //13. 要获取与客户端唯一对应的channel,才能处理客户端的请求
                        /*SelectableChannel*/
                        SocketChannel selectableChannel = (SocketChannel) key.channel();
                        //返回值是SelectableChannel,获取channel的方法不是getChannel(),而是SelectionKey提供的channel()
                        //返回值SelectableChannel没有read和write方法,没法和buffer交互数据

                        //14. 读数据:程序要想读channel的数据必须通过内存中的buffer
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //15. buffer从channel中读数据
                        int readByte = 0;    //读到的字节数,read的返回值可以是int或者long
                        byteBuffer.clear();  //为向buffer中写入数据做准备
                        while ((readByte = selectableChannel.read(byteBuffer)) > 0) {
                            //buffer从channel中循环读取数据的时候:注意:flip()
                            byteBuffer.flip();  //为从buffer输出数据做准备
                        }
                        //读取到的内容就在buffer里,程序从buffer中取数据要从字节(二进制)转成字符串。
                        System.out.println("form client:" + new String(byteBuffer.array()));
                    }
                    //当select()返回值大于0时,表示所有SelectionKey集合中有事件发生,所以将发生事件的已经注册的channel
                    //从所有的SelectionKey集合中拿到已选择的SelectionKey中;
                    //当处理完已选择的selectionKey中的channel,就将其移到已废弃的selectionKey中
                    selectionKeys.remove(key);

                    //之前的处理IOException的方式是throw,坏处是:如果客户端先关闭就会报错:
                    //java.io.IOException: 远程主机强迫关闭了一个现有的连接。
                } catch (IOException ioe) {  //如果出现异常,就说明客户端异常,可能是意外关闭了
                    //所以将遍历的已选择的SelectionKey集合上的channel从selector集合上取消注册
                    key.cancel();
                }
            }

        }
    }
}

  • 客户端代码
package client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
import java.util.Set;

public class ClientDemo {

    private String HOST = "127.0.0.1";
    private static final int PORT = 6666;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public ClientDemo() {
        try {
            this.selector = Selector.open();
            //源码中:checkPort()是0-65535的整数0xFFFF,通常49152往上的端口可以自由使用
            InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT);
            this.socketChannel = SocketChannel.open(inetSocketAddress);

            socketChannel.configureBlocking(false);
            socketChannel.register(this.selector, SelectionKey.OP_READ);

            this.username = socketChannel.getRemoteAddress().toString().substring(1);
            System.out.println(this.username + "登录成功");
        } catch (IOException i) {
            i.getStackTrace();
        }

    }

    //向服务器发消息
    public void sendMsgToServer(String info) {
        info = this.username + "说" + info;
        try {
//            //从键盘获取数据
//            Scanner scanner = new Scanner(System.in);
//            while (scanner.hasNextLine()){
//                System.out.println(scanner.next());
//                //装入buffer
//                ByteBuffer byteBuffer = ByteBuffer.wrap(scanner.next().getBytes());
//                //写入channel
//                socketChannel.write(byteBuffer);

            this.socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //接收服务器的消息
    public void readFromServer() {
        try {
            //轮询selector是否有事件发生(这里关心的就是读事件,因为没有连接事件,不是服务器)
            int select = selector.select();
            if (select > 0) {
                //遍历selector集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey key : selectionKeys) {
                    if (key.isReadable()) {
                        //获取channel
                        Channel channel = key.channel();
                        SocketChannel socketChannel2 = (SocketChannel) channel;
                        //创建buffer
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //从channel中读数据到buffer
                        socketChannel2.read(byteBuffer);
                        //显示出来
                        System.out.println(new String(byteBuffer.array()));
                    }
                    //channel处理后必须要移除!!!!!!!!!1
                    selectionKeys.remove(key);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ClientDemo groupChatClient = new ClientDemo();
        //匿名内部类用lambda表达式优化写法
        new Thread(() -> {
            //不停的读取
            while (true) {
                //客户端起多个线程读数据
                groupChatClient.readFromServer();
                try {
                    Thread.currentThread().sleep(3000);
                } catch (InterruptedException ine) {
                    ine.printStackTrace();
                }
            }

        }).start();

        //客户端的一个主线程向服务器写数据
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String str = scanner.nextLine();
            groupChatClient.sendMsgToServer(str);
        }
    }
}

  • 运行结果
    在这里插入图片描述
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值