Netty 实战万字详解

简介: Netty 实战

Netty简介

Netty 是最流行的 NIO 框架,是基于 Java NIO 的异步事件驱动的网络应用框架。Netty 提供了简单易用的API从网络处理代码中解耦业务逻辑。Netty 是完全基于 NIO 实现的,所以整个 Netty 都是异步的。

许多框架和开源组件的底层 rpc 都是使用的 Netty,如 DubboElasticsearch 等等。下面是官网给出的一些 Netty 的特性:

设计方面

  • 对各种传输协议提供统一的 API(使用阻塞和非阻塞套接字时候使用的是同一个 API,只是需要设置的参数不一样)。
  • 基于一个灵活、可扩展的事件模型来实现关注点清晰分离。
  • 高度可定制的线程模型——单线程、一个或多个线程池。
  • 真正的无数据报套接字(UDP)的支持(since 3.1)。

易用性

  • 完善的 Javadoc 文档和示例代码。
  • 不需要额外的依赖,JDK 5 (Netty 3.x) 或者 JDK 6 (Netty 4.x) 已经足够。

性能

  • 更好的吞吐量,更低的等待延迟。
  • 更少的资源消耗。
  • 最小化不必要的内存拷贝。

安全性

  • 完整的 SSL/TLS 和 StartTLS 支持

对于初学者,上面的特性我们在脑中有个简单了解和印象即可, 下面开始我们的实战部分。

Netty实战

实现Http 服务器

开发环境: IDEA+Gradle+Netty4
添加依赖: compile 'io.netty:netty-all:4.1.26.Final'

第一个示例我们使用 Netty 编写一个 Http 服务器的程序,启动服务我们在浏览器输入网址来访问我们的服务,便会得到服务端的响应。功能很简单,下面我们看看具体怎么做?

服务启动类

public class HttpServer {
    public static void main(String[] args) {
        //构造两个线程组
        EventLoopGroup bossrGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //服务端启动辅助类
            ServerBootstrap bootstrap = new ServerBootstrap();
 
            bootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new HttpServerInitializer());
 
            ChannelFuture future = bootstrap.bind(8080).sync();
            //等待服务端口关闭
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 优雅退出,释放线程池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

注:

在编写 Netty 程序时,一开始都会生成 NioEventLoopGroup 的两个实例,分别是 bossGroupworkerGroup,也可以称为 parentGroupchildGroup,为什么创建这两个实例,作用是什么?可以这么理解,bossGroupworkerGroup 是两个线程池, 它们默认线程数为 CPU 核心数乘以 2,bossGroup 用于接收客户端传过来的请求,接收到请求后将后续操作交由 workerGroup 处理。

接下来我们生成了一个服务启动辅助类的实例 bootstrapboostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组。channel 方法用于指定服务器端监听套接字通道 NioServerSocketChannel,其内部管理了一个 Java NIO 中的ServerSocketChannel实例。

channelHandler 方法用于设置业务职责链,责任链是我们下面要编写的,责任链具体是什么,它其实就是由一个个的 ChannelHandler 串联而成,形成的链式结构。正是这一个个的 ChannelHandler 帮我们完成了要处理的事情。

接着我们调用了 bootstrap 的 bind 方法将服务绑定到 8080 端口上,bind 方法内部会执行端口绑定等一系列操,使得前面的配置都各就各位各司其职,sync 方法用于阻塞当前 Thread,一直到端口绑定操作完成。接下来一句是应用程序将会阻塞等待直到服务器的 Channel 关闭。

启动类的编写大体就是这样了,下面要编写的就是上面提到的责任链了。如何构建一个链,在 Netty 中很简单,不需要我们做太多,代码如下:

public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
    protected void initChannel(SocketChannel sc) throws Exception {
        ChannelPipeline pipeline = sc.pipeline();
        //处理http消息的编解码
        pipeline.addLast("httpServerCodec", new HttpServerCodec());
        //添加自定义的ChannelHandler
        pipeline.addLast("httpServerHandler", new HttpServerHandler());
    }
}

我们自定义一个类 HttpServerInitializer 继承 ChannelInitializer 并实现其中的 initChannel方法。

ChannelInitializer 继承 ChannelInboundHandlerAdapter,用于初始化 ChannelChannelPipeline。通过 initChannel 方法参数 sc 得到 ChannelPipeline 的一个实例。

当一个新的连接被接受时, 一个新的 Channel 将被创建,同时它会被自动地分配到它专属的 ChannelPipeline

ChannelPipeline 提供了 ChannelHandler 链的容器,推荐读者仔细自己看看 ChannelPipeline 的 Javadoc,文章后面也会继续说明 ChannelPipeline 的内容。

Netty 是一个高性能网络通信框架,同时它也是比较底层的框架,想要 Netty 支持 Http(超文本传输协议),必须要给它提供相应的编解码器。

所以我们这里使用 Netty 自带的 Http 编解码组件 HttpServerCodec 对通信数据进行编解码,HttpServerCodecHttpRequestDecoderHttpResponseEncoder 的组合,因为在处理 Http 请求时这两个类是经常使用的,所以 Netty 直接将他们合并在一起更加方便使用。所以对于上面的代码:

