实现一款高可用的 TCP 数据传输服务器(Java版)

2023-11-03

1.netty能做什么

首先netty是一款高性能、封装性良好且灵活、基于NIO(真·非阻塞IO)的开源框架。可以用来手写web服务器、TCP服务器等,支持的协议丰富,如:常用的HTTP/HTTPS/WEBSOCKET,并且提供的大量的方法,十分灵活,可以根据自己的需求量身DIV一款服务器。

用netty编写TCP的服务器/客户端

1.可以自己设计数据传输协议如下面这样:

2.可以自定义编码规则和解码规则

3.可以自定义客户端与服务端的数据交互细节,处理socket流攻击、TCP的粘包和拆包问题

2.Quick Start

创建一个普通的maven项目,不依赖任何的三方web服务器,用main方法执行即可。

加入POM依赖

<!--netty的依赖集合,都整合在一个依赖里面了-->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.6.Final</version>
</dependency>
<!--这里使用jackson反序列字节码-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.7</version>
</dependency>
<!--加入log4j 便于深入学习整合运行过程的一些细节-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

设计一套基于TCP的数据传输协议

public class TcpProtocol {
    private byte header=0x58;
    private int len;
    private byte [] data;
    private byte tail=0x63;

    public byte getTail() {
        return tail;
    }

    public void setTail(byte tail) {
        this.tail = tail;
    }

    public TcpProtocol(int len, byte[] data) {
        this.len = len;
        this.data = data;
    }

    public TcpProtocol() {
    }

    public byte getHeader() {
        return header;
    }

    public void setHeader(byte header) {
        this.header = header;
    }

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }
}

这里使用16进制表示协议的开始位和结束位,其中0x58代表开始,0x63代表结束,均用一个字节来进行表示。

TCP服务器的启动类

public class TcpServer {
    private  int port;
    private Logger logger = Logger.getLogger(this.getClass());
    public  void init(){
        logger.info("正在启动tcp服务器……");
        NioEventLoopGroup boss = new NioEventLoopGroup();//主线程组
        NioEventLoopGroup work = new NioEventLoopGroup();//工作线程组
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();//引导对象
            bootstrap.group(boss,work);//配置工作线程组
            bootstrap.channel(NioServerSocketChannel.class);//配置为NIO的socket通道
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                protected void initChannel(SocketChannel ch) throws Exception {//绑定通道参数
                    ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));//设置log监听器,并且日志级别为debug,方便观察运行流程
                    ch.pipeline().addLast("encode",new EncoderHandler());//编码器。发送消息时候用过
                    ch.pipeline().addLast("decode",new DecoderHandler());//解码器,接收消息时候用
                    ch.pipeline().addLast("handler",new BusinessHandler());//业务处理类,最终的消息会在这个handler中进行业务处理
                }
            });
            bootstrap.option(ChannelOption.SO_BACKLOG,1024);//缓冲区
            bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);//ChannelOption对象设置TCP套接字的参数,非必须步骤
            ChannelFuture future = bootstrap.bind(port).sync();//使用了Future来启动线程,并绑定了端口
            logger.info("启动tcp服务器启动成功,正在监听端口:"+port);
            future.channel().closeFuture().sync();//以异步的方式关闭端口

        }catch (InterruptedException e) {
            logger.info("启动出现异常:"+e);
        }finally {
            work.shutdownGracefully();
            boss.shutdownGracefully();//出现异常后,关闭线程组
            logger.info("tcp服务器已经关闭");
        }

    }

    public static void main(String[] args) {
        new TcpServer(8777).init();
    }
    public TcpServer(int port) {
        this.port = port;
    }
}

只要是基于netty的服务器,都会用到bootstrap 并用这个对象绑定工作线程组,channel的Class,以及用户DIV的各种pipeline的handler类,注意在添加自定义handler的时候,数据的流动顺序和pipeline中添加hanlder的顺序是一致的。也就是说,从上往下应该为:底层字节流的解码/编码handler、业务处理handler。

编码器

编码器是服务器按照协议格式返回数据给客户端时候调用的,继承MessageToByteEncoder代码:

public class EncoderHandler extends MessageToByteEncoder {
    private  Logger logger = Logger.getLogger(this.getClass());
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
        if (msg instanceof TcpProtocol){
            TcpProtocol protocol = (TcpProtocol) msg;
            out.writeByte(protocol.getHeader());
            out.writeInt(protocol.getLen());
            out.writeBytes(protocol.getData());
            out.writeByte(protocol.getTail());
            logger.debug("数据编码成功:"+out);
        }else {
            logger.info("不支持的数据协议:"+msg.getClass()+"\t期待的数据协议类是:"+TcpProtocol.class);
        }
    }
}

解码器

解码器属于比较核心的部分,自定义解码协议、粘包、拆包等都在里面实现,继承自ByteToMessageDecoder,其实ByteToMessageDecoder的内部已经帮我们处理好了拆包/粘包的问题,只需要按照它的设计原则去实现decode方法即可:

