前言:
Netty中处理输入输出字节的最重要的类就是ChannelInboundHandler(处理输入字节)、ChannelOutboundHandler(处理输出字节)
下面我们从最简单的使用到复杂使用来回顾一下关于这些Handler的处理
Netty版本说明:笔者使用的是netty-4.1.10版本,以下代码均基于此编写
1.服务端
服务端负责接收客户端请求、接收客户端数据、返回响应
我们来创建一下服务端的代码,下面是一段模板代码,先不添加Handler处理
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss和worker
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
//设置线程池
bootstrap.group(boss, worker);
//设置socket工厂
bootstrap.channel(NioServerSocketChannel.class);
//设置管道工厂
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
// TODO
}
});
//设置参数,TCP参数
bootstrap.option(ChannelOption.SO_BACKLOG, 2048);//serverSocketchannel的设置,链接缓冲池的大小
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);//socketchannel的设置,维持链接的活跃,清除死链接
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);//socketchannel的设置,关闭延迟发送
//绑定端口8088
ChannelFuture future = bootstrap.bind(8088).sync();
System.out.println("server start...");
//等待服务端关闭
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally{
//释放资源
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
注意:以上就是一段模板代码,我们真正要做的就是在TODO这里添加合适的Handler
此时服务端不具有业务处理功能
protected void initChannel(Channel ch) throws Exception {
// TODO
}
2.原生使用ChannelInboundHandler的方式
对于输入信息,我们可以通过实现ChannelInboundHandler的方式来处理,最重要的方法如下:
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception;
我们通过实现channelRead方法来实现msg的获取
当然,我们一般不会直接使用该接口,而是使用一个Adapter类,ChannelInboundHandlerAdapter,该类做了接口的基本实现,我们一般都是通过继承该类
下面我们就来实现一个最简单版本的,获取客户端输入信息,并打印出来
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* 初级版处理
* 将字节数组转换为字符串
*/
public class SimpleInHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof ByteBuf){
ByteBuf in = (ByteBuf)msg;
try {
if(in.readableBytes() > 0){
// 转换为String
String str = in.toString(CharsetUtil.UTF_8);
// TODO logic
System.out.println("receive data: " + str);
}
} finally {
// 释放资源
ReferenceCountUtil.release(in);
}
}
}
}
注意:我们使用完ByteBuf之后,需要主动去释放资源,否则,资源一直在内存中加载,容易造成内存泄漏
那么,我们每个Handler都需要这些释放的逻辑,可不可以把这些操作抽象成一个模板呢?答案是可以的
3.这里简单说一下如何测试Handler
* 首先在Server类中的initChannel方法添加SimpleInHandler实例,然后启动Server类,此时Server会监听在本机8088端口
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new SimpleInHandler())
}
* 客户端发送消息
笔者在这里使用了一种比较讨巧的方式,就是使用Windows命令窗的telnet命令
点击回车,如果出现以下即说明连接成功,连接失败的话会有失败提醒
这种情况下,我们可以输入字符,然后按回车,即发送成功
但是这个会有一个问题,就是输入一个字符就自动回车,无法输入整行字符,我们可以 按 Ctrl+]键,进入
这时可以使用send xxx 来发送整行信息
4.SimpleChannelInboundHandler的使用
SimpleChannelInboundHandler是一个抽象类,主要实现如下:
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
// 需要我们主动实现的接口
protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
...
}
SimpleChannelInboundHandler就是我们在2中提问的一个抽象模板,资源的释放已经被抽象出去了,我们只需要关注channelRead0方法,去实现我们的业务即可
具体使用如下:
/**
* 继承SimpleChannelInboundHandler版本,省去release处理
* 将字节数组转换为字符串
*/
public class SimpleChannelInHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
if(msg.readableBytes() > 0){
// 转换为String
String str = msg.toString(CharsetUtil.UTF_8);
// TODO logic
System.out.println("receive data: " + str);
}
}
}
注意:我们可以把所有的输入当做字符串来处理,这样暂时是没什么问题的,那么实际以上代码也可以算作是一个模板,因为堆ByteBuf的字节转换为字符串处理都是相同 的处理,那么Netty有这样的模板类嘛?答案是肯定的
5.StringDecoder
StringDecoder就是专门用来将字节数组转换为String的Handler类,如果没有其他特殊情况的话,我们就可以直接将StringDecoder添加到我们的Server中,然后后面的Handler处理就可以直接处理String类型的数据就可以。具体使用如下:
* 创建一个用于打印客户端输入信息的Handler
/**
* 用于处理服务端接收到的信息,目前就是打印出来
* @author Administrator
*
*/
public class PrintHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("receive data: " + msg);
}
}
注意:这里继承SimpleChannelInboundHandler的时候,泛型可以直接写String
* 服务端代码
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new PrintHandler());
}
}
我们把StringDecoder放在前面,PrintHandler放在后面,这样请求就会先经过StringDecoder处理,处理成String之后,再交给PrintHandler处理
注意:我们在使用String处理的时候,会碰到一种情况,在发送TCP请求的时候,客户端什么时候才算是一次输入呢?大部分就是使用换行符来表示一次输入,如果我们直接使用StringDecoder,那么其对一次输入的划分是有问题的,这时我们在StringDecoder之前有一个Handler可以先将输入字符按照换行符来截断,截断后的ByteBuf再作为StringDecoder的一次输入,那么有没有这样的Handler类呢?答案是肯定的
6.LineBasedFrameDecoder
/**
* A decoder that splits the received {@link ByteBuf}s on line endings.
* <p>
* Both {@code "\n"} and {@code "\r\n"} are handled.
* For a more general delimiter-based decoder, see {@link DelimiterBasedFrameDecoder}.
*/
public class LineBasedFrameDecoder extends ByteToMessageDecoder {
通过注解可以了解到该类主要是对输入字节进行划分的,按照\n或者\r\n来进行划分
注意:还要一个类DelimiterBasedFrameDecoder,可以让用户自定义字节划分符
参考:Netty in Action
日拱一卒,砥砺前行!
欢迎关注微信公众号:开发者实用工具合集
实时获取最新动态