Hello Netty World

Posted Qiyuanc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hello Netty World相关的知识,希望对你有一定的参考价值。

Netty 的 Hello World。

Hello Netty World

Netty 介绍

(官网)Netty is an asynchronous event-driven network application framework,
for rapid development of maintainable high performance protocol servers & clients.
(翻译)Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

先说结论,Netty 可以干什么:通常,我们使用 SpringBoot 搭建服务器,可以在网页上通过 URL 发出请求,服务器处理请求后返回响应,此时客户端和服务器是通过 HTTP 协议交换数据的;而使用 Netty,我们可以基于客户端和服务器之间的字节流,自定义编码解码规则(网络协议),从而实现自定义网络协议的应用程序。

再来说说 Netty 是什么:官网的介绍是,Netty 是一个网络应用程序框架(类似 SpringBoot 是 Web 应用程序的框架),即用于网络编程的一个框架。涉及到网络编程,就少不了网络编程底层的套接字(Socket),原生的 Socket 编程就不说了,Java 中已经提供了 NIO(Non-Blocking IO,非阻塞 IO)用于高性能的网络编程,那 NIO 和 Netty 有什么关系呢?再说一下结论:Netty 也是一种 NIO 框架,但对比 Java 原生的 NIO 和其他 NIO 框架,它更好使。

接着对比一下 NIO 和 Netty 的区别:

先看 NIO 的缺点:

  1. API 复杂,作为 Java 直接提供的 API,NIO 的 API 封装度较低,使用起来比较繁杂,学习成本高,需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等对象;

  2. 使用 NIO 需要对多线程和网络编程较为了解,因为 NIO 涉及到 Reactor 模式(也叫 Dispatcher 模式,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程),没有良好的基础很难使用 NIO 写出健康的程序。

  3. NIO 还会有一些自带的 BUG,如 epoll bug,会导致 Selector 空轮询造成 CPU 占用 100%(最终是修复了,但影响力太大,臭名昭著)。

与之相比,Netty 就具有相对的优点:

  1. 底层封装了 NIO,API 简单,非常容易上手;

  2. 内置多种编码器解码器,支持多种协议,功能强大;

  3. 对比原生 NIO 和其他 NIO 框架,Netty 的性能是最好的;

  4. 社区活跃,使用者多,出现 BUG 的修复速度也非常快,质量有保证。

总之,对于构建应用来说,即使没有任何 NIO 相关知识,直接使用 Netty 也不会有任何负担。当然,最好的还是能了解底层的原理。下面通过构建一个 Netty 的 Hello World 程序,就可以感受到 Netty 的强大。

Hello Netty

服务端

从0开始,创建一个 Maven 的 Java 项目,引入 Netty 的依赖:

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

然后,就可以直接开始构建服务器了,创建 MyServer 类,代码如下:

public class MyServer 
    public static void main(String[] args) throws Exception 
        // 创建两个线程组 boosGroup、workerGroup(步骤1)
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try 
            // 创建服务端的启动对象 ServerBootstrap,设置启动参数(步骤2)
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 设置上面创建的线程组 boosGroup 和 workerGroup
            bootstrap.group(bossGroup, workerGroup)
                    // 设置服务端的通道实现类型
                    .channel(NioServerSocketChannel.class)
                    // 设置线程队列的连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // 设置保持活动连接状态
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    // 使用匿名内部类的形式初始化通道对象 并设置处理器(步骤3,处理器当然是自己写,自定义业务流程)
                    .childHandler(new ChannelInitializer<SocketChannel>() 
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception 
                            // 为服务端 添加一个处理器
                            socketChannel.pipeline().addLast(new MyServerHandler());
                        
                    );
            System.out.println("Netty 服务端启动-Hello Server!");
            // 绑定端口号 服务端启动(步骤4)
            ChannelFuture channelFuture = bootstrap.bind(7777).sync();
            // 监听关闭通道
            channelFuture.channel().closeFuture().sync();
         finally 
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        
    

简单归纳一下创建服务器的步骤:

  1. 创建两个线程组对象 bossGroup 和 workerGroup;

  2. 创建服务端的启动对象 ServerBootstrap,设置线程组、启动参数;

  3. 为服务端的通道设置处理器(顾名思义,此处就是自定义处理数据的地方了);

  4. 为服务端绑定端口,启动服务。

可见服务端的启动非常简单,步骤1、2、4可以说都是固定步骤,只有在步骤3中,需要添加处理数据的通道处理器,此处的 MyServerHandler 就是自定义的,代码如下:

