AGPS-Netty tcp笔记

版权声明:本文为博主原创文章,转载请附出处,万一不附,我又不能咬你! https://blog.csdn.net/yu757371316/article/details/78955412

1、TCP粘包和拆包

  • 熟悉TCP编程的可能都知道,无论是服务端还是客户端,当我们读取或者是发送消息的时候,都需要考虑TCP底层的粘包和拆包机制。
  • TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连成一片的,其间并没有分界线。再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实现情况进行包的拆分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
  • 处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。

2、解决办法

2.1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。

2.2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。

2.3、将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int来表示消息的总长度

3、Netty ByteBuf

  当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。从功能角度而言,ByteBuffer完全可以满足NIO编程的需要,但是NIO编程过于复杂,也存在局限性。ByteBuffer长度固定,一旦分配完成,不可动态修改。

  JDK ByteBuffer由于只有一个位置指针用于处理读写操作,因此每次读写的时候都需要额外调用flip(),否则功能将出错。

  Netty ByteBuf提供了两个指针用于支持顺序读取和写入操作:readerIndex用于标识读取索引,writerIndex用于标识写入索引。两个位置指针将ByteBuf缓冲区分割成三个区域。
(1) readerIndex到writerIndex之间的空间为可读的字节缓冲区
(2) writerIndex到capacity之间为可写的字节缓冲区
(3) 0到readerIndex之间是已经读取过的缓冲区

这里写图片描述

下面是Netty ByteBuf的常用的几个方法。

isReadable: 缓冲区是否可读 
markReaderIndex: 记录当前缓冲区读指针的位置 
resetReaderIndex: 重置缓冲区至markReaderIndex的位置处(markReaderIndex的初始位置为0) 
readerIndex: 当前缓冲区读指针的位置 
readableBytes: 当前缓冲区可读的字节数 
read*: 读取数据

写方法和上面类似

备注: 当一开始使用resetReaderIndex重置缓冲区读指针的位置时候,读指针会被重置0;
当使用markReaderIndex记录过当前缓冲区读指针的位置时,再使用resetReaderIndex重置缓冲区读指针的位置,读指针会被重置到markReaderIndex记录的位置处。

3、自定义协议封装类.

/**
 * User: yzc
 * Date: 2017/12/25 18:48
 * Comment: 自定义协议封装
 * +------------+------------+------------+------------+
 * |协议开始标志  |    长度     |    数据     | 校验位(异或)|
 * +------------+------------+------------+------------+
 * |   0x7F     |    2字节    |    数据     |    1字节   |
 * +------------+------------+------------+------------+
 */
public class SmartProtocol {

    /**
     * 自定义协议的开始位置
     */
    private static final byte HEAD_DATA = 0x7F;

    /**
     * 消息的长度
     */
    private short contentLength;

    /**
     * 消息的内容
     */
    private byte[] content;

    /**
     * 校验位
     */
    private byte checkBit;

    public SmartProtocol(short contentLength, byte[] content, byte checkBit) {
        this.contentLength = contentLength;
        this.content = content;
        this.checkBit = checkBit;
    }
}

4、自定义协议的编码器

/**
 * User: yzc
 * Date: 2017/12/25 19:01
 * Comment: 自定义编码器
 */
public class SmartEncoder extends MessageToByteEncoder<SmartProtocol> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, SmartProtocol smartProtocol, ByteBuf byteBuf) throws Exception {
        //1、写入消息头开始标志
        byteBuf.writeByte(SmartProtocol.getHeadData());
        //2、写入消息长度
        byteBuf.writeShort(smartProtocol.getContentLength());
        //3、写入数据
        byteBuf.writeBytes(smartProtocol.getContent());
        //4、写入校验位数据
        byteBuf.writeByte(smartProtocol.getCheckBit());
    }
}

5、自定义协议的解码器

/**
 * User: yzc
 * Date: 2017/12/25 19:07
 * Comment: 自定义解码器
 */
public class SmartDecoder extends ByteToMessageDecoder {

    private static Logger logger = LogManager.getLogger();

