认识Netty

前言

上文介绍了几种IO模型以及Java NIO,了解了在网络编程时使用哪种模型可以提高系统性能及效率。即使Java NIO可以帮助开发人员编写和维护网络应用程序,但由于其复杂性以及bug问题,还是诞生很多强大和流行的网络编程框架,比如Netty、Undertow、Grizzly,在平时的开发中大家更倾向于选择这些框架进行开发,而在我们学习和理解网络编程的底层原理时,使用Java NIO可以更加直接和深入地了解底层操作。本文对 Netty 进行简单介绍,并通过 Netty 实现一个HTTP服务器。

Netty介绍

Netty是一个基于Java的高性能网络应用框架,它封装了Java NIO的复杂性,提供了简单而强大的网络编程API,使得开发者能够更方便地构建高性能、可伸缩的网络应用程序。所以说学习Netty前先理解一下Java NIO是很有必要的,不然云里雾里的。

使用Netty能有多强大呢?包括但不限于以下几点:

  1. 高性能的IO处理:如果基于Java NIO开发一个成熟的应用,要非常注意如ByteBuffer内存泄漏、Channel注册连接、线程管理等问题。而Netty能够更好地处理连接管理、线程模型和内存管理等方面的问题,从而提供更高的吞吐量和更低的延迟。
  2. 强大的功能扩展:如果基于Java NIO写一个HTTP协议、Websocket协议,那我们需要考虑格式、编解码问题,而Netty提供提供了丰富的扩展点,比如编解码器、处理器和拦截器等,开发人员可以通过不同的配置搭建HTTP、WebSocket、TCP和UDP等协议,也可以轻松地添加编解码器,实现自定义协议。
  3. 可靠性和稳定性:Netty具有良好的容错能力和稳定性,能够处理各种网络故障和异常情况,并提供了多种容错和恢复机制,如断线重连和心跳机制等。

总的来说,在开发网络应用程序时使用Netty能够更专注于业务逻辑。下图为Netty所支持的功能

Netty发展历程

为了进一步了解Netty,这里介绍一下Netty的前世今生。

  • 2004年:Netty的前身Jboss Netty项目在JBoss公司内部启动,目标是提供一个可扩展的、易用的网络编程框架。
  • 2008年:Netty项目在JBoss公司内部开源,并发布了第一个公开版本Netty 3.0。该版本主要针对TCP和HTTP协议进行了支持。
  • 2011年:Netty 3.2发布,引入了更多的特性和改进,包括更好的性能和更灵活的API设计。
  • 2012年:Netty 4.0发布,这是一个重大的里程碑版本。在这个版本中,Netty进行了全面的重构和优化,引入了新的API设计和更高级的特性。
  • 2013年:Netty 4.0获得了广泛的认可和采用,并成为了许多大型互联网公司和项目的首选网络编程框架。同年底发布了5.0.0.Alpha1,目标是对Netty 4改进和优化。
  • 2015年:Netty 5在开发过程中遇到了一些挑战和技术难题,决定暂停Netty 5的开发,并将重心转移到Netty 4的改进和维护上。
  • 2016年:Netty 4.1发布,基于4.0版本进一步改进和优化,提供了更好的性能和更多的功能。

目前有很多知名的项目都选用了Netty作为网络通信的基础,比如知名的RPC框架Dubbo、gRPC,消息队列Kafka、RocketMQ,搜索引擎Elasticsearch等,所以当学习了解这些项目时,Netty会作为一个加分项。

Netty核心组件

因为Netty是基于Java NIO封装的,更加的抽象,要使用Netty进行开发,必须要熟悉Netty中的几个核心组件,下面一一介绍:

  1. Channel(通道):与Java NIO中的SocketChannel一样,可以进行数据的读取和写入。
  2. EventLoop(事件循环):EventLoop是Netty的事件处理机制,它负责处理各种事件,包括连接的建立与关闭、数据的读取与写入等。可以理解成Java NIO中的Selector监听socket得事件,只不过Netty时多线程处理,后面代码中有体现。
  3. ChannelHandler(通道处理器) :用来处理Channel中的事件和数据的组件,例如对数据编解码、业务逻辑处理等。Netty提供了许多内置的ChannelHandler,用于处理网络连接和I/O操作。以下是一些常用的ChannelHandler:
    • ChannelInboundHandler:用于处理入站事件,例如连接建立、数据读取等。
    • ChannelOutboundHandler:用于处理出站事件,例如数据写入、连接关闭等。
    • SimpleChannelInboundHandler:继承自ChannelInboundHandler,简化了消息处理的逻辑。
    • SimpleChannelOutboundHandler:继承自ChannelOutboundHandler,简化了消息发送的逻辑。
    • HttpServerCodec:它负责处理 HTTP 请求和响应的编解码。
    • HttpObjectAggregator:将 HTTP 请求的多个部分合并成一个完整的 FullHttpRequest。
    • WebSocketServerProtocolHandler:处理 WebSocket 协议的握手和帧的编解码。
  4. ChannelPipeline(通道管道):ChannelPipeline是一个事件处理器链,用于管理和执行ChannelHandler,每个Channel都有一个对应的Pipeline,当数据进入或离开Channel时,会经过Pipeline中的一系列ChannelHandler进行处理。
  5. ByteBuf(字节缓冲区):ByteBuf是Netty中的字节容器,用于高效地存储和传输字节数据。与Java NIO的ByteBuffer相比,ByteBuf提供了更灵活的API和更高效的内存管理。
  6. Future(异步操作结果):Netty中的操作都是异步的,Future用来获取操作的状态和结果。
  7. Bootstrap(引导类):Bootstrap是启动客户端的类,负责配置和启动客户端的相关组件。
  8. ServerBootstrap(服务器引导类):ServerBootstrap是创建和启动服务器的类,用于配置和管理服务器的各个组件。