pipeline.addLast("httpServerCodec", new HttpServerCodec())

通过 addLast 方法将一个一个的 ChannelHandler 添加到责任链上并给它们取个名称(不取也可以,Netty 会给它个默认名称),这样就形成了链式结构。在请求进来或者响应出去时都会经过链上这些 ChannelHandler 的处理。

最后再向链上加入我们自定义的 ChannelHandler 组件,处理自定义的业务逻辑。下面就是我们自定义的 ChannelHandler

public class HttpServerChannelHandler0 extends SimpleChannelInboundHandler<HttpObject> {
    private HttpRequest request;
 
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        if (msg instanceof HttpRequest) {
            request = (HttpRequest) msg;
            request.method();
            String uri = request.uri();
            System.out.println("Uri:" + uri);
        }
        if (msg instanceof HttpContent) {
 
            HttpContent content = (HttpContent) msg;
            ByteBuf buf = content.content();
            System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));
 
            ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
            response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
 
            ctx.writeAndFlush(response);
 
        }
    }
}

至此一个简单的 Http 服务器就完成了。首先我们来看看效果怎样,我们运行 HttpServer 中的 main 方法。让后使用 Postman 这个工具来测试下,使用 post 请求方式(也可以 get,但没有请求体),并一个 json 格式数据作为请求体发送给服务端,服务端返回给我们一个hello world字符串。

总结

对于自定义的 ChannelHandler, 一般会继承 Netty 提供的SimpleChannelInboundHandler类,并且对于 Http 请求我们可以给它设置泛型参数为 HttpOjbect 类,然后覆写 channelRead0 方法,在 channelRead0 方法中编写我们的业务逻辑代码,此方法会在接收到服务器数据后被系统调用。

Netty 的设计中把 Http 请求分为了 HttpRequestHttpContent 两个部分,HttpRequest 主要包含请求头、请求方法等信息,HttpContent 主要包含请求体的信息。

所以上面的代码我们分两块来处理。在 HttpContent 部分,首先输出客户端传过来的字符,然后通过 Unpooled 提供的静态辅助方法来创建未池化的 ByteBuf 实例, Java NIO 提供了 ByteBuffer 作为它的字节容器,Netty 的 ByteBuffer 替代品是 ByteBuf。

接着构建一个 FullHttpResponse 的实例,并为它设置一些响应参数,最后通过 writeAndFlush 方法将它写回给客户端。

上面这样获取请求和消息体则相当不方便,Netty 又提供了另一个类 FullHttpRequestFullHttpRequest 包含请求的所有信息,它是一个接口,直接或者间接继承了 HttpRequest 和 HttpContent,它的实现类是 DefalutFullHttpRequest

因此我们可以修改自定义的 ChannelHandler 如下:

public class HttpServerChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
 
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
 
        ctx.channel().remoteAddress();
 
        FullHttpRequest request = msg;
 
        System.out.println("请求方法名称:" + request.method().name());
 
        System.out.println("uri:" + request.uri());
        ByteBuf buf = request.content();
        System.out.print(buf.toString(CharsetUtil.UTF_8));
 
 
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
        response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain");
        response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
 
        ctx.writeAndFlush(response);
    }
}

这样修改就可以了吗,如果你去启动程序运行看看,是会抛异常的。前面说过 Netty 是一个很底层的框架,对于将请求合并为一个 FullRequest 是需要代码实现的,然而这里我们并不需要我们自己动手去实现,Netty 为我们提供了一个 HttpObjectAggregator 类,这个 ChannelHandler作用就是将请求转换为单一的 FullHttpReques。

所以在我们的 ChannelPipeline 中添加一个 HttpObjectAggregator 的实例即可。

public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
    protected void initChannel(SocketChannel sc) {
        ChannelPipeline pipeline = sc.pipeline();
        //处理http消息的编解码
        pipeline.addLast("httpServerCodec", new HttpServerCodec());
        pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
        //添加自定义的ChannelHandler
        pipeline.addLast("httpServerHandler", new HttpServerChannelHandler0());
    }
}

实现Netty 客户端

上面的两个示例中我们都是以 Netty 做为服务端,接下来看看如何编写 Netty 客户端,以第一个 Http 服务的例子为基础,编写一个访问 Http 服务的客户端。

public class HttpClient {

   public static void main(String[] args) throws Exception {
       String host = "127.0.0.1";
       int port = 8080;

       EventLoopGroup group = new NioEventLoopGroup();

       try {
           Bootstrap b = new Bootstrap();
           b.group(group)
           .channel(NioSocketChannel.class)
           .handler(new ChannelInitializer<SocketChannel>() {
               @Override
               public void initChannel(SocketChannel ch) throws Exception {
                   ChannelPipeline pipeline = ch.pipeline();
                   pipeline.addLast(new HttpClientCodec());
                   pipeline.addLast(new HttpObjectAggregator(65536));
                   pipeline.addLast(new HttpClientHandler());
               }
           });

           // 启动客户端.
           ChannelFuture f = b.connect(host, port).sync();
           f.channel().closeFuture().sync();

       } finally {
           group.shutdownGracefully();
       }
   }
}

