Dubbo处理TCP拆包粘包问题

#Dubbo处理TCP拆包粘包问题

在TCP网络传输工程中,由于TCP包的缓存大小限制,每次请求数据有可能不在一个TCP包里面,或者也可能多个请求的数据在一个TCP包里面。那么如果合理的decode接受的TCP数据很重要,需要考虑TCP拆包和粘包的问题。我们知道在Netty提供了各种Decoder来解决此类问题,比如LineBasedFrameDecoder,LengthFieldBasedFrameDecoder等等,但是这些都是处理一些通用简单的协议栈,并不能处理高度自定义的协议栈。由于dubbo协议是自定义协议栈,并且包含消息头和消息体两部分,而消息头中包含消息类型、协议版本、协议魔数以及payload长度等信息。所以使用Netty自带的处理方案可能无法满足Dubbo解析自身协议的需求,所以需要Dubbo自己来处理,那自己处理,就需要自己处理TCP的拆包和粘包的问题。这里就对Dubbo处理此类问题进行探讨,从而加深自己对它的理解。

##说明 此处所描述的协议是dubbo协议,其他的协议比如http,webservice等协议不是这里讨论范围。并且这里使用的通信框架以Netty来讲解,Mina以及grizzly也不在种类讨论范围。

##NettyCodecAdapter NettyCodecAdapter是对dubbo协议解析的入口,里面包含decoder和encoder两部分,而TCP的拆包和粘包主要是decoder部分,所以encoder这里不进行讨论。在NettyCodecAdapter中的decoder是由InternalDecoder来实现,它的父类是Netty的SimpleChannelUpstreamHandler可以接受所有inbound消息,那么就可以对接受的消息进行decode。这里需要说明一下对于某一个Channel都有一个私有的InternalDecoder对象,并不是和其他的Channel共享,这里就避免了并发问题,所以在InternalDecoder里面可以用单线程的方式去看待,这样就比较容易理解。

###InternalDecoder 每个channel的inbound消息都会发送到InternalDecodermessageReceived方法,而dubbo会先将接受的消息缓存到InternalDecoderbuffer属性中,这个变量很重要,后面会讨论。下面是messageReceived方法中将接受的消息负载到buffer实现。

