netty是如何一步一步演化而来的,不断进化成就netty

 2023-01-08
原文作者:zxhtom 原文地址:https://juejin.cn/post/6973394376626012168

简介

  • 随着IO类库不断完善,我们基于Java的网络编程学习成本越来越低。之前网络开发都是底层语言编写的,像C语言,C++。其实通过Java也可以编写。Java的io类基于tcp连接提供的socket就是我们实现网络开发的桥段。但是因为是同步阻塞式,所以效率上来说就慢了很多。在Java7之后提供了异步阻塞式io。为我们拉开了一个新天地。

BIO

  • 什么是网络编程?所谓的网络编程其实就是C/S模型。大家都知道Java是开发B/S模型的。C/S实际是client和server端的开发。说白就是两个进程相互通信。client通过tcp连接server然后交互数据
  • BIO 即同步阻塞是编程。在JDK1.4之前我们网络连接都是采用的BIO模式,服务端通过ServerSocket(port)构建服务端。然后服务端通过accept方法阻塞式等待客户端的连接。客户端通过Socket(ip,port)构建客户端。然后通过PrintWriter传递消息。
  • 默认情况下BIO模式是一个客户端对应一个线程。这样对于内存的消耗是严重的。慢慢的这种方式也就被抛弃了。

202212302214588331.png