public class DecoderHandler extends ByteToMessageDecoder {
    //最小的数据长度:开头标准位1字节
    private static int MIN_DATA_LEN=6;
    //数据解码协议的开始标志
    private static byte PROTOCOL_HEADER=0x58;
    //数据解码协议的结束标志
    private static byte PROTOCOL_TAIL=0x63;
    private Logger logger = Logger.getLogger(this.getClass());
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        if (in.readableBytes()>MIN_DATA_LEN){
            logger.debug("开始解码数据……");
            //标记读操作的指针
            in.markReaderIndex();
            byte header=in.readByte();
            if (header==PROTOCOL_HEADER){
                logger.debug("数据开头格式正确");
                //读取字节数据的长度
                int len=in.readInt();
                //数据可读长度必须要大于len,因为结尾还有一字节的解释标志位
                if (len>=in.readableBytes()){
                    logger.debug(String.format("数据长度不够,数据协议len长度为:%1$d,数据包实际可读内容为:%2$d正在等待处理拆包……",len,in.readableBytes()));
                    in.resetReaderIndex();
                    /*
                    **结束解码,这种情况说明数据没有到齐,在父类ByteToMessageDecoder的callDecode中会对out和in进行判断
                    * 如果in里面还有可读内容即in.isReadable为true,cumulation中的内容会进行保留,,直到下一次数据到来,将两帧的数据合并起来,再解码。
                    * 以此解决拆包问题
                     */
                    return;
                }
                byte [] data=new byte[len];
                in.readBytes(data);//读取核心的数据
                byte tail=in.readByte();
                if (tail==PROTOCOL_TAIL){
                    logger.debug("数据解码成功");
                    out.add(data);
                    //如果out有值,且in仍然可读,将继续调用decode方法再次解码in中的内容,以此解决粘包问题
                }else {
                    logger.debug(String.format("数据解码协议结束标志位:%1$d [错误!],期待的结束标志位是:%2$d",tail,PROTOCOL_TAIL));
                    return;
                }
            }else {
                logger.debug("开头不对,可能不是期待的客服端发送的数,将自动略过这一个字节");
            }
        }else {
            logger.debug("数据长度不符合要求,期待最小长度是:"+MIN_DATA_LEN+" 字节");
            return;
        }

    }
}

首先是黏包问题:

如图,正常的数据传输应该是像数据A那样,一包就是一个完整的数据,但也有不正常的情况,比如一包数据包含多个数据。而在ByteToMessageDecoder会默认把二进制的字节码放在byteBuf中,因此我们在code的时候要知道会有这样的场景。

而粘包问题实际上不需要我们去解决,下面是ByteToMessageDecoder的源码,callDecode中回调我们手写解码器的decode方法。

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        while (in.isReadable()) {//buf中是否还有数据
            int outSize = out.size();//标记out的size,解析成功的数据会添加的out中
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);//这个是回调业务handler的channelRead方法
                out.clear();
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;//清空了out,将标记size清零
            }
            int oldInputLength = in.readableBytes();//这里开始准备调用decode方法,标记了解码前的可读内容
            decode(ctx, in, out);//对应DecoderHandler中的decode方法
            if (ctx.isRemoved()) {
                break;
            }

            if (outSize == out.size()) {//相等说明,并没有解析出来新的object到out中
                if (oldInputLength == in.readableBytes()) {//这里很重要,若相等说明decode中没有读取任何内容出来,这里一般是发生拆包后,将ByteBuf的指针手动重置。重置后从这个方法break出来。让ByteToMessageDecoder去处理拆包问题。这里就体现了要按照netty的设计原则来写代码                       
                break;
                } else {
                    continue;//这里直接continue,是考虑让开发者去跳过某些字节,比如收到了socket攻击时,数据不按照协议体来的时候,就直接跳过这些字节
                }
            }

            if (oldInputLength == in.readableBytes()) {//这种情况属于,没有按照netty的设计原则来。要么是decode中没有任何逻辑代码,要么是在out中添加了内容后,调用了byteBuf的resetReaderIndex重置的读操作的指针
                throw new DecoderException(
                        StringUtil.simpleClassName(getClass()) +
                        ".decode() did not read anything but decoded a message.");
            }

            if (isSingleDecode()) {//默认为false,用来设置只解析一条数据
                break;
            }
            //这里结束后,继续wile循环,因为bytebuf仍然有可读的内容,将会继续调用decode方法解析bytebuf中的字节码,以此解决了粘包问题
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

综合上面的源码分析后,我们发现:decode方法在while循环中,也就是bytebuf只要有内容就会一直调用decode方法进行解码操作,因此在解决粘包问题时,只需要按照正常流程来就行了,解析协议开头、数据字节、结束标志后将数据放入到out这个list中即可。后面将会有数据进行粘包测试。

拆包问题

有时候,我们接收到的数据是不完整的,一个包的数据被拆成了很多份被后再发送出去。这种情况有可能是数据太大,被分割成很多份发送出去。比如数据包B被拆成两份进行发送:

拆包问题,同样在ByteToMessageDecoder 给我们解决了,我们只需要按照netty的设计原则去写decode代码即可。

首先,假设需要我们自己去解决拆包问题应该怎么实现?

先从问题开始分析,需要的是数据B,但是却只收到了数据B_1,这个时候应该等待剩余的数据B_2的到来,收到的数据B_1应该用一个累加器存起来,等到B_2到来的时候将两包数据合并起来再进行解码。

那么问题是,如何让ByteToMessageDecoder这个知道数据不完整呢,在DecoderHandler.decode中有这样一段代码:

