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

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依赖
io.netty    netty-all    4.1.6.final     com.fasterxml.jackson.core    jackson-databind    2.9.7     log4j    log4j    1.2.17  
设计一套基于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() {                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()+ 期待的数据协议类是:+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 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 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 1651 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1651 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida0ojq0oje0iiwidwlkijoiodfkztu5ywutmzq4mi00zdfhlwjjndmtn2njmtjmoti1ztuxin0=}2019-01-14 1651 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=mon jan 14 0400 cst 2019}2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1651 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida0ojq0oje0iiwidwlkijoiodfkztu5ywutmzq4mi00zdfhlwjjndmtn2njmtjmoti1ztuxin0=}2019-01-14 1651 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=mon jan 14 0400 cst 2019}2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1651 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1651 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida0ojq0oje0iiwidwlkijoiodfkztu5ywutmzq4mi00zdfhlwjjndmtn2njmtjmoti1ztuxin0=}2019-01-14 1651 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=mon jan 14 0400 cst 2019}  
服务器成功解析出来了三帧的数据,businesshandler的channelread方法被调用了三次。
而抓到的数据包也确实是模拟的三帧数据黏在一个包中:
+-------------------------------------------------+         |  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注释掉,然后在echohandler的channelactive中模拟数据的拆包问题:
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 1733 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 1733 debug [debug] [id: 0x3b8cbbbb, l:/127.0.0.1:51138 - r:/127.0.0.1:8777] flush2019-01-14 1736 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 1736 debug [debug] [id: 0x3b8cbbbb, l:/127.0.0.1:51138 - r:/127.0.0.1:8777] flush  
确实是将数据分成两包发送出去了
再看看服务端的输出日志:
2019-01-14 1733 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 1733 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1733 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1733 debug [org.wisdom.server.decoder.decoderhandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……2019-01-14 1736 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 1736 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1736 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1736 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1736 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida1oja4oje0iiwidwlkijoioweyzji5mmmtmjm5oc00zjkwlwfkzwytzmflndezzju5n2e3in0=}2019-01-14 1736 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='9a2f292c-2398-4f90-adef-fae413f597a7', birthday=mon jan 14 0500 cst 2019}  
在第一包数据,判断到bytebuf中的可读内容不够的时候,终止解码,并且从父类的calldecode中的while循环break出去,在父类的channelread中等待下一包数据到来的时候将两包数据合并起来再次decode解码。
最后测试下同时出现拆包、粘包的场景
还是将tcpclient中的编码器encoderhandler注释掉,然后在echohandler的channelactive方法:
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 1725 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1725 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1725 debug [org.wisdom.server.decoder.decoderhandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……2019-01-14 1728 debug [debug] [id: 0xc46234aa, l:/127.0.0.1:8777 - r:/127.0.0.1:51466] received: 466b2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1728 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida1oje5oje0iiwidwlkijoiode2zjg2zditndbhms00mdrkltgwmwitzmy1nzgxmtjhnjfmin0=}2019-01-14 1728 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='816f86d2-40a1-404d-801b-ff578112a61f', birthday=mon jan 14 0500 cst 2019}2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1728 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida1oje5oje0iiwidwlkijoiode2zjg2zditndbhms00mdrkltgwmwitzmy1nzgxmtjhnjfmin0=}2019-01-14 1728 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='816f86d2-40a1-404d-801b-ff578112a61f', birthday=mon jan 14 0500 cst 2019}2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 开始解码数据……2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 数据开头格式正确2019-01-14 1728 debug [org.wisdom.server.decoder.decoderhandler] 数据解码成功2019-01-14 1728 debug [org.wisdom.server.business.businesshandler] 解码后的字节码:{classname:pojo.user,object:eyjuyw1lijoi5yaj6bmp5bowiiwiywdlijoyncwiymlydghkyxkioiiymde5lzaxlze0ida1oje5oje0iiwidwlkijoiode2zjg2zditndbhms00mdrkltgwmwitzmy1nzgxmtjhnjfmin0=}2019-01-14 1728 info [org.wisdom.server.business.businesshandler] 收到实体对象:user{name='冉鹏峰', age=24, uid='816f86d2-40a1-404d-801b-ff578112a61f', birthday=mon jan 14 0500 cst 2019}  
总结
对于拆包、粘包只要配合netty的设计原则去实现代码,就能愉快且轻松的解决了。本例虽然通过dtobject包装了数据,避免解码时每增加一种对象类型,就要新增一个if判断的尴尬。但是仍然无法处理传输list、map时候的场景。


联想宣布新旗舰Z5:95%极致屏占比,惊人全面屏设计超iPhone X
输电铁塔倾斜监测装置(太阳能供电,无线传输)
Airbus“垂直城市”概念机 打造飞行电动出租服务
PCB设计中应该考虑那些安规
POE以太网供电系统浪涌防护TVS二极管:SMCJ58CA
实现一款高可用的TCP数据传输服务器(Java版)
青云全新升级光格网络,赋能物联网产业的智能化建设
NI PXIe-5644R 射频矢量信号收发仪-首台软件完全自定义的仪器-
LED芯片厂供需结构阶段性失衡,LED行业进入洗牌周期
基于Entropic的Vestel机顶盒产品宣布发售
什么是MOS管?MOS管的典型应用和电路符号
关于蓝牙5给物联网带来的颠覆分析
虹科干货 | OPC UA技术,实现设备控制与互连未来
互联网IPv6技术的过渡及两种演进方案简析
一种新型模块化柔性驱动方法并且可以用3D打印机直接“打印”出来的软性机器人
浅谈对变压器瓦斯保护的检验
红米Note 7 Pro是迄今为止红米系列最强悍的手机
共基放大电路和“沃尔曼化”介绍
5G和自动驾驶技术发展助力中芯国际迈向高端代工
英特尔高管批评设备制造商的周转时间