终于解决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个字节后。

LENGTH_FIELD_LENGTH = 1; // 报文长度字段的长度,0C就是1个字节,长度是1
 
有了上面这两个属性,就能确定要给netty的长度字节了。这里面直接用0C来表示。
 
LENGTH_ADJUSTMENT = -4;
//上面这一个参数就比较扯,我理解的是长度调整偏移量。从0C(长度字节)下标为0 开始计算。
如果为正x,则表示从0C开始(不包含),跳过后x个,开始计算长度,如果为负x,则表示从0C开始(包含),跳过前x个字节,开始计算长度。
举例:
如果我想要将我的报文(8800000C0864452064679150)正常流转到业务处理类MyServerHandler(对报文业务处理)中,就需要填-4。向前4个字节(加上长度字节0C),从88开始往后数12个字节。
如果这个值为0,则如果不断重复发送报文(8800000C0864452064679150),则正常流转到业务处理类MyServerHandler的报文就为(08644520646791508800000C)从0C后开始计算12个字节。
 
为什么会这样?我认为,netty的LengthFieldBasedFrameDecoder本意是指,长度字节(不包含长度字节)之后的报文,才算是业务报文。长度字节的长度只是表明业务报文的长度。
如果你的长度字节表示的是整个报文的长度,那么还是需要自己取计算你的LENGTH_ADJUSTMENT 
 
最后一个属性:
INITIAL_BYTES_TO_STRIP = 0; // 接收到的发送数据包,去除前initialBytesToStrip位
跳过前x个字节开始计算。也就是舍弃报文开始计算,我没用到。这里填0。
posted @ 2023-06-15 19:12  Furaooooo  阅读(406)  评论(0)    收藏  举报