理解BIO和NIO

一。单线程下的BIO实现

服务端:

public class Server {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024];

        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("服务器已启动并监听8080端口");
            while (true) {
                System.out.println();
                System.out.println("服务器正在等待连接...");
                // (1) 阻塞方法获取新的连接
                Socket socket = serverSocket.accept();

                System.out.println("服务器已接收到连接请求...");
                System.out.println();
                System.out.println("服务器正在等待数据...");
                 // (2) 阻塞读
                  // (3) 按字节流方式读取数据
                socket.getInputStream().read(buffer);
                System.out.println("服务器已经接收到数据");
                System.out.println();

                String content = new String(buffer);
                System.out.println("接收到的数据:" + content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

客户端:

public class Client {
    public static void main(String[] args) {

        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1",8080);
            String message = "";
            Scanner sc = new Scanner(System.in);
            message = sc.next();
            socket.getOutputStream().write(message.getBytes());
            socket.close();
            sc.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

场景1:启动server端,然后分别启动client1,client2,client3
运行结果:
启动服务端:在这里插入图片描述
然后启动client1 client2 client3,查看服务端控制台日志
在这里插入图片描述
分析结果现象:
如图所示:服务端启动时,并监听端口8080,然后处于阻塞状态,等待客户端连接,当有客户端连接服务端时,服务端接受连接请求,然后等待客户端发送数据。最后客户端1 发送消息,服务端收到消息。从运行结果上来看,说明了服务器端有两个阻塞,一个是等待客户端连接阻塞,另一个就是读客户端数据阻塞。
在这里插入图片描述
单线程下的BIO实现缺点:
1.当服务器端收到客户端连接,但是没有收到客户端的发送数据请求,服务端读客户端数据read()会一直阻塞,如果其他客户端在来请求是无法响应的。

二、多线程下的BIO实现
场景,每次服务器读到客户端发来请求数据就开启一个线程读数据。

public class Server1 {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024];
        try {
            ServerSocket serverSocket = new ServerSocket(8080);
            System.out.println("服务器已启动并监听8080端口");
            while (true) {
                System.out.println();
                System.out.println("服务器正在等待连接...");
                // (1) 阻塞方法获取新的连接
                Socket socket = serverSocket.accept();
                // (2) 每一个新的连接都创建一个线程,负责读取数据
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("服务器已接收到连接请求...");
                        System.out.println();
                        System.out.println("服务器正在等待数据...");
                        try {
                            // (3) 按字节流方式读取数据
                            socket.getInputStream().read(buffer);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        System.out.println("服务器已经接收到数据");
                        System.out.println();

                        String content = new String(buffer);
                        System.out.println("接收到的数据:" + content);
                    }
                }).start();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

缺点:多线程解决了无法并行读取客户端的数据,但是如果大量出现不发送数据的客户端,会导致很多的连接,连接多会导致服务器压力增大。所以如果出现不活跃的线程比较多,应该采用单线程的方案。但是单线程无法处理并发,于是有了NIO。
NIO重要解决客户连接和读取数据阻塞问题的。

三、模拟NIO
1.解决客户端连接和读取数据阻塞问题。

 public class NioServer {
    public static void main(String[] args)  throws InterruptedException{
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            //Java为非阻塞设置的类
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            while(true) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                if(socketChannel==null) {
                    //表示没人连接
                    System.out.println("正在等待客户端请求连接...");
                    Thread.sleep(5000);
                }else {
                    System.out.println("当前接收到客户端请求连接...");
                }
                if(socketChannel!=null) {
                    //设置为非阻塞
                    socketChannel.configureBlocking(false);
                    byteBuffer.flip();//切换模式  写-->读
                    int effective = socketChannel.read(byteBuffer);
                    if(effective!=0) {
                        String content = Charset.forName("utf-8").decode(byteBuffer).toString();
                        System.out.println(content);
                    }else {
                        System.out.println("当前未收到客户端消息");
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

运行结果:
在这里插入图片描述
分析运行结果:
从结果上看,接受到客户端的请求,没有阻塞,但是又回到客户端等待请求,所以会导致丢失当前客户连接请求并丢失客户端发来的消息,怎么办呢?把当前客户请求存储到list上,然后轮询遍历list,验证客户端是否准备好消息,然后打印。如下代码:

public class NioServer1 {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        List<SocketChannel> socketList = new ArrayList();

        try {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            //设置非阻塞
            serverSocketChannel.configureBlocking(false);
            while (true){
                SocketChannel socketChannel = serverSocketChannel.accept();
                if(socketChannel == null){
                    System.out.println("正在等待客户连接..");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    System.out.println("当前收到客户端的连接请求");
                    socketList.add(socketChannel);
                }
                socketList.forEach(s ->{
                        //设置非阻塞
                        try {
                            s.configureBlocking(false);
                            int read = s.read(byteBuffer);
                            if(read != 0){
                                byteBuffer.flip();//切换模式  写-->读
                                String content = Charset.forName("utf-8").decode(byteBuffer).toString();
                                System.out.println(content);
                                byteBuffer.clear();
                            }else{
                                System.out.println("当前未收到客户端消息");
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                });

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

在这里插入图片描述
分析:如图代码所示,我们没有开启第二个线程,使用的是一个线程解决了并发处理客户端请求和发送消息,这样的模式可以很好的解决了BIO在单线程下的并发处理多个客户端的请求的问题,并且解决了非阻塞情况下客户端丢失的问题。
缺点:在接受消息处理上是有问题的,每次来个客户端都要轮询判断数据是否准备好,假设有1000w个连接,甚至更多,采用这种轮询方式效率是低的。
另外,1000w连接中,我们可能只会有100w会有消息,剩下的900w并不会发送任何消息,那么这些连接程序依旧要每次都去轮询,这显然是不合适的。怎么解决呢?将轮询那块代码使用操作系统级别的系统函数(select函数),主动的去感知有数据的socket。
四、真实的NIO

/**
 * NIO 模型中 selector 的作用,
 * 一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,
 * 而是直接把这条连接注册到 selector 上,
 * 然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,
 */
public class NioServerSelector {
    public static void main(String[] args) throws IOException{
        //使用selector 解决while的轮询问题,把所有的连接的注册到Selector上
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        new Thread(()->{
            try {
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.bind(new InetSocketAddress(8080));
                serverSocketChannel.configureBlocking(false);
                serverSocketChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true){
                    // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
                    if(serverSelector.select(1) > 0){
                        System.out.println("监测是否有新的连接, 当前收到客户端的连接请求..");
                        Set<SelectionKey> selectionKeys = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                        while (keyIterator.hasNext()){
                            SelectionKey key = keyIterator.next();
                            if(key.isAcceptable()){
                                try {
                                    // (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                    SocketChannel clientChannel  = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                }finally {
                                    keyIterator.remove();
                                }
                            }
                        }
                    }else {
                        System.out.println("监测是否有新的连接, 正在等待客户端连接..");
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(()->{
            try {
                ///(2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
                while (true){
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("批量轮询是否有哪些连接有数据可读");
                    if(clientSelector.select(1) > 0){
                        Set<SelectionKey> selectionKeys = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator  = selectionKeys.iterator();
                        while (keyIterator .hasNext()){
                            SelectionKey key = keyIterator.next();
                            if(key.isReadable()){
                                try {
                                    SocketChannel channel = (SocketChannel)key.channel();
                                    // (3) 面向 Buffer
                                    channel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } catch (IOException e) {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                    e.printStackTrace();
                                }
                            }
                        }

                    }

                }
            }catch (Exception e){
                e.printStackTrace();
            }

        }).start();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值