手写RPC框架系列(七) - Netty 快速入门
手写RPC框架系列(七) - Netty 快速入门
前一章节我们介绍了基本的通信相关的基础知识,有了基础知识的铺垫有利于本章节的学习。
本章节包含的大致内容如下:
- 理解 Netty 是什么以及核心组件
- 为什么要选用 Netty
- 快速上手 Netty
- 总结
1. Netty 是什么
引用一段维基对 Netty 的介绍:
Netty 是一个非阻塞I/O客户端-服务器框架,主要用于开发 Java 网络应用程序,如协议服务器和客户端。异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如 TCP 和 UDP 套接字服务器。Netty 包括了反应器编程模式的实现。Netty 最初由JBoss开发,现在由 Netty 项目社区开发和维护。
从这段介绍中我们可以总结出几点:
- 非阻塞I/O客户端-服务器框架
- 用于开发 Java 语言的网络应用程序
- 支持 TCP和UDP套接字服务器
- 基于反应器编程模式的实现
- Netty 最初由 JBoss 开发,现在由 Netty 项目社区开发和维护。
2. 为什么要选用 Netty
上个章节中,我们也对比过 Java 领域内比较常用的网络通信框架。 这三个框架分别是:
2.1 Java 原生 NIO
早先我接触过几个生产的项目就是用的原生的 Java NIO 编写,项目开发的比较早,稳定成熟的 NIO 框架选择不多,故项目组是使用原生的 Java NIO 编写。 在维护以及阅读代码过程中,可以发现明显的几个缺点:
- 代码很复杂,特别是各种状态的判断,如果处理不当的话就会出现阻塞的问题,在生产环境中碰到过几次。
- 开发效率不高,比较网络连接、粘包处理、编解码等大量的重复代码。
- 在 JDK 的前几个版本中,知名的 Epoll Bug 导致 CPU 使用率持续 100%,这也是一直被人吐槽的一个点。
2.2 Apache Mina
Apache Mina 也是一个比较流行的 Java 网络编程框架,出现的时间也比较早,第一次发布出来的版本是 MINA 0.7.1,发布时间在 23/May/2005 ( 版本链接 https://mina.apache.org/mina-project/downloads_old.html)。
此后的数年时间,Mina 项目都一直在更新,但是后面版本迭代的速度越来越慢。 主要一个原因还是社区不太活跃,导致项目更新迭代的更慢。
Mina 比较明显的缺点如下:
- 社区不活跃,更新迭代慢,最近几年几乎停止更新。
- Mina 在某些情况下会有较高的内存消耗,特别是高负载情况下。
- Mina 的 API 有些不太直观,上手起来比较复杂。
- 添加新协议支持会更复杂。
2.3 Netty
Netty 也是一个网络编程框架,出现的时间比 Mina 还要早,并且作者都是同一个人。 目前 Netty 被广泛用于通讯类的产品上,包括一些知名的大数据组件基本都是采用 Netty 进行通信。
Netty 的缺点如下:
- Netty 中错误处理因为是在异步编程模型中,所以会比较复杂。 正确地处理以及传播错误需要对框架有比较深刻的理解。
- 需要谨慎管理资源,典型的就是 ButeBuf 的分配已经释放,如果处理不当会导致内存泄漏。
- 配置选项太多,虽然说灵活,但是配置起来比较复杂。
Netty 的优点如下:
- 高性能以及稳定性,这个已经在全世界海量的生产应用中得到了验证。
- 简单易用, Netty 抽象出了一套高层的 API,隐藏了 Java NIO 的复杂性,可以快速实现复杂的网路通信。
- 多协议支持,常用的通信协议基本都已经支持。
- 社区活跃,文档丰富,这可以解决大部分开发中遇到的问题。
通过上面的介绍,我们可以选择基于 Netty 来开发 RPC 框架网络通信部分。
3. Netty 的基本用法以及快速入门
这里我们将不会详细深入的讲解 Netty ,毕竟我们的重点是侧重于使用 RPC 的网络通信。
通常开发一个 Netty 应用分为以下几个步骤:
- 引入Netty依赖:需要在 pom.xml 中添加如下内容:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
</dependency>
创建 Bootstrap 实例: Netty中,服务端跟客户端创建的 Bootstrap 实例是不一样的,服务端创建的是 ServerBootstrap ,而客户端创建的是 Bootstrap 。
配置 Channel: 选择合适的 Channel 类型,例如 NioServerSocketChannel,并配置 ChnanelPipleline 。 ChannelPipeline 主要是管理数据处理的各个阶段,你可以添加自定义的处理器 ( ChannelHandler ) 来处理业务。
编写对应的业务逻辑处理器: 编写一个或者多个 ChannelHandler 来处理输入的数据,比方说在 channelRead 方法中对收到的数据进行处理。
启动 Bootstrap : 如果服务端的应用,则调用 Bootstrap.bind(端口) 进行服务端口的绑定,客户端的应用则是通过 Bootstrap.connect(目的地址 + 端口) 进行连接创建。
通过以上几个简单的步骤,你就可以创建对应的 Netty 服务端以及客户端的应用,比原生的 Java NIO 大幅度的降低了工作量以及学习成本。
下面我们快速通过一个 HelloWorld 例子来实际上手:
3.1 编写 HelloWorld 服务端
首先创建一个简单的服务端,它接收连接并返回 “Hello, world!“。
import io.netty.bootstrap.ServerBootstrap;
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.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class HelloWorldServer {
private int port;
public HelloWorldServer(int port) {
this.port = port; // 服务器端口
}
public void start() {
// 用于接受客户端连接的线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 用于处理已接受的连接的线程组
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // 指定使用NIO的通信模式
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 为新连接的Channel注册处理器:先解码、编码,再处理业务逻辑
ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("Received: " + msg); // 打印接收到的消息
ctx.writeAndFlush("Hello, world!"); // 发送响应消息给客户端
}
});
}
})
.option(ChannelOption.SO_BACKLOG, 128) // 设置TCP连接的队列长度
.childOption(ChannelOption.SO_KEEPALIVE, true); // 设置保持连接状态
// 绑定端口并启动服务器
ChannelFuture f = b.bind(port).sync();
// 等待服务器socket关闭
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 优雅停机
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
// 服务端的端口设置为 8080
int port = 8080;
new HelloWorldServer(port).start();
}
}
3.2 编写 HelloWorld 客户端
这是一个简单的客户端,它连接到服务器,在连接成功后发送一个消息并接收响应。
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class HelloWorldClient {
private final String host;
private final int port;
public HelloWorldClient(String host, int port) {
this.host = host; // 服务器的IP地址
this.port = port; // 服务器的端口
}
public void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup(); // 创建客户端处理线程组
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class) // 使用NIO通信模式
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
// 设置pipeline,处理网络IO
ch.pipeline().addLast(new StringDecoder(), new StringEncoder(), new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
// 打印服务器的响应
System.out.println("Server replied: " + msg);
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
// 连接成功后,立马给服务端发送消息
ctx.writeAndFlush("Hello from client!");
}
});
}
});
// 连接到服务器
ChannelFuture f = b.connect(host, port).sync();
// 等待连接关闭
f.channel().closeFuture().sync();
} finally {
// 关闭线程组
group.shutdownGracefully();
}
}
public static void main(String[] args) throws InterruptedException {
// 连接到服务端 localhost:8080
new HelloWorldClient("localhost", 8080).start();
}
}
3.3 运行效果
3.3.1 服务端输出
当客户端连接并发送消息
"Hello from client!"
时,服务端的 channelRead0 方法会被触发,从而接收并打印这条消息。因此,服务端会输出:Received: Hello from client!
然后服务端会向客户端发送响应 “Hello, world!“。
3.3.2 客户端输出
客户端启动并连接到服务端后,其 channelActive 方法被触发,客户端发送 “Hello from client!” 到服务端。
当服务端响应 “Hello, world!” 到客户端时,客户端的 channelRead0 方法会被触发,从而接收并打印这条消息。因此,客户端会输出:
Server replied: Hello, world!
这样的输出显示了一个基本的客户端-服务端通信,其中客户端发送了一个初始消息并接收了来自服务端的响应。服务端接收了客户端的消息并发送了一个响应。掌握上面的这种模式是接下来编写 RPC 通信部分的基础。
4. 总结
通过这个章节的学习,我们对基本的 Java 领域的网络通信框架进行对比以及选型,同时上手了一个 Netty 的 HelloWorld 例子。有了这部分基础后,下一章节我们将开始使用 Netty 开发 RPC 相关的通讯部分。
5. 参考链接
[1] https://mina.apache.org Mina 官网
[2] https://mina.apache.org/mina-project/road-map.html Mina的起源
[3] https://netty.io Netty 官网
[4] https://netty.io/wiki/user-guide-for-4.x.html Netty4 的用户文档