终于解决netty粘包问题了!
此文为讲解如何创建netty服务,并主要写如何解决粘包问题。具体代码见github
一、netty服务创建
使用springboot+netty,通过springboot 的 CommandLineRunner 来进行启动。让tcpServer类被spring管理。为了不影响整个spring启动流程,用线程进行启动。
(1)启动类
package org.jeecg.modules.utils;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.sysDataSync.utils.DataPrehandle;
import org.jeecg.modules.sysDataSync.utils.SimpleCanalClientExample;
import org.jeecg.modules.sysDataSync.utils.tcp.AskThreadMain;
import org.jeecg.modules.sysDataSync.utils.tcp.device.OnlineDeviceHandler;
import org.jeecg.modules.sysDataSync.utils.tcp.TcpServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 通用启动程序
*/
@Component
@Slf4j
public class BootStartUtils implements CommandLineRunner {
@Autowired
TcpServer tcpServer;
@Override
public void run(String... args) throws Exception {
//netty 服务
tcpServer.run();
}
}
(2)netty主启动程序类
package org.jeecg.modules.sysDataSync.utils.tcp;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class TcpServer{
private final Logger log = LoggerFactory.getLogger("serialLog");
private Integer port = 4574;
public void run() throws Exception {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//循环组接收连接,不进行处理,转交给下面的线程组
EventLoopGroup bossGroup = new NioEventLoopGroup();
//循环组处理连接,获取参数,进行工作处理
EventLoopGroup workerGroup = new NioEventLoopGroup();
//服务端进行启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
//使用NIO模式,初始化器等等
serverBootstrap.group(bossGroup, workerGroup)
//保持连接数
.option(ChannelOption.SO_BACKLOG, 300)
//保持连接
.option(ChannelOption.SO_KEEPALIVE, true)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
//绑定端口
ChannelFuture channelFuture;
try {
channelFuture = serverBootstrap.bind(port).sync();
if (channelFuture.isSuccess()) {
log.info("服务端启动成功,端口:"+port);
} else {
log.info("服务端启动失败!");
}
//等待服务监听端口关闭,就是由于这里会将线程阻塞,导致无法发送信息,所以我这里开了线程
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("create tcp server error! ",e);
}
finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
});
t1.start();
log.info("tcp server start");
}
}
二、对netty服务进行完善
上面代码跑不起来,众所周知,netty需要有自定义的 ChannelInitializer 来对报文进行处理,因此需要继承ChannelInitializer 创建 MyServerInitializer
(1)MyServerInitializer类
此类中上3个为报文处理类,下面2个监听类用于心跳处理。这里就不讲了。最后一个为业务处理报文的类
package org.jeecg.modules.sysDataSync.utils.tcp;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
//连接注册,创建成功,会被调用
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
ch.pipeline().addLast(new HexLengthFieldBasedFrameDecoder());
ch.pipeline().addLast( "decoder" ,new MyDecoder());
ch.pipeline().addLast( "encoder" ,new MyEncoder());
//设定IdleStateHandler心跳检测每五秒进行一次读检测,如果20秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法
ch.pipeline().addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
ch.pipeline().addLast(new HeartBeatServerHandler());
pipeline.addLast(new MyServerHandler());
}
}
(2)HexLengthFieldBasedFrameDecoder类(核心)
package org.jeecg.modules.sysDataSync.utils.tcp;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
public class HexLengthFieldBasedFrameDecoder extends LengthFieldBasedFrameDecoder {
private static final int LENGTH_FIELD_OFFSET = 3; // 报文长度字段在报文中的偏移量
private static final int LENGTH_FIELD_LENGTH = 1; // 报文长度字段的长度
private static final int LENGTH_ADJUSTMENT = -4; // 长度调整:从什么位置开始进行计算长度(截取到的长度),0为从截取的长度之后的一位开始从1计算
private static final int INITIAL_BYTES_TO_STRIP = 0; // 接收到的发送数据包,去除前initialBytesToStrip位
public HexLengthFieldBasedFrameDecoder() {
super(1024, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH,LENGTH_ADJUSTMENT,INITIAL_BYTES_TO_STRIP);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
return super.decode(ctx, in);
}
}
(3)MyServerHandler(对报文业务处理)
public class MyServerHandler extends SimpleChannelInboundHandler<String> {
private final Logger log = LoggerFactory.getLogger("serialLog");
/**
* 通道连接时调用-处理业务逻辑
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIP = insocket.getAddress().getHostAddress();
Integer clientPort = insocket.getPort();
log.info(clientIP +":" + clientPort +" :通道已连接!");
}
/**
16 * 客户端与服务端断开连接时调用
17 */
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端与服务端连接关闭...");
}
/**
* 处理业务逻辑
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object info) {
// 处理解码后的消息帧
ByteBuf frame = (ByteBuf) info;
// 将消息帧的内容转换为字符串并打印
String receiveMsg = frame.toString(CharsetUtil.UTF_8);
log.info(" ===================接收到了: : " + receiveMsg);
//这里写你的业务报文处理
}
/**
* 服务端接收客户端发送过来的数据结束之后调用
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
log.info("信息接收完毕...");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.info("异常信息:rn" + cause.getMessage());
Throwable cause1 = cause.getCause();
ctx.channel().close();
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
log.info("异常断开");
}
}
三、解决粘包问题
按照上面的代码来写服务端,基本能运行起来,有报错,哪儿报错改哪儿就行
下面说一下在真实业务中,如何解决netty的粘包问题,我这里用自定义LengthFieldBasedFrameDecoder来解决粘包问题。这个比较常用,但问题也比较多。
第0个需要注意的:统一报文协议格式。一定要声明好协议与报文长度,不然后续服务端开发都是坑。
第一个要注意的是,报文在netty 自定义的MyServerInitializer类中,是按顺序进行传递的,每一个handler(addLast方法的入参)的位置都是需要斟酌考虑的。比如我的MyServerInitializer类中,HexLengthFieldBasedFrameDecoder类就在最上面,因为需要处理报文长度问题。
第二个要注意的是,HexLengthFieldBasedFrameDecoder类默认解析的是bytes类型的报文,不需要将你的报文先转换成明文的string类型。
第三个要注意的是,HexLengthFieldBasedFrameDecoder的参数问题。下面详细讲一下如何进行设置HexLengthFieldBasedFrameDecoder类中的4个参数。
我的报文如下(为了方便,这里用明文,16进制的,在内存中):
8800000C0864452064679150
先说明一下,bytes转16进制的报文,2个字符为1个字节。如88 就是一个字节。一个16进制字符占半个字节。这个不会的自己去学
HexLengthFieldBasedFrameDecoder类的四个参数的单位都是字节
如上面的报文中所示,报文的长度单位在第4个字节位置,0C 换算成 10进制是 12,表示报文的长度是12个字节。
那么:
LENGTH_FIELD_OFFSET = 3;// 报文长度字段在报文中的偏移量。从1开始算,长度字节出现在第3个字节后。
8800000C0864452064679150)正常流转到业务处理类MyServerHandler(对报文业务处理)中,就需要填-4。向前4个字节(加上长度字节0C),从88开始往后数12个字节。8800000C0864452064679150),则正常流转到业务处理类MyServerHandler的报文就为(08644520646791508800000C)从0C后开始计算12个字节。

浙公网安备 33010602011771号