客户端启动类编写基本和服务端类似,在客户端我们只用到了一个线程池,服务端使用了两个,因为服务端要处理 n 条连接,而客户端相对来说只处理一条,因此一个线程池足以。

然后服务端启动辅助类使用的是 ServerBootstrap,而客户端换成了 Bootstrap。通过 Bootstrap 组织一些必要的组件,为了方便,在 handler 方法中我们使用匿名内部类的方式来构建 ChannelPipeline 链容器。最后通过 connect 方法连接服务端。

接着编写 HttpClientHandler 类。

public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
 
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        URI uri = new URI("http://127.0.0.1:8080");
        String msg = "Are you ok?";
        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
                uri.toASCIIString(), Unpooled.wrappedBuffer(msg.getBytes("UTF-8")));
 
        // 构建http请求
//        request.headers().set(HttpHeaderNames.HOST, "127.0.0.1");
//        request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
        // 发送http请求
        ctx.channel().writeAndFlush(request);
    }
 
    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) {
 
        FullHttpResponse response = msg;
        response.headers().get(HttpHeaderNames.CONTENT_TYPE);
        ByteBuf buf = response.content();
        System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));
 
    }
}

在 HttpClientHandler 类中,我们覆写了 channelActive 方法,当连接建立时,此方法会被调用,我们在方法中构建了一个 FullHttpRequest 对象,并且通过 writeAndFlush 方法将请求发送出去。

channelRead0 方法用于处理服务端返回给我们的响应,打印服务端返回给客户端的信息。至此,Netty 客户端的编写就完成了,我们先开启服务端,然后开启客户端就可以看到效果了。

希望通过前面介绍的几个例子能让大家基本知道如何编写 Netty 客户端和服务端,下面我们来说说 Netty 程序为什么是这样编写的,这也是 Netty 中最为重要的一部分知识,可以让你在编写 netty 程序时做到心中有数。

相关文章
|
6月前
Netty实战: HTTP文件列表服务器
Netty实战: HTTP文件列表服务器
70 0
|
Java
由浅入深Netty组件实战3
由浅入深Netty组件实战3
61 0
|
前端开发 算法 Java
由浅入深Netty组件实战2
由浅入深Netty组件实战2
222 0
|
缓存 安全 Java
由浅入深Netty基础知识NIO三大组件原理实战 2
由浅入深Netty基础知识NIO三大组件原理实战
74 0
|
Java
由浅入深Netty基础知识NIO三大组件原理实战 1
由浅入深Netty基础知识NIO三大组件原理实战
101 0
|
6月前
|
Java Unix Linux
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
当涉及到网络通信和高性能的Java应用程序时,Netty是一个强大的框架。它提供了许多功能和组件,其中之一是JNI传输。JNI传输是Netty的一个特性,它为特定平台提供了高效的网络传输。 在本文中,我们将深入探讨Netty提供的特定平台的JNI传输功能,分析其优势和适用场景。我们将介绍每个特定平台的JNI传输,并讨论其性能、可靠性和可扩展性。通过了解这些特定平台的JNI传输,您将能够更好地选择和配置适合您应用程序需求的网络传输方式,以实现最佳的性能和可靠性。
145 7
【Netty技术专题】「原理分析系列」Netty强大特性之Native transports扩展开发实战
|
前端开发 安全 Java
由浅入深Netty组件实战1
由浅入深Netty组件实战1
83 0
|
6月前
|
网络协议 Java 测试技术
阿里内部Netty实战小册,值得拥有
Netty 是一个高性能的 Java 网络通信框架,简化了网络编程并涵盖了最新的Web技术。它提供了一种抽象,降低了底层复杂性,使得更多开发者能接触网络编程。Netty 因其易用性、高效性和广泛的应用场景受到推崇,适合互联网行业从业者学习,有助于理解和开发基于Netty的系统。免费的《Netty实战小册》详细介绍了Netty的各个方面,包括概念、架构、编解码器、网络协议和实际案例,帮助读者深入理解和应用Netty。如需完整版小册,可点击链接获取。
阿里内部Netty实战小册,值得拥有
|
3月前
|
前端开发 网络协议
Netty实战巅峰:从零构建高性能IM即时通讯系统,解锁并发通信新境界
【8月更文挑战第3天】Netty是一款高性能、异步事件驱动的网络框架,适用于开发高并发网络应用,如即时通讯(IM)系统。本文将指导你利用Netty从零构建高性能IM程序,介绍Netty基础及服务器/客户端设计。服务器端使用`ServerBootstrap`启动,客户端通过`Bootstrap`连接服务器。示例展示了简单的服务器启动过程。通过深入学习,可进一步实现用户认证等功能,打造出更完善的IM系统。
150 1
|
3月前
|
编解码 NoSQL Redis
(十一)Netty实战篇:基于Netty框架打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。