    /**
     * 协议开始的标准HEAD_DATA,byte类型,占据1个字节.
     * 表示数据的长度contentLength,short类型,占据2个字节.
     */
    public final int BASE_LENGTH = 1 + 2;

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        // 可读长度必须大于基本长度
        if (byteBuf.readableBytes() >= BASE_LENGTH) {
            // 防止socket字节流攻击/客户端传来的数据过大
            if (byteBuf.readableBytes() > 128) {
                byteBuf.skipBytes(byteBuf.readableBytes());
            }
            // 记录包头开始的index
            int beginReader;
            while (true) {
                // 获取包头开始的index
                beginReader = byteBuf.readerIndex();
                // 标记包头开始的index
                byteBuf.markReaderIndex();
                // 读到了协议的开始标志,结束while循环
                if (byteBuf.readByte() == SmartProtocol.getHeadData()) {
                    break;
                }
                // 未读到包头,略过一个字节
                // 每次略过,一个字节,去读取,包头信息的开始标记
                byteBuf.resetReaderIndex();
                byteBuf.readByte();

                // 当略过一个字节之后,数据包的长度可能又变得不满足
                // 此时应该结束。等待后面的数据到达
                if (byteBuf.readableBytes() < BASE_LENGTH) {
                    return;
                }
            }

            // 消息的长度
            short contentLength = byteBuf.readShort();
            // 判断请求数据包数据是否到齐
            if (byteBuf.readableBytes() < contentLength) {
                // 还原读指针
                byteBuf.readerIndex(beginReader);
                return;
            }
            // 读取data数据
            byte[] data = new byte[contentLength];
            byteBuf.readBytes(data);

            // 判断校验位请求数据包数据是否到齐
            if (byteBuf.readableBytes() < 1) {
                // 还原读指针
                byteBuf.readerIndex(beginReader);
                return;
            }
            // 读取校验位数据
            byte checkBit = byteBuf.readByte();
            //校验判断数据是否出错
            byte checkResult = CheckUtil.getXorCheck(contentLength, data);
            logger.debug("receive checkBit:" + checkBit + " calculate checkResult:" + checkResult);
            if (checkBit == checkResult) {
                SmartProtocol protocol = new SmartProtocol(contentLength, data, checkBit);
                list.add(protocol);
            }
        }
    }
}

6、服务端加入协议的编/解码器

@Service("neoChannelInitializer")
public class NeoChannelInitializer extends ChannelInitializer<SocketChannel> implements ApplicationContextAware {

    private static ApplicationContext context;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        //tcpServerHandler必须是每次请求都重新创建一个,底层pipiline不是可共享的,否则多次请求下将报错
        TcpServerHandler tcpServerHandler = ((TcpServerHandler) context.getBean("tcpServerHandler"));

        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("smartDecoder", new SmartDecoder());
        pipeline.addLast("smartEncoder", new SmartEncoder());

        // //心跳监测0表示关闭 读超时:5s内没有数据接收,写超时:没有数据发送 全部空闲时间:没有数据接收或者发送
        pipeline.addLast("ping", new IdleStateHandler(5, 0, 0));
        pipeline.addLast(tcpServerHandler);
    }
}

7、自定义Handler

/**
 * User: yzc
 * Date: 2017/12/24 15:37
 * Comment: Tcp服务器实际的业务判断处理
 */
//@Shareable
public class TcpServerHandler extends ChannelHandlerAdapter {

    private static Logger logger = LogManager.getLogger();

    @Autowired
    private AgpsUserServiceI agpsUserServiceImpl;