if (len>=in.readableBytes()){
    logger.debug(String.format("数据长度不够,数据协议len长度为:%1$d,数据包实际可读内容为:%2$d正在等待处理拆包……",len,in.readableBytes()));
    in.resetReaderIndex();
    /*
    **结束解码,这种情况说明数据没有到齐,在父类ByteToMessageDecoder的callDecode中会对out和in进行判断
    * 如果in里面还有可读内容即in.isReadable为true,cumulation中的内容会进行保留,,直到下一次数据到来,将两帧的数据合并起来,再解码。
    * 以此解决拆包问题
     */
    return;
}

当读到协议中的len大于bytebuf的可读内容时候说明数据不完整,发生了拆包,调用resetReaderIndex将读操作指针复位,并结束方法。再看看父类中的CallDecode方法的一段代码:

if (outSize == out.size()) {//相等说明,并没有解析出来新的object到out中
    if (oldInputLength == in.readableBytes()) {//这里很重要,若相等说明decode中没有读取任何内容出来,这里一般是发生拆包后,将ByteBuf的指针手动重置。重置后从这个方法break出来。让ByteToMessageDecoder去处理拆包问题。这里就体现了要按照netty的设计原则来写代码                       
    break;//退出该方法
    } else {
        continue;//这里直接continue,是考虑让开发者去跳过某些字节,比如收到了socket攻击时,数据不按照协议体来的时候,就直接跳过这些字节
    }
}

退出callDecode后,返回到channelRead中:

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            ByteBuf data = (ByteBuf) msg;
            first = cumulation == null;
            if (first) {
                cumulation = data;
            } else {
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            callDecode(ctx, cumulation, out);//注意这里传入的不是data,而是cumulator,这个对象相当于一个累加器,也就是说每次调用callDecode的时候传入的byteBuf实际上是经过累加后的cumulation
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new DecoderException(t);
        } finally {
            if (cumulation != null && !cumulation.isReadable()) {//这里若是数据被读取完,会清空累加器cumulation
                numReads = 0;
                cumulation.release();
                cumulation = null;
            } else if (++ numReads >= discardAfterReads) {
                // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                // See https://github.com/netty/netty/issues/4275
                numReads = 0;
                discardSomeReadBytes();
            }

            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

channelRead方法是,收到一包数据后就会调用一次。至此,netty帮我们完美解决了拆包问题。我们只需要按着他的设计原则:len>byteBuf.readableBytes时候,重置读指针,结束decode即可。

业务处理handler类

这一层中数据已经被完整的解析出来了,可以直接使用了:

public class BusinessHandler extends ChannelInboundHandlerAdapter {
    private ObjectMapper objectMapper= ByteUtils.InstanceObjectMapper();
    private Logger logger = Logger.getLogger(this.getClass());
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof byte []){
            logger.debug("解码后的字节码:"+new String((byte[]) msg,"UTF-8"));
            try {
                Object objectContainer = objectMapper.readValue((byte[]) msg, DTObject.class);
                if (objectContainer instanceof DTObject){
                    DTObject data = (DTObject) objectContainer;
                    if (data.getClassName()!=null&&data.getObject().length>0){
                        Object object = objectMapper.readValue(data.getObject(), Class.forName(data.getClassName()));
                        logger.info("收到实体对象:"+object);
                    }
                }
            }catch (Exception e){
                logger.info("对象反序列化出现问题:"+e);
            }

        }
    }
}

由于在decode中并没有将字节码反序列成对象,因此需要进一步反序列化。在传输数据的时候,可能传递的对象不只是一种,因此在反序列化也要考虑到这一问题。解决办法是将传输的对象进行二次包装,将全名类信息包含进去:

public class DTObject {
    private String className;
    private byte[] object;
}

这样在反序列化的时候使用Class.forName()获取Class,避免了要写很多if循环判断反序列化的对象的Class。前提是要类名和包路径要完全匹配!

接下来编写一个TCP客户端进行测试

启动类的init方法:

public  void init() throws InterruptedException {
    NioEventLoopGroup group = new NioEventLoopGroup();
    try {
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(group);
    bootstrap.channel(NioSocketChannel.class);
    bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
    bootstrap.handler(new ChannelInitializer() {
        @Override
        protected void initChannel(Channel ch) throws Exception {
            ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
            ch.pipeline().addLast(new EncoderHandler());
            ch.pipeline().addLast(new EchoHandler());
        }
    });
    bootstrap.remoteAddress(ip,port);
    ChannelFuture future = bootstrap.connect().sync();

        future.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally {
        group.shutdownGracefully().sync();
    }
}

客户端的handler:

public class EchoHandler extends ChannelInboundHandlerAdapter {

    //连接成功后发送消息测试
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        User user = new User();
        user.setBirthday(new Date());
        user.setUID(UUID.randomUUID().toString());
        user.setName("冉鹏峰");
        user.setAge(22);
        DTObject dtObject = new DTObject();
        dtObject.setClassName(user.getClass().getName());
        dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
        TcpProtocol tcpProtocol = new TcpProtocol();
        byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
        tcpProtocol.setLen(objectBytes.length);
        tcpProtocol.setData(objectBytes);
        ctx.write(tcpProtocol);
        ctx.flush();
    }
}

