Netty 入门案例
1. 前言
本节主要是使用 Netty 来开发服务端和客户端,Netty 的开发模式基本上都是主启动类 + 自定义业务 Handler,Netty 是基于责任链的模式来管理自定义部分的 Handler,本节带大家感受一下 Netty 的开发。
需求: 本节主要通过 Netty 来实现我们的第一个 Demo,主要功能是分别建立两个项目(客户端和服务端),客户端向服务端发送 Hello World
,服务端接受到数据之后打印到控制台,并且给客户端响应。
2. 环境搭建
第一步: 使用 Maven 构建工程,项目结构如下:
第二步: netty-demo-client
和 netty-demo-server
两个工程的 pom.xml 导入 Netty 坐标,Netty 主流有三个版本,分别是 3.x、4.x、5.x,一般主流都是使用 4.x 版本。
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
第三步: netty-demo-client 工程相关类
建立两个类,分别是客户端启动类 NettyClient.java
和业务处理类 NettyClientHandler.java
。
第四步: netty-demo-server 工程相关类
建立两个类,分别是服务端启动类 NettyServer.java
和业务处理类 NettyServerHandler.java
3. 核心流程
客户端和服务端通信流程图如下图所示:
核心步骤说明:
- 在 NettyClientHandler 的 channelActive () 方法往服务端发送消息;
- 在 NettyServerHandler 的 channelRead () 方法接受消息,并且响应消息给客户端;
- 在 NettyClientHandler 的 channelRead () 方法接受服务端的响应消息。
4. 如何自定义 Handler
在 Netty 的开发当中,最核心就是自定义 Handler,通常根据不同的业务定义不同的 Handler。自定义 Handler 一般分为三个核心步骤:
- 需要继承
ChannelInboundHandlerAdapter
类;
实例:
public class TestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//业务处理
}
}
- 重写几个核心的方法,其中 channelRead () 是业务逻辑编写,使用最多;
方法名称 | 触发时机 | 常见业务场景 |
---|---|---|
channelActive | 连接就绪时触发 | 连接时进行登录认证 |
channelRead | 通道有数据可读取时触发 | 读取数据并且做处理,这个是用的最多的方法 |
channelInactive | 连接断开时触发 | 连接断开,删除服务端对于的 Session 关系;也可以在这里实现断开重新 |
exceptionCaught | 发生异常时触发 | 发生日常时,做日志记录 |
- 把 Handler 添加到 Pipeline 管道里面
实例:
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
//自定义业务 Handler
ch.pipeline().addLast(new TestHandler());
}
});
5. 服务端实现
5.1 服务端启动类
public class NettyServer {
public static void main(String[] args) {
//线程组-主要是监听客户端请求
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
//线程组-主要是处理具体业务
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
//启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
//指定线程组
.group(bossGroup, workerGroup)
//指定 NIO 模式
.channel(NioServerSocketChannel.class)
//双向链表管理
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
//责任链,指定自定义处理业务的 Handler
ch.pipeline().addLast(new NettyServerHandler());
}
});
//绑定端口号
serverBootstrap.bind(80);
}
}
代码说明:
- 以上都是模板代码,需要变动的是根据不同的业务自定义对应的 Handler,并且在 initChannel () 添加逻辑处理器;
- 根据实际情况指定 bind () 方法的端口号,注意的是端口号不能和其它端口号冲突;
- 这里大家先熟练掌握模板代码的编写,后面章节会讲解 NioEventLoopGroup、Pipeline 等核心组件的原理。
5.2 自定义 Handler
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
//接受客户端端响应时触发该事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//转换为 ByteBuf 缓冲区(底层是 byte[] 数组)
ByteBuf buffer=(ByteBuf)msg;
//定义一个 byte[] 数组
byte[] bytes=new byte[buffer.readableBytes()];
//缓冲区把数据写到 byte[] 数组
buffer.readBytes(bytes);
//把 byte[] 转换字符串
String req=new String(bytes,"UTF-8");
System.out.println("客户端请求:"+req);
//给客户端响应信息>>>>>>>>>>>>>>>>>>>>>>>>>>>>
String res="Hello World>>>>Client";
//把字符串转换 ByteBuf
ByteBuf buf=getByteBuf(ctx,res);
//把 ByteBuf 写到通道并且刷新
ctx.writeAndFlush(buf);
}
private ByteBuf getByteBuf(ChannelHandlerContext ctx,String str) {
// 1. 获取二进制抽象 ByteBuf
ByteBuf buffer = ctx.alloc().buffer();
// 2. 准备数据,指定字符串的字符集为 utf-8
byte[] bytes = str.getBytes(Charset.forName("utf-8"));
// 3. 填充数据到 ByteBuf
buffer.writeBytes(bytes);
return buffer;
}
}
代码说明:
-
这个逻辑处理器继承自 ChannelInboundHandlerAdapter,然后覆盖了 channelRead () 方法;
-
channelRead () 方法,接受客户端请求数据时会触发该方法,一般是用来处理具体的业务;
-
Netty 是面向 ByteBuf 通讯的,接受数据和响应数据都需要转换 ByteBuf,ByteBuf 的 API 后面再详细讲解,这里我们需要知道的是常见创建 ByteBuf 有常见两种方式,①通过 Unpooled 非池化工具类来操作;②通过
ctx.alloc().buffer()
来获取。最后我们调用ctx.channel().writeAndFlush()
把数据写到服务端。
6. 客户端实现
6.1 客户端启动类
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
// 1.指定线程模型
.group(workerGroup)
// 2.指定 IO 类型为 NIO
.channel(NioSocketChannel.class)
// 3.IO 处理逻辑
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
//自定义业务 Handler
ch.pipeline().addLast(new NettyClientHandler());
}
});
// 4.建立连接
ChannelFuture future=bootstrap.connect("127.0.0.1", 80).sync();
}
}
代码说明:
- 以上都是模板代码,需要变动的是根据不同的业务自定义对应的 Handler,并且在 initChannel () 添加逻辑处理器;
- connect () 方法,指定对应服务端 ip 和 port。
6.2 自定义 Handler
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
//客户端连接成功之后触发该事件,只会触发一次
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World".getBytes()));
}
//接受服务端响应时触发该事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer=(ByteBuf)msg;
byte[] bytes=new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
String res=new String(bytes,"UTF-8");
System.out.println("服务端响应:"+res);
}
}
代码说明:
- 这个逻辑处理器继承自 ChannelInboundHandlerAdapter,然后覆盖了 channelActive () 和 channelRead () 方法;
- channelActive (),这个方法会在客户端连接建立成功之后被调用,可以在这个方法里面,做一些初始化的工作,该方法仅被调用一次;
- channelRead 方法,在接受客户端响应时触发,会触发多次。
7. 测试效果
服务端打印:
客户端打印:
8. 视频演示
9. 小结
通过以上的代码,我们主要实现了客户端和服务端之间的通讯,需要掌握以下关键点:
- 客户端和服务端的启动类代码,这个基本上是固定写法;
- 掌握 Handler 的作用以及如何自定义,几个核心方法的触发时机以及常见的应用场景;
- 和传统的 socket 编程不同的是,Netty 里面数据是以 ByteBuf 为单位的。