前言
环境
JDK:64位 Jdk1.8
Netty:4.1.39.Final
问题描述
项目中使用Netty接受客户端的消息,客户端为硬件设备,在接受数据后发送数据到服务端。
同时因为客户端没有联网,所以可能会因为开关机等原因导致时间不准确,因此需要服务端添加一个校验时间的操作,如果时间差距过大则发送重新设置时间的指令。
相关代码
@Slf4j
@Component
public class NettyServer {
@Autowired
private SocketConfig socketConfig;
private EventLoopGroup bossGroup = null;
private EventLoopGroup workerGroup = null;
public void start() throws Exception {
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(2048, 2048, 2048))
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new DeviceChannelInitHandler());
//无关代码省略
...
} catch (Exception e) {
log.info("netty异常:", e);
}
}
...
}
处理器
@Slf4j
public class DeviceChannelInitHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
...
//使用自定义分割符
customizeSplitHandler(socketChannel, true, "$");
socketChannel.pipeline().addLast(new StringDecoder());
// 添加自定义的业务处理器
socketChannel.pipeline().addLast(new DeviceServiceHandler());
socketChannel.pipeline().addLast(new StringEncoder());
...
}
/**
* 自定义分隔符处理器
*
* @param socketChannel
* @param stripDelimiter
* @param split
*/
private static void customizeSplitHandler(SocketChannel socketChannel, boolean stripDelimiter, String split) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
buffer.writeBytes(split.getBytes());
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, stripDelimiter, buffer));
}
}
业务处理器
@Slf4j
public class DeviceServiceHandler extends SimpleChannelInboundHandler<String> {
/**
* 解码协议(字符串转换)
*
* @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
* belongs to
* @param dateStr the message to handle
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, String dateStr) throws Exception {
...
//如果设备发送的时间和当前系统时间差距太大,则修正设备时间
String sendDate = DateUtil.getStringCustomFormatByDate(new Date(), "yyyyMMddHHmmss");
String command = "#SETTIME" + sendDate + "!$";
//发送指令
ctx.writeAndFlush(command);
...
}
}
解决方法
经过逐一排查,查看git的历史记录,发现原因是之前的同事将字符串编码器 StringEncoder 放在了业务处理器DeviceServiceHandler 的下面。
导致在数据发送时,业务处理器DeviceServiceHandler 会先处理数据,但此时数据还没有被编码为字节数据。由于 StringEncoder 还没有被调用,数据将以未编码的形式发送,这可能导致客户端无法正确解析数据,从而无法接收到正确的数据。
@Slf4j
public class DeviceChannelInitHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
...
customizeSplitHandler(socketChannel, true, "$");
socketChannel.pipeline().addLast(new StringDecoder());
//将字符串编码器 StringEncoder 放在业务处理器上面,客户端即可收到数据
socketChannel.pipeline().addLast(new StringEncoder());
socketChannel.pipeline().addLast(new DeviceServiceHandler());
...
}
}
如果对Netty中入站和出站处理器还不是很了解,可以看以下这篇文章:
Netty组件Handler & Pipeline