这个handler是为了模拟在TCP连接建立好之后发送一包的数据到服务端经行测试,通过channel的write去发送数据,只要在启动类TcpClient配置了编码器的EncoderHandler,就可以直接将对象tcpProtocol传进去,它将在EncoderHandler的encode方法中被自动转换成字节码放入bytebuf中。

正常数据传输测试:

结果:

2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:30:34 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjMwOjE0IiwidWlkIjoiOGY0OTM0OGEtMWNmMy00ZTEyLWEzZTAtY2M1ZTJjZTkzMDdlIn0="}
2019-01-14 16:30:34 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='8f49348a-1cf3-4e12-a3e0-cc5e2ce9307e', birthday=Mon Jan 14 04:30:00 CST 2019}

可以看到最终的实体对象User被成功的解析出来。

在debug模式下还会看到这样的一个表格在控制台输出:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 79 |YmlydGhkYXkiOiIy|
|00000060| 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 30 |MDE5LzAxLzE0IDA0|
|00000070| 4f 6a 4d 77 4f 6a 45 30 49 69 77 69 64 57 6c 6b |OjMwOjE0IiwidWlk|
|00000080| 49 6a 6f 69 4f 47 59 30 4f 54 4d 30 4f 47 45 74 |IjoiOGY0OTM0OGEt|
|00000090| 4d 57 4e 6d 4d 79 30 30 5a 54 45 79 4c 57 45 7a |MWNmMy00ZTEyLWEz|
|000000a0| 5a 54 41 74 59 32 4d 31 5a 54 4a 6a 5a 54 6b 7a |ZTAtY2M1ZTJjZTkz|
|000000b0| 4d 44 64 6c 49 6e 30 3d 22 7d 63                |MDdlIn0="}c     |
+--------+-------------------------------------------------+----------------+

这个是相当于真实的数据抓包展示,数据被转换成字节码后是以二进制的形式在TCP缓存区冲传输过来。但是二进制太长了,所以一般都是转换成16进制显示的,一个表格显示一个字节的数据,数据由地位到高位由左到右,由上到下进行排列。

其中0x58为TcpProtocol中设置的开始标志,00 00 00 b5为数据的长度,因为是int类型所以占用了四个字节从7b--7d内容为要传输的数据内容,结尾的0x63为TcpProtocol设置的结束标志位。

粘包测试

为了模拟粘包,首先将启动类TcpClient中配置的编码器的EncoderHandler注释掉:

bootstrap.handler(new ChannelInitializer() {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
        //ch.pipeline().addLast(new EncoderHandler()); 因为需要在byteBuf中手动模拟粘包的场景
        ch.pipeline().addLast(new EchoHandler());
    }
});

然后在发送的时候故意将三帧的数据,放在一个包中就行发送,在EchoHanlder做如下修改:

 public void channelActive(ChannelHandlerContext ctx) throws Exception {
        User user = new User();
        user.setBirthday(new Date());
        user.setUID(UUID.randomUUID().toString());
        user.setName("冉鹏峰");
        user.setAge(24);
        DTObject dtObject = new DTObject();
        dtObject.setClassName(user.getClass().getName());
        dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
        TcpProtocol tcpProtocol = new TcpProtocol();
        byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
        tcpProtocol.setLen(objectBytes.length);
        tcpProtocol.setData(objectBytes);
        ByteBuf buffer = ctx.alloc().buffer();
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        //模拟粘包的第二帧数据
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        //模拟粘包的第三帧数据
        buffer.writeByte(tcpProtocol.getHeader());
        buffer.writeInt(tcpProtocol.getLen());
        buffer.writeBytes(tcpProtocol.getData());
        buffer.writeByte(tcpProtocol.getTail());
        ctx.write(buffer);
        ctx.flush();
    }

运行结果:

2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}

服务器成功解析出来了三帧的数据,BusinessHandlerchannelRead方法被调用了三次。

而抓到的数据包也确实是模拟的三帧数据黏在一个包中:

         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 79 |YmlydGhkYXkiOiIy|
