Netty 实战万字详情

292 阅读9分钟

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 程序时做到心中有数。

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 N 天,点击查看活动详情