public class MyServerHandler extends ChannelInboundHandlerAdapter 

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
        // Read时触发
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到客户端" + ctx.channel().remoteAddress() + "发送的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception 
        // ReadComplete后触发,发送消息给客户端
        ctx.writeAndFlush(Unpooled.copiedBuffer("服务端收到,你是" + ctx.channel().remoteAddress(), CharsetUtil.UTF_8));
    

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception 
        // 发生异常关闭通道
        ctx.close();
    

MyServerHandler 类继承了 ChannelInboundHandlerAdapter,它就是一个处理器了。其中,重写了 channelRead 等方法,这些方法都会在对应的场合被调用,如 channelRead,对于接收到的数据 MyServerHandler 会调用 channelRead 方法处理数据,处理完成后又会调用 channelReadComplete 方法。

此处可以看到,channelRead 中读取了数据,在服务端打印了收到的数据(将数据转化为字节流后再转 Sting 读取),读取完后,又由 channelReadComplete 方法给客户端发送回复。

其中用到了 ChannelHandlerContext 对象,GPT 给出的介绍如下:

ChannelHandlerContext(通道处理上下文)是 Netty 中处理通道事件的核心接口,它封装了 Channel 和 ChannelHandler 之间的关联关系,以及 ChannelHandler 之间的交互操作。每当 Netty 中的通道(Channel)被激活时,会生成一个 ChannelHandlerContext 对象,并通过 ChannelPipeline 向通道处理器 ChannelHandler 传递事件和数据,让它们能够对通道进行处理。
简单来说,ChannelHandlerContext 提供了一种 Channel 和 ChannelHandler 之间进行通信和交互的方式,可以通过它来访问 Channel、调用 ChannelPipeline 中的其他处理器等。

客户端

完成了服务端,就到客户端了,与服务端类似,代码如下:

public class MyClient 

    public static void main(String[] args) throws Exception 
        // 创建 NIO 线程组(步骤1)
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try 
            // 创建客户端的启动对象 Bootstrap,设置启动参数(步骤2)
            Bootstrap bootstrap = new Bootstrap();
            // 设置线程组
            bootstrap.group(eventExecutors)
                    // 设置客户端的通道实现类型
                    .channel(NioSocketChannel.class)
                    // 使用匿名内部类初始化通道,并设置处理器(步骤3,处理器也是自定义的)
                    .handler(new ChannelInitializer<SocketChannel>() 
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception 
                            // 为客户端 添加一个处理器
                            ch.pipeline().addLast(new MyClientHandler());
                        
                    );
            System.out.println("Netty 客户端到位-Hello Client!");
            // 设置 IP 和 端口 连接服务端,操作系统会给客户端分配一个空闲端口号
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7777).sync();
            // 对通道关闭进行监听
            channelFuture.channel().closeFuture().sync();
         finally 
            //关闭线程组
            eventExecutors.shutdownGracefully();
        
    

简单归纳一下创建客户端的步骤:

  1. 创建 NIO 线程组对象 eventExecutors;

  2. 创建客户端的启动对象 Bootstrap,设置线程组、启动参数;

  3. 为客户端的通道设置处理器(与客户端系统,自定义的数据处理);

  4. 为客户端设置连接 IP 和端口,启动连接。

可以看到,创建客户端的流程与服务端差不多,只是参数不太一样。自定义的 MyClientHandler 代码如下:

public class MyClientHandler extends ChannelInboundHandlerAdapter 

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception 
        // Active时触发(连接时),发送消息到服务端
        ctx.writeAndFlush(Unpooled.copiedBuffer("こんにちは、世界", CharsetUtil.UTF_8));
    

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 
        // Read时触发
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到服务端" + ctx.channel().remoteAddress() + "的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    

其中,channelActive 方法在连接时触发,向服务器发送消息;收到数据时调用 channelRead 处理数据,在客户端打印数据内容(字节流数据转 String)。

启动

完成了服务端和客户端后,就可以启动它们尝试一下了。首先启动服务端,直接运行 MyServer:

再运行 MyClient:

可以看到 MyClient 直接有输出内容了,这是因为启动连接成功后 MyClient 调用 channelActive 向 MyServer 发送了数据,MyServer 调用 channelRead 读取了数据,完成后又调用 channelReadComplete 向 MyClient 发送了数据,最后由 MyClient 的 channelRead 打印到了控制台上。

回到 MyServer 的控制台查看输出:

