Netty框架学习(二)之基础知识:BIO与NIO的实现案例与优劣对比

1. 概述

  • 继续开始Netty框架之旅,本文仍然还没有进入到Netty框架使用中。
  • 在那之前,我们一起来看看Java的Socket编程,Netty是基于NIO实现的,而原生的NIO是什么样的呢。
  • 这篇文章中,我们将会看到2种编程模式的特点以及优劣性的对比。

2. BIO实现客户端与服务端通信代码

这里将实现一个客户端和一个服务端,客户端发起一次请求,服务端接收请求并返回处理结果。
下文将根据这段代码说明BIO存在的问题。

  1. 首先是服务端代码:
/**
 * @author GrainRain
 * @date 2020/05/19 20:10
 *
 * Description BIOServer
 **/
public class BIOServer {
    public static final int PORT = 8090;
    public static void main(String[] args){
        ServerSocket server = null;
        try {
            server = new ServerSocket(PORT);
            System.out.println("the server is started");
            while (true) {
                //每获取一个连接,将socket交给handler处理,此处发生阻塞
                Socket socket = server.accept();
                //此处可以采用线程池处理
                new Thread(new BIOMessageHandler(socket)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(server != null) {
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    server = null;
                }
            }
        }
    }
}
  1. 服务端消息处理器(本质上是一个线程)
/**
 * @author GrainRain
 * @date 2020/05/19 20:26
 **/
public class BIOMessageHandler implements Runnable {

    private Socket socket;
    public BIOMessageHandler(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = socket.getInputStream();
            byte[] bytes = new byte[2 << 20];
            StringBuilder builder = new StringBuilder();
            int len = -1;
            while ((len = in.read(bytes)) != -1){
                builder.append(new String(bytes,0,len));
            }
            System.out.println("receive message,the data is >>" + builder.toString());
            out = socket.getOutputStream();
            out.write("receive success, now time is"+ System.currentTimeMillis()).getBytes());
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            close(in,out);
        }
    }

    public void close(AutoCloseable ... autoCloseables){
        if(autoCloseables==null){
            return;
        }

        for (int i = 0; i < autoCloseables.length; i++) {
            AutoCloseable autoCloseable = autoCloseables[i];
            try {
                autoCloseable.close();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                autoCloseable = null;
            }
        }
    }
}
  • 从以上代码可以看出,每获取一个连接,我们都为其分配一个线程进行处理。就算是在空闲状态,也就是说客户端只是单纯的连接但没有进行数据通信,那么这个线程也并不能被释放,始终占用着系统资源。当大量请求涌入时,过多的线程将导致系统资源耗竭
  • 当然,我们可以通过线程池使得一组线程处理多个请求,实现一种伪异步模型。尽管这样不会导致系统线程过多导致资源耗竭。但本质上,由于同步阻塞模型的限制,读写过程都是处于阻塞状态,当客户端网络很慢时,将会导致线程资源持续的占用,如果线程池正在执行的都是这样的劣质慢任务,那么大量的等待线程被挂起而得不到处理,最终产生超时,此时给外部的感觉是服务已经宕掉了。有句话说的很对,当服务的性能依赖于大量良莠不齐的客户端,性能无法得到保障
  • 但是,代码确实非常的简单便于理解。对于并发并不大的服务而言,还是值得使用的。
  1. 客户端代码
  • 客户端就更为简单了,实现一个发送数据的功能。
/**
 * @author GrainRain
 * @date 2020/05/19 20:32
 * Description BIO客户端
 **/
public class BIOClient {
    public static final String HOST = "127.0.0.1";
    public static final int PORT = 8090;
    public static void main(String[] args) {
        Socket socket = null;
        BufferedReader in = null;
        OutputStream out = null;
        try{
            socket = new Socket(HOST,PORT);
            out = socket.getOutputStream();
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out.write("I'm client".getBytes());
            //通知已完成写出
            socket.shutdownOutput();
            String back = in.readLine();
            //通知已完成读入数据
            socket.shutdownInput();
            System.out.println("the client receive message >> " + back);
        }catch(Exception e){
            e.printStackTrace();
        }finally {
        }
    }

    public void close(AutoCloseable ... autoCloseables){
        if(autoCloseables==null){
            return;
        }

        for (int i = 0; i < autoCloseables.length; i++) {
            AutoCloseable autoCloseable = autoCloseables[i];
            try {
                autoCloseable.close();
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                autoCloseable = null;
            }
        }
    }
}

3. NIO实现客户端与服务端通信代码

  • Java从1.7开始,对NIO的API进行了重大升级,实现了真正意义上的NIO支持。

  • Java NIO引入了3大的组件,包括buffer(缓冲区)、Channel(通道)、Selector(多路复用选择器)。通过这几大组件的分工协作,能够实现多路复用的非阻塞通信模型。
    几大组件的详细介绍在此不做过多说明,读者可自行百度或翻阅源码。

  • 以下是基于这几大组件实现的NIO模式的客户端与服务端代码。功能实现上上,仍然同前文的同步阻塞模型(BIO)一样。

  1. 首先是服务端代码,首先是运行一个多路复用器线程,进行事件监听。
/**
 * @author GrainRain
 * @date 2020/05/19 21:38
 **/
public class NIOServer {
    public static final int PORT = 8090;
    public static void main(String[] args) {
        new Thread(new MessageSelector(PORT)).start();
    }
}

上面这段非常简单,就是启动一个多路复用选择器,通过多路复用选择器轮询监听Channel,将活跃的事件进行处理。
2. 以下是多路复用选择器线程,这部分非常关键。

/**
 1. @author GrainRain
 2. @date 2020/05/19 21:41
 **/