    @Autowired
    private TechtotopServiceI techtotopServiceImpl;

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        logger.debug("CLIENT" + ctx.channel().remoteAddress().toString() + " 接入连接");
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ctx.close();
        logger.debug("CLIENT" + ctx.channel().remoteAddress().toString() + " 断开连接");
        super.channelInactive(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            if (e.state() == IdleState.READER_IDLE) {
                logger.debug("读数据超时");
                ctx.close();
            } else if (e.state() == IdleState.WRITER_IDLE) {
                logger.debug("写数据超时");
                ctx.close();
            } else {
                logger.debug("总读写数据超时");
                ctx.close();
            }
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        SmartProtocol reqSmartProtocol = (SmartProtocol) msg;
        logger.debug("receive client : [" + reqSmartProtocol.toString() + "]");
        ByteBuf reqBuffer = Unpooled.copiedBuffer(reqSmartProtocol.getContent());
        //判断请求字符串是不是指定的格式
        ...
        具体逻辑
        ...
        ReferenceCountUtil.release(reqBuffer); //reqBuffer使用完后手动回收,避免内存泄漏。
       ....
        SmartProtocol smartProtocol = techtotopServiceImpl.getTcpEphemeris();
        ctx.writeAndFlush(smartProtocol).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 发送响应发回客户端
     * @param ctx
     * @param resp
     */
    private void sendResponse(ChannelHandlerContext ctx, String resp) {
        if (resp == null) {
            resp = "";
        }
        short contentLength = Short.parseShort(String.valueOf(resp.length()));
        byte checkBit = CheckUtil.getXorCheck(contentLength, resp.getBytes());
        SmartProtocol smartProtocol = new SmartProtocol(contentLength, resp.getBytes(), checkBit);
        ctx.writeAndFlush(smartProtocol).addListener(ChannelFutureListener.CLOSE); //注册监听,服务端数据发送完毕后,主动关闭链路
}

8、Server类

//用于分配处理业务线程的线程组个数
    protected static final int BIZGROUPSIZE = Runtime.getRuntime().availableProcessors() * 2;    //默认

    //业务出现线程大小
    protected static final int BIZTHREADSIZE = 8;

public void bind(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE);
        EventLoopGroup workerGroup = new NioEventLoopGroup(BIZTHREADSIZE);
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .option(ChannelOption.SO_KEEPALIVE, false)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                    .option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024) //水位高值
                    .option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024)   //水位低值
                    .option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT) //容量可自动动态调整的接收缓冲区分配器,减少内存使用
                    .childHandler(neoChannelInitializer);

            ChannelFuture f = b.bind(port).sync();

            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
            logger.info("TcpServer 已关闭");
        }
    }

9、优化

ChannelOption.SO_BACKLOG  --等待队列的大小
ChannelOption.SO_KEEPALIVE, false --是否保持长连接
ChannelOption.CONNECT_TIMEOUT_MILLIS --客户端连接超时时间
ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024 //水位高值
ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024   //水位低值 --设置后可判断buffer的isWritable是false还是true,从而控制读写,防止buffer不断地增长占用太多系统资源

true。所以应用应该判断isWritable
ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT //容量可自动动态调整的接收缓冲区分配器,减少内存使用

//心跳监测0表示关闭 读超时:5s内没有数据接收,写超时:没有数据发送 全部空闲时间:没有数据接收或者发送
pipeline.addLast("ping", new IdleStateHandler(5, 0, 0));

//发送完数据后,立刻关闭链接
ctx.writeAndFlush(smartProtocol).addListener(ChannelFutureListener.CLOSE);

10、netty和spring整合
要点:对于每一个客户端,我们都是new一个handler处理,所以在整合Spring的时候一定要注意,每次新的处理请求,一定要重新创建一个handler实例:tcpServerHandler必须是每次请求都重新创建一个,底层pipiline不是可共享的,否则多次请求下将报错。
在Spring中有一个设置bean的scope为prototype表示多实例模式。这里一定要主要Spring的prototype模式的坑,解决办法参考:prototype作用域的“坑”

我的实现:

<bean id="tcpServerHandler" class="com.neo.agps.tcp.server.TcpServerHandler" scope="prototype"/>

获取handler时,实现spring的ApplicationContextAware接口的setApplicationContext方法;
每次需要handler都这样:context.getBean("tcpServerHandler"));

11、server的自启动
这里还是使用Spring的扩展,tcpserver实现ApplicationListener接口
然后实现onApplicationEvent方法:

@Override
    public void onApplicationEvent(ContextRefreshedEvent evt) {
        if (evt.getApplicationContext().getParent() == null) {
            ...
        }
    }

注意一定要判断父容器,否则可能会启动两次

参考文章:
Netty之解决TCP粘包拆包(自定义协议)

Netty中的那些坑(上篇)

Netty系列之Netty百万级推送服务设计要点

Netty构建游戏服务器(三)–netty spring简单整合

Netty百万级推送服务设计要点

猜你喜欢

转载自blog.csdn.net/yu757371316/article/details/78955412
今日推荐