|00000060| 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 30 |MDE5LzAxLzE0IDA0|
|00000070| 4f 6a 51 30 4f 6a 45 30 49 69 77 69 64 57 6c 6b |OjQ0OjE0IiwidWlk|
|00000080| 49 6a 6f 69 4f 44 46 6b 5a 54 55 35 59 57 55 74 |IjoiODFkZTU5YWUt|
|00000090| 4d 7a 51 34 4d 69 30 30 5a 44 46 68 4c 57 4a 6a |MzQ4Mi00ZDFhLWJj|
|000000a0| 4e 44 4d 74 4e 32 4e 6a 4d 54 4a 6d 4f 54 49 31 |NDMtN2NjMTJmOTI1|
|000000b0| 5a 54 55 78 49 6e 30 3d 22 7d 【63】 58 00 00 00 b5 |ZTUxIn0="}cX....|
|000000c0| 7b 22 63 6c 61 73 73 4e 61 6d 65 22 3a 22 70 6f |{"className":"po|
|000000d0| 6a 6f 2e 55 73 65 72 22 2c 22 6f 62 6a 65 63 74 |jo.User","object|
|000000e0| 22 3a 22 65 79 4a 75 59 57 31 6c 49 6a 6f 69 35 |":"eyJuYW1lIjoi5|
|000000f0| 59 61 4a 36 62 6d 50 35 62 4f 77 49 69 77 69 59 |YaJ6bmP5bOwIiwiY|
|00000100| 57 64 6c 49 6a 6f 79 4e 43 77 69 59 6d 6c 79 64 |WdlIjoyNCwiYmlyd|
|00000110| 47 68 6b 59 58 6b 69 4f 69 49 79 4d 44 45 35 4c |GhkYXkiOiIyMDE5L|
|00000120| 7a 41 78 4c 7a 45 30 49 44 41 30 4f 6a 51 30 4f |zAxLzE0IDA0OjQ0O|
|00000130| 6a 45 30 49 69 77 69 64 57 6c 6b 49 6a 6f 69 4f |jE0IiwidWlkIjoiO|
|00000140| 44 46 6b 5a 54 55 35 59 57 55 74 4d 7a 51 34 4d |DFkZTU5YWUtMzQ4M|
|00000150| 69 30 30 5a 44 46 68 4c 57 4a 6a 4e 44 4d 74 4e |i00ZDFhLWJjNDMtN|
|00000160| 32 4e 6a 4d 54 4a 6d 4f 54 49 31 5a 54 55 78 49 |2NjMTJmOTI1ZTUxI|
|00000170| 6e 30 3d 22 7d 【63】 58 00 00 00 b5 7b 22 63 6c 61 |n0="}cX....{"cla|
|00000180| 73 73 4e 61 6d 65 22 3a 22 70 6f 6a 6f 2e 55 73 |ssName":"pojo.Us|
|00000190| 65 72 22 2c 22 6f 62 6a 65 63 74 22 3a 22 65 79 |er","object":"ey|
|000001a0| 4a 75 59 57 31 6c 49 6a 6f 69 35 59 61 4a 36 62 |JuYW1lIjoi5YaJ6b|
|000001b0| 6d 50 35 62 4f 77 49 69 77 69 59 57 64 6c 49 6a |mP5bOwIiwiYWdlIj|
|000001c0| 6f 79 4e 43 77 69 59 6d 6c 79 64 47 68 6b 59 58 |oyNCwiYmlydGhkYX|
|000001d0| 6b 69 4f 69 49 79 4d 44 45 35 4c 7a 41 78 4c 7a |kiOiIyMDE5LzAxLz|
|000001e0| 45 30 49 44 41 30 4f 6a 51 30 4f 6a 45 30 49 69 |E0IDA0OjQ0OjE0Ii|
|000001f0| 77 69 64 57 6c 6b 49 6a 6f 69 4f 44 46 6b 5a 54 |widWlkIjoiODFkZT|
|00000200| 55 35 59 57 55 74 4d 7a 51 34 4d 69 30 30 5a 44 |U5YWUtMzQ4Mi00ZD|
|00000210| 46 68 4c 57 4a 6a 4e 44 4d 74 4e 32 4e 6a 4d 54 |FhLWJjNDMtN2NjMT|
|00000220| 4a 6d 4f 54 49 31 5a 54 55 78 49 6e 30 3d 22 7d |JmOTI1ZTUxIn0="}|
|00000230|【63】                                              |c               |
+--------+-------------------------------------------------+----------------+

可以看到确实存在三个尾巴【63】

在netty4.x版本中,粘包问题确实被netty的ByteToMessageDecoder中的CallDecode方法中给处理掉了。

拆包问题

这次还是将TcpClient中的编码器EncoderHandler注释掉,然后在EchoHandlerchannelActive中模拟数据的拆包问题:

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    User user = new User();
    user.setBirthday(new Date());
    user.setUID(UUID.randomUUID().toString());
    user.setName("冉鹏峰");
    user.setAge(24);
    DTObject dtObject = new DTObject();
    dtObject.setClassName(user.getClass().getName());
    dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
    TcpProtocol tcpProtocol = new TcpProtocol();
    byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
    tcpProtocol.setLen(objectBytes.length);
    tcpProtocol.setData(objectBytes);
    ByteBuf buffer = ctx.alloc().buffer();
    buffer.writeByte(tcpProtocol.getHeader());
    buffer.writeInt(tcpProtocol.getLen());
    buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//只发送二分之一的数据包
    //模拟拆包
    ctx.write(buffer);
    ctx.flush();
    Thread.sleep(3000);//模拟网络延时
    buffer = ctx.alloc().buffer();        
    buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen()));//将剩下的二分之和尾巴发送过去
    buffer.writeByte(tcpProtocol.getTail());
    ctx.write(buffer);
    ctx.flush();

}

运行结果:

首先是客户端这边:

2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] WRITE: 95B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49    |YmlydGhkYXkiOiI |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] FLUSH
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] WRITE: 92B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 |yMDE5LzAxLzE0IDA|
|00000010| 31 4f 6a 41 34 4f 6a 45 30 49 69 77 69 64 57 6c |1OjA4OjE0IiwidWl|
|00000020| 6b 49 6a 6f 69 4f 57 45 79 5a 6a 49 35 4d 6d 4d |kIjoiOWEyZjI5MmM|
|00000030| 74 4d 6a 4d 35 4f 43 30 30 5a 6a 6b 77 4c 57 46 |tMjM5OC00ZjkwLWF|
|00000040| 6b 5a 57 59 74 5a 6d 46 6c 4e 44 45 7a 5a 6a 55 |kZWYtZmFlNDEzZjU|
|00000050| 35 4e 32 45 33 49 6e 30 3d 22 7d 63             |5N2E3In0="}c    |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] FLUSH