public class MessageSelector implements Runnable{
    private int port;
    /**
     * 多路复用选择器
     * 通过selector实现一个线程处理多个请求
     */
    private Selector selector;
    private boolean isKeepAlive;
    private ServerSocketChannel serverSocketChannel;
    /**
     * 消息处理器
     */
    private NIOMessageHandler nioMessageHandler;
    public MessageSelector(int port){
        this.port = port;
        nioMessageHandler = NIOMessageHandler.getInstance();
    }

    @Override
    public void run() {
        registrySelector();
        this.isKeepAlive(true);
        while (isKeepAlive){
            try {
                //轮询
                selector.select(1000);
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                selectionKeys.stream().forEach(selectionKey -> {
                    try {
                        //将轮询到的活跃请求交给handler处理
                        this.selectKeyHandler(selectionKey);
                    } catch (Exception e) {
                        if (selectionKey != null) {
                            selectionKey.cancel();
                            if (selectionKey.channel() != null) {
                                try {
                                    selectionKey.channel().close();
                                } catch (IOException ex) {
                                    ex.printStackTrace();
                                }
                            }
                        }

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

    public void registrySelector(){
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(port));
            //注册服务端serverSocketChannel的accept事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        }catch (Exception e){
            e.printStackTrace();
            System.exit(1);
        }
    }

    public boolean isKeepAlive(boolean isKeep){
        this.isKeepAlive = isKeep;
        return isKeep;
    }

    public void selectKeyHandler(SelectionKey selectionKey) throws IOException {
        //新连接请求,获取连接,注册读事件
        if (selectionKey.isAcceptable()){
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            //设为非阻塞
            socketChannel.configureBlocking(false);
            //注册读事件
            socketChannel.register(selector,SelectionKey.OP_READ);
        }
        //如果是读事件,则交给另一组线程池进行业务逻辑处理
        if(selectionKey.isReadable()){
            SocketChannel socketChannel = null;
            try {
                socketChannel  = (SocketChannel) selectionKey.channel();
                nioMessageHandler.socketHandle(socketChannel);
            }catch (Exception e){
                socketChannel.close();
                selectionKey.cancel();
            }finally {
                socketChannel = null;

            }
        }
    }
}
  • 以上过程启动了一个selector进程,用于监听所有注册的Channel,当通道中相应事件被激活时,则对应该事件进行处理。
  • 可以看出,此处实现了一个线程处理多个请求,对于一些慢任务逻辑,可以直接转交给一个专门的线程池进行处理。
    在这种模型下,由于缓冲区等组件的引入,并不会因为客户端通信问题而导致服务端的线程被阻塞。整个过程是非阻塞的,服务端只需要从触发事件的socket获取buffer中的数据进行处理即可。
    由此,真正实现了异步通信的要求,服务端性能不再受客户端制约。
  • 以上就是NIO的核心代码,而数据处理器代码和客户端代码相对简便,此处客户端仅给出本测试案例的实现,并不是NIO应有的实现模式。
  1. 数据处理器
/**
 * @author GrainRain
 * @date 2020/05/19 22:12
 **/
public class NIOMessageHandler {
    private static NIOMessageHandler nioMessageHandler = new NIOMessageHandler();
    private ExecutorService threadPool;
    private NIOMessageHandler(){
        //初始化线程池
        threadPool = new ThreadPoolExecutor(2,
                5,
                30,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1024));
    }

    public static NIOMessageHandler getInstance(){
        return nioMessageHandler;
    }

    public void socketHandle(SocketChannel socketChannel){
        if(socketChannel==null){
            return;
        }
        threadPool.submit(new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                //数据读取
                int read = socketChannel.read(byteBuffer);
                if (read > 0) {
                    Buffer buffer = byteBuffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    byteBuffer.get(bytes);
                    String msg = new String(bytes);

                    System.out.println("the server receive message >>" + msg);

                    String sendMsg = "hello, I'm server!!!";
                    //写出数据
                    socketChannel.write(ByteBuffer.wrap(sendMsg.getBytes()));
                }
                return null;
            }
        });
    }
}
  1. 客户端代码实现
    客户端就只是单纯的连接服务端,发送数据和接收数据,并没有做更多的考虑。
/**
 * @author GrainRain
 * @date 2020/05/19 22:28
 **/
public class NIOClient {
    public static final int PORT = 8090;
    public static final int LOCAL_PORT = 8081;
    public static final String IP = "127.0.0.1";
    public static void main(String[] args) {
        SocketChannel socketChannel = null;
        try {
            socketChannel  = SocketChannel.open();
            socketChannel.bind(new InetSocketAddress(LOCAL_PORT));
            //设为非阻塞
            socketChannel.configureBlocking(false);
            //连接服务端
            socketChannel.connect(new InetSocketAddress(IP,PORT));
            while (!socketChannel.isConnected()){
                socketChannel.finishConnect();
            }
            //发送数据
            socketChannel.write(ByteBuffer.wrap("hello server, I'm client".getBytes()));
            Thread.sleep(100);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            //读取数据
            socketChannel.read(buffer);
            buffer.flip();
            System.out.println(new String(buffer.array(),0,buffer.limit()));
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                socketChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                socketChannel = null;
            }
        }
    }
}

4. 总结

  • 以上是有关Java BIO与NIO实现客户端与服务端案例,并介绍了两种模式的优缺点。
  • 可以看出NIO虽好,但是代码也确实复杂很多,开发难度较大,而且有许多需要考虑的问题。而Netty的出现将这些麻烦的事情都给处理好了,提供了简单易用的API供我们使用。
  • 下一篇将开始正式进入Netty,看看Netty为我们做了哪些工作。希望各位大佬能给出您宝贵的意见,共同交流学习。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值