Netty入门

Netty 5用户指南中文版

前言

1、传统IO编程

在学习Java基础的网络编程时,我们写过这样的代码

    // 在10086端口启动服务监听
    ServerSocket ss = new ServerSocket(10086);
    System.out.println("服务器正常启动。。。");
    while (true) {
        // 阻塞,等待客户端接入
        Socket socket = ss.accept();
        System.out.println("新的用户接入。。。");
        // 启动一个新的线程处理客户连接
        new ServerThread(socket).start();
    }

上面的服务端代码中我们可以看到,在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护。当客户端比较多时,单机服务端可能需要创建大量的线程处理这些连接,这就带来如下几个问题:

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

为了解决这三个问题,JDK在1.4之后提出了NIO。

2、NIO编程

更多NIO详细内容请参考之前的博客:https://blog.csdn.net/qq_31142553/article/details/85925718

直接上一段代码来示范一下

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) 读取数据以块为单位批量读取
                                    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)
  • 数据的读写以内存块为单位,参见(3)

所以不推荐直接使用Java NIO来进行网络开发,原因如下

  • 1、JDK的NIO编程需要了解很多的概念,编程复杂,对NIO入门非常不友好,编程模型不友好,ByteBuffer的api简直反人类
  • 2、对NIO编程来说,一个比较合适的线程模型能充分发挥它的优势,而JDK没有给你实现,你需要自己实现,就连简单的自定义协议拆包都要你自己实现
  • 3、JDK的NIO底层由epoll实现,该实现饱受诟病的空轮训bug会导致cpu飙升100%
  • 4、项目庞大之后,自行实现的NIO很容易出现各类bug,维护成本较高,上面这一坨代码我都不能保证没有bug

JDK的NIO犹如带刺的玫瑰,虽然美好,让人向往,但是使用不当会让你抓耳挠腮,痛不欲生,正因为如此,Netty横空出世!

一、Netty简介

直接摘自官网(https://netty.io/):

Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

Netty是一个NIO客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化并简化了TCP和UDP套接字服务器等网络编程。

“快速简便”并不意味着最终的应用程序会受到可维护性或性能问题的影响。Netty经过精心设计,具有丰富的协议,如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议。因此,Netty成功地找到了一种在不妥协的情况下实现易于开发,性能,稳定性和灵活性的方法。

二、Netty特征

1、设计

  • 适用于各种传输类型的统一API - 阻塞和非阻塞套接字
  • 基于灵活且可扩展的事件模型,可以清晰地分离关注点
  • 高度可定制的线程模型 - 单线程,一个或多个线程池,如SEDA
  • 真正的无连接数据报套接字支持(自3.1起)

2、便于使用

  • 详细记录的Javadoc,用户指南和示例
  • 没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了
    • 注意:某些组件(如HTTP / 2)可能有更多要求。 有关更多信息,请参阅 “要求”页面。

3、性能

  • 吞吐量更高,延迟更低
  • 减少资源消耗
  • 最小化不必要的内存复制

4、安全

  • 完整的SSL / TLS和StartTLS支持

5、社区

  • 早发布,经常发布
  • 自2003年以来,作者一直在编写类似的框架,他仍然觉得你的反馈很珍贵!

三、选Netty弃NIO的原因

  • 使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞
  • Netty底层IO模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从NIO模型变身为IO模型
  • Netty自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
  • Netty解决了JDK的很多包括空轮询在内的bug
  • Netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程模型做到非常高效的并发处理
  • 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
  • Netty社区活跃,遇到问题随时邮件列表或者issue
  • Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大

四、 Netty使用案例

1、引入Maven依赖

    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.6.Final</version>
    </dependency>

2、服务端实现部分

public class NettyServer {
    public static void main(String[] args) {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        NioEventLoopGroup boos = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
                .group(boos, worker)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    protected void initChannel(NioSocketChannel ch) {
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                .bind(8000);
    }
}

这么一小段代码就实现了我们前面NIO编程中的所有的功能,包括服务端启动,接受新连接,打印客户端传来的数据,怎么样,是不是比JDK原生的NIO编程优雅许多?

简单了解下

  • boos对应 IOServer.java中的接受新连接线程,主要负责创建新连接
  • worker对应 IOClient.java中的负责读取数据的线程,主要用于读取数据以及业务逻辑处理

3、客户端实现部分

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();

        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                });

        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

        while (true) {
            channel.writeAndFlush(new Date() + ": hello world!");
            Thread.sleep(2000);
        }
    }
}

在客户端程序中,group对应了我们IOClient.java中main函数起的线程,运行main函数,回到NettyServer.java的控制台,将会看到效果。

主要参考文章:https://www.jianshu.com/p/a4e03835921a

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值