确实是将数据分成两包发送出去了

再看看服务端的输出日志:

2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x8e5811b3, L:/127.0.0.1:8777 - R:/127.0.0.1:51138] RECEIVED: 95B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49    |YmlydGhkYXkiOiI |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x8e5811b3, L:/127.0.0.1:8777 - R:/127.0.0.1:51138] RECEIVED: 92B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 |yMDE5LzAxLzE0IDA|
|00000010| 31 4f 6a 41 34 4f 6a 45 30 49 69 77 69 64 57 6c |1OjA4OjE0IiwidWl|
|00000020| 6b 49 6a 6f 69 4f 57 45 79 5a 6a 49 35 4d 6d 4d |kIjoiOWEyZjI5MmM|
|00000030| 74 4d 6a 4d 35 4f 43 30 30 5a 6a 6b 77 4c 57 46 |tMjM5OC00ZjkwLWF|
|00000040| 6b 5a 57 59 74 5a 6d 46 6c 4e 44 45 7a 5a 6a 55 |kZWYtZmFlNDEzZjU|
|00000050| 35 4e 32 45 33 49 6e 30 3d 22 7d 63             |5N2E3In0="}c    |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:08:36 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjA4OjE0IiwidWlkIjoiOWEyZjI5MmMtMjM5OC00ZjkwLWFkZWYtZmFlNDEzZjU5N2E3In0="}
2019-01-14 17:08:36 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='9a2f292c-2398-4f90-adef-fae413f597a7', birthday=Mon Jan 14 05:08:00 CST 2019}

在第一包数据,判断到bytebuf中的可读内容不够的时候,终止解码,并且从父类的callDecode中的while循环break出去,在父类的channelRead中等待下一包数据到来的时候将两包数据合并起来再次decode解码。

最后测试下同时出现拆包、粘包的场景

还是将TcpClient中的编码器EncoderHandler注释掉,然后在EchoHandlerChannelActive方法:

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    User user = new User();
    user.setBirthday(new Date());
    user.setUID(UUID.randomUUID().toString());
    user.setName("冉鹏峰");
    user.setAge(24);
    DTObject dtObject = new DTObject();
    dtObject.setClassName(user.getClass().getName());
    dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
    TcpProtocol tcpProtocol = new TcpProtocol();
    byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
    tcpProtocol.setLen(objectBytes.length);
    tcpProtocol.setData(objectBytes);
    ByteBuf buffer = ctx.alloc().buffer();
    buffer.writeByte(tcpProtocol.getHeader());
    buffer.writeInt(tcpProtocol.getLen());
    buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//拆包,只发送一半的数据

    ctx.write(buffer);
    ctx.flush();
    Thread.sleep(3000);
    buffer = ctx.alloc().buffer();
    buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen())); //拆包发送剩余的一半数据   
    buffer.writeByte(tcpProtocol.getTail());
    //模拟粘包的第二帧数据
    buffer.writeByte(tcpProtocol.getHeader());
    buffer.writeInt(tcpProtocol.getLen());
    buffer.writeBytes(tcpProtocol.getData());
    buffer.writeByte(tcpProtocol.getTail());
    //模拟粘包的第三帧数据
    buffer.writeByte(tcpProtocol.getHeader());
    buffer.writeInt(tcpProtocol.getLen());
    buffer.writeBytes(tcpProtocol.getData());
    buffer.writeByte(tcpProtocol.getTail());
    ctx.write(buffer);
    ctx.flush();

}

最后直接查看服务端的输出结果:

2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……
2019-01-14 17:19:28 DEBUG [DEBUG] [id: 0xc46234aa, L:/127.0.0.1:8777 - R:/127.0.0.1:51466] RECEIVED: 466B
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}

总结

对于拆包、粘包只要配合netty的设计原则去实现代码,就能愉快且轻松的解决了。本例虽然通过DTObject包装了数据,避免解码时每增加一种对象类型,就要新增一个if判断的尴尬。但是仍然无法处理传输List、Map时候的场景。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