server

  • 为了缓解线程压力。这里构造线程池。初始大小5个
    
    private static final ThreadLocal<ExecutorService> executorService = new ThreadLocal<ExecutorService>() {
        @Override
        protected ExecutorService initialValue() {
            return Executors.newFixedThreadPool(5);
        }
    };
  • 然后是构建ServerSocket。然后就是一直在阻塞式等待客户端的连接。什么叫阻塞式等待。就是一直在等待客户端连接。没有新的客户端连接就不继续执行。当客户端连接后。accept就会返回当前客户端的连接对象。然后我们将他进行封装后放到线程池中。线程池中有可用资源就会将可用线程加载这个客户端对象。
    
    public static void start() throws IOException {
        try {
            // 通过构造函数创建ServerSocket
            server = new ServerSocket(HostConstant.PORT);
            System.out.println("服务器已启动,端口号:" + HostConstant.PORT);
            while (true) {
                // 真正处理的还是Socket
                Socket socket = server.accept();// 阻塞方法
                // 把客户端请求打包成一个任务,放到线程池执行
                executorService.get().execute(new ServerHandler(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (server != null) {
                server.close();
            }
        }
    
    }
    
    public class ServerHandler implements Runnable {
        private Socket socket;
    
        public ServerHandler(Socket socket) {
            this.socket = socket;
        }
    
        @Override
        public void run() {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
                PrintWriter pw = new PrintWriter(socket.getOutputStream(), true);
                String message;
                String result;
                // 通过输入流读取客户端传输的数据
                while ((message = br.readLine()) != null) {
                    System.out.println("server receive data:" + message);
                    result = response(message);
                    // 将业务结果通过输出流返回给客户端
                    pw.println(result);
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    socket = null;
                }
            }
        }
    
        // 返回给客户端的应答
        public static String response(String msg) {
            return "Hello," + msg + ",Now is " + new java.util.Date(System.currentTimeMillis()).toString();
        }
    }

##client

    
    public class BIOClient {
        public void startConnect() {
            try {
                Socket socket = new Socket(HostConstant.IP, HostConstant.PORT);
                new ReadMsg(socket).start();
                PrintWriter pw = null;
                // 写数据到服务端
                pw = new PrintWriter(socket.getOutputStream());
                pw.println(UUID.randomUUID());
                pw.flush();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        public static class ReadMsg extends Thread {
            Socket socket;
    
            public ReadMsg(Socket socket) {
                this.socket = socket;
            }
    
            @Override
            public void run() {
                try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
                    String line = null;
                    // 通过输入流读取服务端传输的数据
                    while ((line = br.readLine()) != null) {
                        System.out.printf("%s\n", line);
                    }
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
            Integer time = 6;
            for (Integer i = 0; i < time; i++) {
                new BIOClient().startConnect();
            }
        }
    }

缺点

  • 上面服务端和客户端实际上是伪异步。表面上看起来不会因为客户端的增加导致内存溢出。但是因为实际上还是accpet同步阻塞等待。所以在连接性能上还是不好。
  • 我们在ServerHanndler中是读取客户端传输的数据通过BufferedReader.readLine这个方法。我们跟踪下去发现实际调用的是InputStream.read这个方法。

202212302215002442.png

    
    /**
      * Reads the next byte of data from the input stream. The value byte is
      * returned as an <code>int</code> in the range <code>0</code> to
      * <code>255</code>. If no byte is available because the end of the stream
      * has been reached, the value <code>-1</code> is returned. This method
      * blocks until input data is available, the end of the stream is detected,
      * or an exception is thrown.
      *
      * <p> A subclass must provide an implementation of this method.
      *
      * @return     the next byte of data, or <code>-1</code> if the end of the
      *             stream is reached.
      * @exception  IOException  if an I/O error occurs.
      */
    public abstract int read() throws IOException;
  • 我们在翻阅他们的源码说明,会发现上面注释大意为:当没有字节的时候意味着已经结束了我们会返回-1。这个方法一直阻塞知道获取到字节或者是结束或者是抛出异常。我们之前也说了服务端等待连接的时候是阻塞式等待。这会造成客户端连接的一些问题。但是客户端连接上以后开始通信了。服务端获取客户端的消息也是采用阻塞式等待的。这会直接造成交互等待从而造成交互拥堵。换句话说客户端A发送了100条消息。服务端会一条一条处理。像食堂打菜排队一样。
  • 同样的道理OutputStream也是阻塞式的。这里有兴趣的读者可以自行翻阅源码查看。这也说明我们连接是阻塞的。通信也是阻塞的。这就是BIO暴露的缺点。
  • 因为阻塞,会造成后期客户端的接入无法成功,会一直等待。造成连接超时。

NIO

NIO=Non Block IO .即非阻塞式编程。

  • 在BIO中我们通过ServerSocket、Socket实现服务端和客户端的开发。在NIO中对应提供了ServerSocketChannel、SocketChannel两个类实现通信。这两个类支持阻塞式和非阻塞式模式。阻塞模式这里不做介绍造成的后果由和BIO一样。下面我们来看看如何实现NIO

##ByteBuffer

  • ByteBuffer使我们NIO通信的一个缓冲区,我们的读写都是借助与它传递的。因为他提供了类似指针指向,我们操作指向就可以获取到字节。

##Channel

  • 在NIO中他被看做是一个通道。通过Channel控制读和写。BIO中是通过Stream方式传递的。Channel和Stream相比最大的特点Channel是双向的。简单的理解就是读用Channel,写也用Channel。在BIO中InputStream和OutputStream分别负责读和写。上面提到的ServerSocketChannelSocketChannel都是Channel的子类。

##Selector

-多路复用器。这里我们可以理解为注册中心。所有的handler再向selector注册的时候会带上标签(SelectorKey)。在某个Channel发生读或写的事件时这个Channel会处于就绪状态。在Selector轮询的时候会筛选出来。然后我们在根据SelectorKey判断监听的是什么事件。从而做出处理。查阅资料得知selector没有连接限制。理论上一个selector可以管理N个Channel。

202212302215014673.png

  • 上面是服务器的开发流程。其实就是用一个线程来管理selector。然后不停遍历selector然后捕获事件。服务端和客户端捕获不同的事件。下面大概列举了下事件

202212302215023124.png

  • 框架搭建就是流程上的,剩下的就是我们在selectorKey这个事件上进行业务的io处理。在读写期间就是ByteBuffer来实现。NIO的实现和BIO相对比较复杂。

进化

关于NIO2.0 和nettry 他们在次基础上进行提升!不得不说现在基本上是netty的天下了。下章节我们针对netty来展开讨论!今天端午节不说了我吃粽子去了

记得吃完粽子,回来点个赞哦!!!