1 private class InternalDecoder extends SimpleChannelUpstreamHandler { 2 3 private com.alibaba.dubbo.remoting.buffer.ChannelBuffer buffer = 4 com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; 5 6 @Override 7 public void messageReceived(ChannelHandlerContext ctx, MessageEvent event) throws Exception { 8 Object o = event.getMessage(); 9 if (! (o instanceof ChannelBuffer)) { 10 ctx.sendUpstream(event); 11 return; 12 } 13 14 ChannelBuffer input = (ChannelBuffer) o; 15 int readable = input.readableBytes(); 16 if (readable <= 0) { 17 return; 18 } 19 20 com.alibaba.dubbo.remoting.buffer.ChannelBuffer message; 21 if (buffer.readable()) { 22 if (buffer instanceof DynamicChannelBuffer) { 23 buffer.writeBytes(input.toByteBuffer()); 24 message = buffer; 25 } else { 26 int size = buffer.readableBytes() + input.readableBytes(); 27 message = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer( 28 size > bufferSize ? size : bufferSize); 29 message.writeBytes(buffer, buffer.readableBytes()); 30 message.writeBytes(input.toByteBuffer()); 31 } 32 } else { 33 message = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.wrappedBuffer( 34 input.toByteBuffer()); 35 } 36 37 NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler); 38 Object msg; 39 int saveReaderIndex; 40 41 try { 42 // decode object. 43 do { 44 saveReaderIndex = message.readerIndex(); 45 try { 46 msg = codec.decode(channel, message); 47 } catch (IOException e) { 48 buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; 49 throw e; 50 } 51 if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) { 52 message.readerIndex(saveReaderIndex); 53 break; 54 } else { 55 if (saveReaderIndex == message.readerIndex()) { 56 buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; 57 throw new IOException("Decode without read data."); 58 } 59 if (msg != null) { 60 Channels.fireMessageReceived(ctx, msg, event.getRemoteAddress()); 61 } 62 } 63 } while (message.readable()); 64 } finally { 65 if (message.readable()) { 66 message.discardReadBytes(); 67 buffer = message; 68 } else { 69 buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; 70 } 71 NettyChannel.removeChannelIfDisconnected(ctx.getChannel()); 72 } 73 } 74 75 @Override 76 public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { 77 ctx.sendUpstream(e); 78 } 79 }

首先是判断当前decoder对象的buffer中是否有可以读取的消息,如果有则进行合并,并且把对象引用赋予message局部变量,所以message则获取了当前channel的inbound消息。得到inbound消息之后,那么接下来就是对协议的解析了。

1 do { 2 saveReaderIndex = message.readerIndex(); 3 try { 4 msg = codec.decode(channel, message); 5 } catch (IOException e) { 6 buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; 7 throw e; 8 } 9 if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) { 10 message.readerIndex(saveReaderIndex); 11 break; 12 } else { 13 if (saveReaderIndex == message.readerIndex()) { 14 buffer = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER; 15 throw new IOException("Decode without read data."); 16 } 17 if (msg != null) { 18 Channels.fireMessageReceived(ctx, msg, event.getRemoteAddress()); 19 } 20 } 21 } while (message.readable());

这里首先要做的是把当前message的读索引保存到局部变量saveReaderIndex中,用于后面的消息回滚。后面紧接着是对消息的decode,这里的codecDubboCountCodec对象实体,这里需要注意一点,DubboCountCodecdecode每次只会解析出一个完整的dubbo协议栈,带着这个看看decode的实现。

1 public Object decode(Channel channel, ChannelBuffer buffer) throws IOException { 2 int save = buffer.readerIndex(); 3 MultiMessage result = MultiMessage.create(); 4 do { 5 Object obj = codec.decode(channel, buffer); 6 if (Codec2.DecodeResult.NEED_MORE_INPUT == obj) { 7 buffer.readerIndex(save); 8 break; 9 } else { 10 result.addMessage(obj); 11 logMessageLength(obj, buffer.readerIndex() - save); 12 save = buffer.readerIndex(); 13 } 14 } while (true); 15 if (result.isEmpty()) { 16 return Codec2.DecodeResult.NEED_MORE_INPUT; 17 } 18 if (result.size() == 1) { 19 return result.get(0); 20 } 21 return result; 22 }

这里暂存了当前buffer的读索引,同样也是为了后面的回滚。可以看到当decode返回的是NEED_MORE_INPUT则表示当前的buffer中数据不足,不能完整解析出一个dubbo协议栈,同时将buffer的读索引回滚到之前暂存的索引并且退出循环,将结果返回。那接下来看看什么时候会返回NEED_MORE_INPUT,最终会定位到在ExchangeCodecdecode方法会解析出协议栈。

1 protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException { 2 // check magic number. 3 if (readable > 0 && header[0] != MAGIC_HIGH 4 || readable > 1 && header[1] != MAGIC_LOW) { 5 int length = header.length; 6 if (header.length < readable) { 7 header = Bytes.copyOf(header, readable); 8 buffer.readBytes(header, length, readable - length); 9 } 10 for (int i = 1; i < header.length - 1; i ++) { 11 if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) { 12 buffer.readerIndex(buffer.readerIndex() - header.length + i); 13 header = Bytes.copyOf(header, i); 14 break; 15 } 16 } 17 return super.decode(channel, buffer, readable, header); 18 } 19 // check length. 20 if (readable < HEADER_LENGTH) { 21 return DecodeResult.NEED_MORE_INPUT; 22 } 23 24 // get data length. 25 int len = Bytes.bytes2int(header, 12); 26 checkPayload(channel, len); 27 28 int tt = len + HEADER_LENGTH; 29 if( readable < tt ) { 30 return DecodeResult.NEED_MORE_INPUT; 31 } 32 33 // limit input stream. 34 ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len); 35 36 try { 37 return decodeBody(channel, is, header); 38 } finally { 39 if (is.available() > 0) { 40 try { 41 if (logger.isWarnEnabled()) { 42 logger.warn("Skip input stream " + is.available()); 43 } 44 StreamUtils.skipUnusedStream(is); 45 } catch (IOException e) { 46 logger.warn(e.getMessage(), e); 47 } 48 } 49 } 50 }

这个方法开始是对telnet协议进行解析(由于dubbo支持telnet连接,所以这里提供了支持,可以忽略这一部分)。看到会有两个地方返回NEED_MORE_INPUT,一个是当前buffer的可读长度还没有消息头长,说明当前buffer连协议栈的头都不完整,所以需要继续读取inbound数据,另一个是当前buffer包含了完整的消息头,便可以得到payload的长度,发现它的可读的长度,并没有包含整个协议栈的数据,所以也需要继续读取inbound数据。如果上面两个情况都不复核,那么说明当前的buffer至少包含一个dubbo协议栈的数据,那么从当前buffer中读取一个dubbo协议栈的数据,解析出一个dubbo数据,当然这里可能读取完一个dubbo数据之后还会有剩余的数据。

上面对dubbo解析出一个完整的dubbo协议栈过程进行了讨论,但是还没有对TCP的拆包和粘包问题做过多的讨论。下面结合上面内容做一个综合讨论。

我这里对TCP拆包和粘包分别列举一个场景来讨论。

###当反生TCP拆包问题时候 这里假设之前还没有发生过任何数据交互,系统刚刚初始化好,那么这个时候在InternalDecoder里面的buffer属性会是EMPTY_BUFFER。当发生第一次inbound数据的时候,第一次在InternalDecoder里面接收的肯定是dubbo消息头的部分(这个由TCP协议保证),由于发生了拆包情况,那么此时接收的inbound消息可能存在一下几种情况

1、当前inbound消息只包含dubbo协议头的一部分

2、当前inbound消息只包含dubbo的协议头

3、当前inbound消息只包含dubbo消息头和部分payload消息

通过上面的讨论,我们知道发生上面三种情况,都会触发ExchangeCodec返回NEED_MORE_INPUT,由于在DubboCountCodec对余返回NEED_MORE_INPUT会回滚读索引,所以此时的buffer里面的数据可以当作并没有发生过读取操作,并且DubboCountCodec的decode也会返回NEED_MORE_INPUT,在InternalDecoder对于当判断返回NEED_MORE_INPUT,也会进行读索引回滚,并且退出循环,最后会执行finally内容,这里会判断inbound消息是否还有可读的,由于在DubboCountCodec里面进行了读索引回滚,所以次数的buffer里面是完整的inbound消息,等待第二次的inbound消息的到来,当第二次inbound消息过来的时候,再次经过上面的判断。

###当发生TCP粘包的时候 当发生粘包的时候是tcp将一个以上的dubbo协议栈放在一个tcp包中,那么有可能发生下面几种情况

1、当前inbound消息只包含一个dubbo协议栈

2、当前inbound消息包含一个dubbo协议栈,同时包含部分另一个或者多个dubbo协议栈内容

如果发生只包含一个协议栈,那么当前buffer通过ExchangeCodec解析协议之后,当前的buffer的readeIndex位置应该是 buffer尾部,那么在返回到InternalDecodermessage的方法readable返回的是false,那么就会对buffer重新赋予EMPTY_BUFFER实体,而针对包含一个以上的dubbo协议栈,当然也会解析出其中一个dubbo协议栈,但是经过ExchangeCodec解析之后,message的readIndex不在message尾部,所以messagereadable方法返回的是true。那么则会继续遍历message,读取下面的信息。最终要么message刚好整数倍包含完整的dubbo协议栈,要不ExchangeCodec返回NEED_MORE_INPUT,最后将未读完的数据缓存到buffer中,等待下次inbound事件,将buffer中的消息合并到下次的inbound消息中,种类又回到了拆包的问题上。

##总结

dubbo在处理tcp的粘包和拆包时是借助InternalDecoderbuffer缓存对象来缓存不完整的dubbo协议栈数据,等待下次inbound事件,合并进去。所以说在dubbo中解决TCP拆包和粘包的时候是通过buffer变量来解决的。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

tcp粘包与udp丢包的原因

tcp粘包与udp丢包的原因一,什么是tcp粘包与udp丢包TCP是面向流的, 流要说明就像河水一样, 只要有水, 就会一直流向低处, 不会间断. TCP为了提高传输效率, 发送数据的时候, 并不是直接发送数据到网路, 而是先暂存到系统缓冲, 超过时间或者缓冲满了, 才把缓冲区的内容发送

tcp的三次握手四次挥手

tcp的三次握手流程:在tcp/ip协议中,tcp协议提供可靠的连接服务,采用三次握手建立一个连接。第一次握手:建立连接时,客户端发送SYN包【synj】到服务器,并进入SYN_SEND状态,等待服务器确认;第二次握手:服务器收到SYN包,必须确认客户的SYN(ackj1),同时自己也发送一个SYN包(syn

Netty使用解码器Decoder解决TCP粘包和拆包问题

解码器Decoder和ChannelHandler的关系netty的解码器通常是继承自ByteToMessageDecoder,而它又是继承自ChannelInboundHandlerAdapter,其实也是一种ChannelHandler和我们自定义的ChannelHandler一样都是来处理进