实现一款高可用的 TCP 数据传输服务器(Java版) 的相关文章

  • Spark:用列的平均值替换数据框中的空值

    如何创建 UDF 以编程方式将每列中 Spark 数据框中的空值替换为列平均值 例如 在示例中 数据 col1 空值的值为 2 4 6 8 5 5 5 示例数据 col1 col2 col3 2 null 3 4 3 3 6 5 null
  • LoaderManager 不接受“这个”

    好吧 我投降 我想不明白 我正在学习 Udacity 的 Android 基础知识课程 需要了解如何使用加载器加载数据 但是 当我使用以下行时 this 以红色突出显示并显示以下错误 Wrong 3rd argument type Foun
  • Log4j2 自动配置

    我在应用时遇到问题log4j2 xml自动配置正确 我认为这与我的文件夹排列有关 我在用着maven添加 log4j2 库并按如下方式安排我的项目 一个包含所有 通用 类的项目 供我的系统的服务器和客户端使用 另一个 核心 项目 服务器端应
  • 将 Spring 的 @Scheduled 注解与特定执行器一起使用

    如何告诉我的 Spring 计划方法使用特定的执行器运行 例如 这是我的 spring 调度程序方法之一 Scheduled fixedRate 1000 public void scheduleJobs doThese 这是我的 Java
  • 使用 Netbeans 导出 JAR

    如何使用Netbeans将java项目导出到JAR 我找不到像 Eclipse 那样的选项 您需要启用该选项 Project Properties gt Build gt Packaging gt Build JAR after compi
  • Java 更改包名称大小写约定的原因

    就在你说这是重复的之前 我已经看到了其他问题 但我仍然想发布这个 所以我在读用 Java 思考 Bruce Eckel这段话是关于小写命名约定的 Java 1 0 和 Java 1 1 中的域扩展com edu org net等 按照惯例都
  • android listviews:页眉和页脚视图

    在我的 ListActivity 中 我需要页眉和页脚视图 位于列表的顶部和底部 分别用作列表上的上一页和下一页按钮 因为我只想一次仅显示 20 个项目 我通过执行以下操作来设置头视图和脚视图 getListView addHeaderVi
  • 如何在 Java 中通过 TLS v.1.2 创建安全的 TCP 连接

    我想通过 TLS v1 2 在两个系统之间创建通信 其中包含的信息是保密的 我想避免 https Web 服务调用 而直接想在 TCP 层执行消息交换 您能否建议如何实现此功能 以便我可以通过 TLS v1 2 安全地传输数据 EDIT 阅
  • 为什么在 Java 7 中使用方法重载时,自动装箱不会推翻可变参数?

    我们的 Java 项目中有一个 LogManager 类 如下所示 public class LogManager public void log Level logLevel Object args do something public
  • netty 4.x 中 ServerBootstrap.option() 和 ServerBootstrap.childOption() 有什么区别

    根据文档4 0 中值得注意的新内容 http netty io wiki new and noteworthy in 4 0 html wiki h3 31 netty4提供了新的bootstrap API 文档给出了以下代码示例 publ
  • 编辑 CSV 文件(设计实现)

    我开始设计一个程序 该程序将根据已找到并保存到主 CSV Excel 文件中的相似字符串及其标识 自动执行正确查找和识别字符串的过程 现在我想正确设计它 这样我以后在实现 CSV Excel 读写部分时就不会遇到问题 我可能会使用 Open
  • Axis2 不返回自己的对象

    我编写了一些网络服务 其中一些返回一个简单的字符串 这个可以工作 另一些返回一个对象列表 不 axis2 1 5 1 无法处理集合类型 所以我将返回类型更改为Object 但我仍然收到这个异常 ERROR java lang Runtime
  • Hibernate添加带有子实体id的实体

    我的对象 状态 java Entity Table name STATE public class State Id GeneratedValue strategy GenerationType AUTO private Long id C
  • java.util.Prefs 抛出 BackingStoreException - 为什么?

    我有一个系统可以缓存启动时 SOAP 调用的微小 简单结果 我需要实例能够在启动时重新加载其缓存 以防 SOAP 服务失效 并且还需要处理使用此缓存文件的多个实例的可能性 我选择使用java util prefs但是 Java 的内置自动同
  • 在抽象类上使用 Lombok @SuperBuilder 注释和 toBuilder ?

    我有以下课程 SuperBuilder toBuilder true public abstract class Parent SuperBuilder toBuilder true public class Child extends P
  • Java ReentrantReadWriteLocks - 如何在读锁中安全地获取写锁?

    我现在在我的代码中使用可重入读写锁 http java sun com javase 6 docs api java util concurrent locks ReentrantReadWriteLock html同步对树状结构的访问 这
  • Resilience4j 重试 - 记录来自客户端的重试尝试?

    是否可以使用resilience4j在客户端记录重试尝试 也许通过某种配置或设置 目前 我正在将resilience4j与Spring boot Webflux一起使用基于注释 它运作良好 该项目很棒 当我们将服务器日志放在服务器端时 为了
  • JSP 自定义标签库:嵌套评估

    假设我有自定义标签库
  • 根据 netbeans 中的单选按钮切换组件的“启用”属性

    我在按钮组中有两个单选按钮 在同一面板中我有一个文本框和一个按钮 我想仅在选择第二个按钮时启用文本框和按钮 并在选择另一个单选按钮时禁用文本框和按钮 我已经尝试过这个但没有成功 private void radio button2Actio
  • Java - 类型转换问题

    long a long Math pow 2 32 a 4294967296 long a int long Math pow 2 32 a 0 long a int Math pow 2 32 a 2147483647 WTF 第一个表达

