• Netty 总结(一)


    使用传统的IO编程,实现客户端服务端通信

    场景为每隔2秒钟客户端向服务端发送带有时间戳的Hello World,服务端接收后打印。

    此为客户端IO,连接上服务端 8000 端口之后,每隔 2 秒,我们向服务端写一个带有时间戳的 "hello world"。

    public class IOClient {
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    Socket socket = new Socket("127.0.0.1", 8000);
                    while (true) {
                        try {
                            socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                            Thread.sleep(2000);
                        } catch (Exception e) {
                        }
                    }
                } catch (IOException e) {
                }
            }).start();
        }
    }
    

    此为服务端IO, server端首先创建**ServerSocket **绑定了8000端口,然后创建新的线程,阻塞的获取新的连接,当获取新的连接后,
    再创建一个线程用于读取数据,并且读取数据是以字节流的方式。

    public class IOServer {
        public static void main(String[] args) throws Exception {
    
            ServerSocket serverSocket = new ServerSocket(8000);
    
            // (1) 接收新连接线程
            new Thread(() -> {
                while (true) {
                    try {
                        // (1) 阻塞方法获取新的连接
                        Socket socket = serverSocket.accept();
    
                        // (2) 每一个新的连接都创建一个线程,负责读取数据
                        new Thread(() -> {
                            try {
                                int len;
                                byte[] data = new byte[1024];
                                InputStream inputStream = socket.getInputStream();
                                // (3) 按字节流方式读取数据
                                while ((len = inputStream.read(data)) != -1) {
                                    System.out.println(new String(data, 0, len));
                                }
                            } catch (IOException e) {
                            }
                        }).start();
    
                    } catch (IOException e) {
                    }
    
                }
            }).start();
        }
    }
    

    上面的 demo,从服务端代码中我们可以看到,在传统的 IO 模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个 while 死循环,那么 1w 个连接对应 1w 个线程,继而 1w 个 while 死循环,这就带来如下几个问题:

    • 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起
    • 线程切换效率低下:单机 CPU 核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
    • 除了以上两个问题,IO 编程中,我们看到数据读写是以字节流为单位。

    1线程资源受限

    传统的IO编程中,每个连接都创建新的线程,而NIO则将连接绑定到固定的线程,而线程负责连接的所有读写


    NIO 模型中,他把这么多 while 死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个 while 死循环就能监测1w个连接是否有数据可读的呢? 这就是 NIO 模型中 selector 的作用,一条连接来了之后,现在不创建一个 while 死循环去监听是否有数据可读了,而是直接把这条连接注册到 selector 上,然后,通过检查这个 selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明 IO 与 NIO 的区别。

    每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100 个小朋友就需要 100 个老师来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
    所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批量领到厕所,这就是 NIO 模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。
    这就是 NIO 模型解决线程资源受限的方案,实际开发过程中,我们会开多个线程,每个线程都管理着一批连接,相对于 IO 模型中一个线程管理一条连接,消耗的线程资源大幅减少

    2线程切换效率低下
    由于 NIO 模型中线程数量大大降低,线程切换效率因此也大幅度提高

    IO 读写是面向流的,一次性只能从流中读取一个或者多个字节,并且读完之后流无法再读取,你需要自己缓存数据。 而 NIO 的读写是面向 Buffer 的,你可以随意读取里面任何一个字节数据,不需要你自己缓存数据,这一切只需要移动读写指针即可。

    public class NIOServer {
        public static void main(String[] args) throws IOException {
            Selector serverSelector = Selector.open();
            Selector clientSelector = Selector.open();
    
            new Thread(() -> {
                try {
                    // 对应IO编程中服务端启动
                    ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                    listenerChannel.socket().bind(new InetSocketAddress(8000));
                    listenerChannel.configureBlocking(false);
                    listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
    
                    while (true) {
                        // 监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
                        if (serverSelector.select(1) > 0) {
                            Set<SelectionKey> set = serverSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.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();
                                    }
                                }
    
                            }
                        }
                    }
                } catch (IOException ignored) {
                }
    
            }).start();
    
    
            new Thread(() -> {
                try {
                    while (true) {
                        // (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
                        if (clientSelector.select(1) > 0) {
                            Set<SelectionKey> set = clientSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();
    
                            while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();
    
                                if (key.isReadable()) {
                                    try {
                                        SocketChannel clientChannel = (SocketChannel) key.channel();
                                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                        // (3) 面向 Buffer
                                        clientChannel.read(byteBuffer);
                                        byteBuffer.flip();
                                        System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                                .toString());
                                    } finally {
                                        keyIterator.remove();
                                        key.interestOps(SelectionKey.OP_READ);
                                    }
                                }
    
                            }
                        }
                    }
                } catch (IOException ignored) {
                }
            }).start();
    
    
        }
    }
    
    
    • NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在我们这个例子中serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读
    • 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等,参见(1)
    • clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过 clientSelector.select(1)方法可以轮询出来,进而批量处理,参见(2)
    • 数据的读写面向 Buffer,参见(3)

    NIO的实现非常不友好,自定义的NIO容易出现各种bug,因而出现了Netty,下篇文章介绍netty

  • 相关阅读:
    USB描述符(转)
    (转)Linux设备驱动之HID驱动 源码分析
    Linux USB 鼠标驱动程序详解(转)
    (转)linux如何获取鼠标相对位置信息
    从零写一个编译器(四):语法分析之构造有限状态自动机
    从零写一个编译器(三):语法分析之几个基础数据结构
    从零写一个编译器(二):语法分析之前置知识
    从零写一个编译器(一):输入系统和词法分析
    自底向上语法分析
    递归下降和LL(1)语法分析
  • 原文地址:https://www.cnblogs.com/gloria-liu/p/10205010.html
Copyright © 2020-2023  润新知