Hello Netty 程序完成,就是这么简单了。

Netty 中文教程 Hello World !详解

1.HelloServer 详解

    HelloServer首先定义了一个静态终态的变量---服务端绑定端口7878。至于为什么是这个7878端口,纯粹是笔者个人喜好。大家可以按照自己的习惯选择端口。当然了。常用的几个端口(例如:80,8080,843(Flash及Silverlight策略文件请求端口等等),3306(Mysql数据库占用端口))最好就不要占用了,避免一些奇怪的问题。

    HelloServer类里面的代码并不多。只有一个main函数,加上内部短短的几行代码。

    Main函数开始的位置定义了两个工作线程,一个命名为WorkerGroup,另一个命名为BossGroup。都是实例化NioEventLoopGroup。这一点和3.x版本中基本思路是一致的。Worker线程用于管理线程为Boss线程服务。

讲到这里需要解释一下EventLoopGroup,它是4.x版本提出来的一个新概念。类似于3.x版本中的线程。用于管理Channel连接的。在main函数的结尾就用到了EventLoopGroup提供的便捷的方法,shutdownGraceFully(),翻译为中文就是优雅的全部关闭。感觉是不是很有意思。作者居然会如此可爱的命名了这样一个函数。查看相应的源代码。我们可以在DefaultEventExecutorGroup的父类MultithreadEventExecutorGroup中看到它的实现代码。关闭了全部EventExecutor数组child里面子元素。相比于3.x版本这是一个比较重大的改动。开发者可以很轻松的全部关闭,而不需要担心出现内存泄露。

    在try里面实例化一个ServerBootstrap b。设置group。设置channel为NioServerSocketChannel。

设置childHandler,在这里使用实例化一个HelloServerInitializer类来实现,继承ChannelInitializer<SocketChannel>。内部的代码我们可以在前文的注视中大致了解一下,主要作用是设置相关的字节解码编码器,和代码处理逻辑。Handler是Netty包里面占很大一个比例。可见其的作用和用途。Handler涉及很多领域。HTTP,UDP,Socket,WebSocket等等。详细的部分在本章的第三节解释。

设置好Handler绑定端口7878,并调用函数sync(),监听端口(等待客户端连接和发送消息)。并监听端口关闭(为了防止线程停止)。

    最后finally我们要优雅的全部关闭服务端。^_^

 

2.HelloClient详解

    相比于服务端的代码。客户端要精简一些。

    客户端仅仅只需要一个worker的EventLoopGroup。其次是类似于ServerBootstrap的HandlerInitializer。

    唯一不同的可能就是客户端的connect方法。服务端的绑定并监听端口,客户端是连接指定的地址。Sync().channel()是为了返回这个连接服务端的channel,并用于后面代码的调用。

BufferedReader 这个是用于控制台输入的。不做详细的解释了就。大家都懂的。

当用户输入一行内容并回车之后。循环的读取每一行内容。然后使用writeAndFlush向服务端发送消息。

3.HandlerInitializer详解

         Handler在Netty中是一个比较重要的概念。有着相当重要的作用。相比于Netty的底层。我们接触更多的应该是他的Handler。在这里我将它剥离出来单独解释。

         ServerHandlerInitializer继承与ChannelInitializer<SocketChannel>需要我们实现一个initChannel()方法。我们定义的handler就是写在这里面。

    在最开始的地方定义了一个DelimiterBasedFrameDecoder。按直接翻译就是基于分隔符的帧解码器。再一次感觉框架的作者的命名,好直接好简单。详细的内容我们在后面的文章中在为大家详细的解释。目前大家知道他是以分隔符为分割标准的解码器就好了。

    也许有人会问分隔符是什么?我只能 !*_* :“纳尼 !!”。分隔符其实就是“\n”我们在学习C语言的时候最常用的的也许就是这个分隔符了吧。

    下面的则是StringDecoder 和StringEncoder。字符串解码器和字符串编码器。

    最后面则是我们自己的逻辑。服务/客户端逻辑是在消息解码之后处理的。然后服务/客户端返回相关消息则是需要对消息进行相对应的编码。最终才是以二进制数据流的形势发送给服务/客户端的。

以上是关于Hello Netty World的主要内容,如果未能解决你的问题,请参考以下文章

又见“Hello World”,第一个Netty示例!

Netty 中文教程 Hello World !详解

java 使用netty搭建tcp服务器(hello world)

Netty-入门

HttpServer性能比较

为啥“hello” + + '/' + “world” == “hello47world”?