随机推荐

  • VS2013编译FFMPEG遇到的坑

    因为需要 用到老的FFMPEG库 但官方又没有编译好的LIB和DLL 只能下载源码自己编译了 但对LINUX环境不懂 只有在WINDOWS上配置环境编译 记录过程如下 1 下载FFMPEG源码 下载地址 http ffmpeg zerano
  • RV32I指令集

    RV32I是最基本的32位Base指令集 它支持32位寻址空间 支持字节地址访问 仅支持小端格式 little endian 高地址高位 低地址地位 寄存器也是32位整数寄存器 RV32I指令集的目的是尽量简化硬件的实施设计 所以它只有47
  • 蓝奏云访问修复(JavaScript 几个参数及命令讲解)

    每次都要更改蓝奏云分享链接中的 www lanzous com 为 ww lanzous com 麻烦 改 Hosts 有效果 但写个脚本不香么 蓝奏云访问 用到的命令和参数 window location href 当前页面的 URL 字
  • 解决粘包问题的三种方法

    方法一 定长数据流 服务器客户端提前协商 每个消息定长 不足的空白字符补足 方法二 特殊结束符 双方协商定义一个特殊的分隔符号 比如 等 只要没有发送分隔符就意味着一条数据没有结束 方法三 协议 相对最成熟额数据传递方式 由服务器开发者提供
  • http常见字段

    Host字段 客户端发送请求时 用来指定服务器域名 有了Host字段 就可以将请求发往同一台服务器上的不同网站 Content Length 字段 服务器在返回数据时 会有Content Length字段 表名本次回应的数据长度 Conne
  • Vue 团队公开快如闪电的全新脚手架工具,未来将替代 Vue-CLI,才300余行代码,学它!...

    1 前言 大家好 我是若川 欢迎关注我的公众号若川视野源码共读活动ruochuan12 想学源码 极力推荐之前我写的 学习源码整体架构系列 jQuery underscore lodash vuex sentry axios redux k
  • 求生之路2 局域网联机教程

    1 下载游侠网的 求生之路2 生存之旅2 V2 1 2 1中文绿色免安装公网联机版 2 双方保持版本号一致 3 版本号在E Left 4 Dead 2 left4dead2 steam inf里 4 对主程序建立快捷方式 5 在快捷方式的属
  • 文件服务器磁盘增量备份,完全备份、差异备份以及增量备份

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 常用的数据备份方式有完全备份 差异备份以及增量备份 那么这三种备份方式有什么区别 在具体应用中又该如何选择呢 1 备份方式简介 完全备份 Full Backup 备份全部选中的文件夹 并不依赖文
  • JS alert() 弹出框乱码的问题

    今天写了一个html 中间引用了js文件 chrome显示和html的字符集都是utf 8 但执行事件onclick alert 请输入数字 弹出乱码 百度一下 找到了解决方法 好了 废话不说了 1 在html中 引入js 插入的语句中指定
  • Mysql 启动报错解析:Starting MySQL.. ERROR! The server quit without updating PID file (/usr/local/mysql/dat

    问题 root centos74 service mysqld start Starting MySQL ERROR The server quit without updating PID file usr local mysql dat
  • 【Obsidian样式】修改文件夹名称和文件名称前的图标

    声明 本文大部分内容来自于https www bilibili com read cv17822164 方法一 Obsidian Icon Folder插件 Github https github com FlorianWoelki obs
  • 中断管理基础学习笔记 - 6. 软中断

    目录 1 前言 2 软中断概述 软中断类型 软中断数据结构 3 注册软中断 4 触发软中断 raise softirq raise softirq irqoff 5 执行软中断 中断返回 ksoftirqd local bh enable
  • 在windows环境下使用wget

    众所周知呢wget这个工具是Linux Unix下才能使用的 但是本人公司的电脑上没有这两个系统 产品经理又硬性要求 所以查百度 问朋友终于搞定了 wget在windows的环境下使用原来以前都有前辈做到了 这里是自己做下笔记 以方便自己以
  • telbot负载均衡设置

    telbot负载均衡设置 概述 golang 横向扩展电报机器人 采用webhook方式 nginx做负载均衡 这样就可以分担请求 减小单个服务器的负担 配置方式 负载端 package main import tele gopkg in
  • idea eclipse 快捷键全局搜索替换代码 修改包名

    全局搜索替换 选中项目右击 点击Replace in files 在文件中替换 输入查找 替换的内容 replaceAll 替换所有 全局搜索查找代码Ctrl H File mask 选择文件类型 可以更精确查找 文件查找Ctrl Shif
  • STM32+LZMA的移植经历

    连续一天加一个晚上 查资料 发现无解 压缩不可能在STM32上进行的 因为代码中那个内存分配的结构体占内存太大了 解压是可以实现的 因此这个算法在STM32上也是有一些作用 在某些场合可以在PC机进行压缩存入ARM中 然后ARM解压 比如某
  • Sql Server判断数据库、表、存储过程、函数是否存在

    判断数据库是否存在 if exists select from sys databases where name 数据库名 drop database 数据库名 判断表是否存在 if exists select from sysobject
  • 【无标题】vxworks ARM Pl330DMA 数据传输指令流创建

    pl330DmaChanMicroCodeCreate create micro code for dma transfer This routine create micro code for dma transfer RETURNS O
  • Linux chromium 源码下载及编译

    1 Chromium Souce Code Download 1 1 download source code 1 gt 方法一 获得所有发布tag的信息 git fetch tags 根据37 0 2062 103版本建立一个你自己的分支
  • 实现一款高可用的 TCP 数据传输服务器(Java版)

    1 netty能做什么 首先netty是一款高性能 封装性良好且灵活 基于NIO 真 非阻塞IO 的开源框架 可以用来手写web服务器 TCP服务器等 支持的协议丰富 如 常用的HTTP HTTPS WEBSOCKET 并且提供的大量的方法