编写一个网络应用程序需要实现某种编解码器,编解码器的作用就是讲原始字节数据与自定义的消息对象进行互转。网络中都是以字节码的数据形式来传输数据的,服务器编码数据后发送到客户端,客户端需要对数据进行解码,因为编解码器由两部分组成:
- Decoder(解码器)
- Encoder(编码器)
解码器负责处理“入站”数据,编码器负责处理“出站”数据。编码器和解码器的结构很简单,消息被编码后解码后会自动通过ReferenceCountUtil.release(message)释放。
需要补充说明的是,Netty中有两个方向的数据流
入站(ChannelInboundHandler):从远程主机到用户应用程序则是“入站(inbound)”
出站(ChannelOutboundHandler):从用户应用程序到远程主机则是“出站(outbound)”
今天我们主要学习编码器,也就是Encoder
实现逻辑
完成一个编码器的编写主要是实现一个抽象类MessageToMessageEncoder,其中我们需要重写方法是
/**
* Encode from one message to an other. This method will be called for each written message that can be handled
* by this encoder.
*
* @param ctx the {@link ChannelHandlerContext} which this {@link MessageToMessageEncoder} belongs to
* @param msg the message to encode to an other one
* @param out the {@link List} into which the encoded msg should be added
* needs to do some kind of aggragation
* @throws Exception is thrown if an error accour
*/
protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
其中泛型参数I表示我们需要接收的参数类型,如你需要将ByteBuf类型转换为Date类型,那么泛型I就是ByteBuf(事实上当实现ByteBuf编码为其他类型的时候是不需要使用MessageToMessageEncoder,Netty提供了ByteToMessageCodec,其本质也是实现了MessageToMessageEncoder)
代码编写
需求说明
客户端发过来一个数字(ByteBuf),我们将此类型转换为数字,获取当前时间加上此数字的时间后返回客户端,具体逻辑如下:
编码器的编写
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.util.CharsetUtil;
import java.time.LocalDateTime;
import java.util.List;
public class TimeEncoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
//将ByteBuf转换为String,注意此处,我是Mac OS,数据结尾是\r\n,如果是其他类型的OS,此处可能需要调整
String dataStr = msg.toString(CharsetUtil.UTF_8).replace("\r\n","");
//将String转换为Integer
Integer dataInteger = Integer.valueOf(dataStr);
//获取当前时间N小时后的数据
LocalDateTime now = LocalDateTime.now();
LocalDateTime dataLocalDatetime = now.plusHours(dataInteger);
out.add(dataLocalDatetime);
}
}
服务端处理代码
此处的服务端HandleAdapter和前面两个章节的HandleAdapter有所区别的是:其继承了SimpleChannelInboundHandler<I>
并且传递了一个泛型参数,这里需要说明一下,SimpleChannelInboundHandler是ChannelInboundHandler一个子类,他能够自动帮我们处理一些数据,在ChannelInboundHandler中,我们使用channel方法来接收数据,那么在SimpleChannelInboundHandler中我们使用protected abstract void messageReceived(ChannelHandlerContext ctx, I msg) throws Exception;
来接收客户端的参数,可以看到的是,其参数中自动的实现了我们需要处理的泛型I msg,另外看一下SimpleChannelInboundHandler中channelRead方法的实现代码
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
//acceptInboundMessage() 检查参数的类型是否和设定的泛型是否匹配
//可以看到匹配的话,会进行强制类型转换并调用messageReceived方法
//否则的话,不执行,也就是说,这里的泛型一定要和编码器转换的结果类型一致,否则将接收不到参数
//当前如果你需要自己转换,那么你也可以和ChannelInBoundHandleAdapter一样,重写channelRead方法
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
messageReceived(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}
那么继续实现我们的HandleAdapter,代码非常简单,这里不再赘述。需要注意的是,我们这里没有做解码器,也就是说入站的时候需要ByteBuf类型的数据,因此使用channel.writeAndFlush(Object)的时候,需要的就是ByteBuf类型的数据类型(当然如果pipeline中添加了StringDecoder解码器,那么你就可以直接使用字符串类型的数据了)
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class TimeServerChannelHandleAdapter extends SimpleChannelInboundHandler<LocalDateTime> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("添加了新的连接信息:id = " + ctx.channel().id());
}
@Override
protected void messageReceived(ChannelHandlerContext ctx, LocalDateTime msg) throws Exception {
// 转换时间格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String dateFormat = msg.format(dateTimeFormatter);
System.out.println("接收到参数:" + dateFormat);
ctx.channel().writeAndFlush(Unpooled.copiedBuffer(dateFormat, Charset.defaultCharset()));
}
}
服务端启动代码
服务前启动代码和以前的代码非常类似,只需要在pipeline添加上适配的编码器即可,当然需要注意顺序(这个知识点以后我在仔细的阐述)
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;
public class TimeServer {
public void start() throws Exception {
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
.group(boosGroup, workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childHandler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//设置编码
pipeline.addLast(new TimeEncoder());
//设置服务处理
pipeline.addLast(new TimeServerChannelHandleAdapter());
}
});
ChannelFuture sync = bootstrap.bind(9998).sync();
System.out.println("Netty Server start with 9998 port");
sync.channel().closeFuture().sync();
} finally {
workGroup.shutdownGracefully();
boosGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
TimeServer server = new TimeServer();
server.start();
}
}
效果展示
这里为了不写太多的代码,防止造成知识的不理解,迷惑,这里我们使用telnet命令来测试数据,
启动服务器端
运行TimeServer代码的中的main方法即可
使用Telnet发送数据
telnet的命令格式是
usage: telnet [-l user] [-a] [-s src_addr] host-name [port]
可以看到大部分参数都是可选的,只有主机名称必填
发送数据的效果
继续在telnet中发送一个数据5,我们分别看下服务端的打印的数据和telnet接收到的数据
服务端打印的数据如下:
telnet端打印的接收到的数据
总结
至此,一个简单的编码器就完成,我们总结一下步骤
- 继承MessageToMessageDecoder抽象类,实现decode()方法
- 配置HandleAdapter 实现channelRead或者messageReceived方法
- 配置服务启动类,配置ChannelPipeline,添加编码器和HandleAdapter
- 编写客户端或者使用telnet或者其他手段测试