实现HTTP服务器

下面以HTTP协议为例,用Netty编写一个HTTP服务器。在这之前,我们先用上篇文章的NIOServer接收一下浏览器的请求,大概是这样的:

yaml 复制代码
GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

可以看到接收到了一个HTTP请求的报文数据,有请求行、请求头和请求主体,这时候也能看到浏览器返回的响应是:ERR_INVALID_HTTP_RESPONSE,发送的响应无效。为什么?这是因为NIOServer中的输出格式HTTP协议不认识。 所以如果使用Java NIO实现一个HTTP服务器,需要处理很多的工作,但是如果用Netty实现一个HTTP服务器非常简单,直接上代码:

java 复制代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.CharsetUtil;

import java.util.List;
import java.util.Map;

public class HttpServer {

    private final int port;

    public HttpServer(int port) {
        this.port = port;
    }

    public void start() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline p = ch.pipeline();
                    //将原始的HTTP请求和响应数据进行编解码
                    p.addLast(new HttpServerCodec());
                    //将HTTP请求或响应的多个部分合并成一个完整的消息
                    p.addLast(new HttpObjectAggregator(65536));
                    p.addLast(new SimpleChannelInboundHandler<FullHttpRequest>() {
                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
                            //查询URI中的参数:
                            QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
                            Map<String, List<String>> params = decoder.parameters();
                            System.out.println(params);
                            ByteBuf content = request.content();
                            //请求主体中的参数
                            String requestBody = content.toString(CharsetUtil.UTF_8);
                            System.out.println(requestBody);
                            String responseContent = "你好, Netty!";
                            FullHttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, Unpooled.wrappedBuffer(responseContent.getBytes()));
                            response.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_HTML + ";charset=utf-8");
                            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
                            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
                        }
                    });
                }
            });

            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        HttpServer server = new HttpServer(port);
        System.out.println("Server started on port " + port);
        server.start();
    }
}

运行这个示例后,你可以使用浏览器或者其他工具发送HTTP请求到 http://localhost:8080 ,一个HTTP服务器就诞生了,非常简单。接下来对代码进行讲解:

代码中的b.group(bossGroup, workerGroup)意思是有两个线程组会去处理服务器中的IO事件,bossGroup只用一个线程来专门负责监听服务端的端口,接收客户端连接请求,并将连接分配给 workerGroup 中的 EventLoop 进行处理;workerGroup负责处理已接收的连接的 I/O 事件,将请求解码、处理业务逻辑以及发送响应等操作都交给 EventLoop 来处理。这个是典型的主从Reactor模式,通过将连接的接收和处理分离到不同的线程池中,可以提高网络应用程序的性能,模型如下。

NioServerSocketChannel是指定服务器的Channel类型,还有NioDatagramChannel等类型,取决于应用场景。

.handler(new LoggingHandler(LogLevel.INFO))是为bossGroup指定一个通道处理器,记录进出 Channel 的数据流,将相关信息打印到日志中,便于排查。

.childHandler()则是为workerGroup中的EventLoop配置处理器,比如请求解码、处理业务逻辑以及发送响应。ChannelPipeline就是添加具体的通道处理器,代码中的HttpServerCodecHttpObjectAggregator处理器都是用来处理HTTP请求的编解码,SimpleChannelInboundHandler则是拿到经过多个处理器的数据流后进行业务逻辑及响应。

总结

Netty是一个非常优秀的、强大的、高性能的网络通信框架,在这个互联网飞速发展的时代,我们需要了解并且使用像这样的优秀的框架,帮助我们快速开发应用,在使用它的同时要知其原理,也可以在业务中进行创新,就像Dubbo、gRPC、Zookeeper一样采用Netty成为与其一样优秀的框架。

相关推荐
曹牧20 分钟前
文档格式:OFD
java
豆角焖肉1 小时前
Maven进阶与搭建私服
java·maven
大不点wow1 小时前
Java序列化与反序列化:让对象走出JVM
java·开发语言·jvm
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
噢,我明白了1 小时前
Java中日期和字符串的处理
java·开发语言·日期
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
dkbnull1 小时前
Spring Boot请求处理组件对比详解
java·spring boot
jun_bai1 小时前
Orthanc服务器使用java上传dicom影像文件
java·运维·服务器