在實際網(wǎng)絡應用中,我們接收和發(fā)送的數(shù)據(jù)都是以實際應用數(shù)據(jù)類型為單位的(比如一個Http數(shù)據(jù)體,或者一個ThriftObject)。而對于Socket而言,它處理的是TCP傳輸層的數(shù)據(jù),在它接收或發(fā)送的一個TCP包中,可能正好對應一個ThriftObject,或者多個ThriftObject、ThriftObject的一部分,甚至可能由多個ThriftObject的多個部分組成。這就是TCP的粘包半包問題。
Netty提供了一種機制可以幫助我們方便地處理TCP半包和粘包問題,它是通過嵌入ChannelHandler來實現(xiàn)的。
下面以一段簡單的代碼實例來看一下,如何在Netty中處理TCP粘包和拆包問題。可以看到代碼中添加了類型為FixedLengthFrameDecoder
的ChannelHandler,添加這段代碼的效果就是每次會截取定量長度為1024的字節(jié)數(shù)據(jù)作為下層ChannelHandler的input處理對象。
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//處理TCP半包粘包問題
ch.pipeline().addLast(new FixedLengthFrameDecoder(1024));
}
})
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new SimpleNettyServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
1. 源碼分析
我們就以FixedLengthFrameDecoder
為例,它的實現(xiàn)非常簡單,繼承自類ByteToMessageDecoder
,并對父類的抽象方法decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
進行了實現(xiàn)。所以,要分析Netty對TCP粘包拆包的處理,核心邏輯在于ByteToMessageDecoder
。
public class FixedLengthFrameDecoder extends ByteToMessageDecoder{
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
{.....}
}
1.1 流程概述
4個核心方法:
ByteToMessageDecoder channelRead(..)
ByteToMessageDecoder callDecode(..)
ByteToMessageDecoder fireChannelRead(..)
FixedLengthFrameDecoder decode(..)
method | Class | 說明 |
---|---|---|
void channelRead( ChannelHandlerContext ctx, Object msg) | ByteToMessageDecoder | 入口方法,入?yún)sg會是一個ByteBuf。主要過程就是整合一下組裝成一個新的ByteBuf cumulation,然后對這個cumulation 進行callDecode |
void callDecode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) | ByteToMessageDecoder | 對ByteBuf進行解碼,解析成一組JavaObject到out中,會在該方法中調用實際的decode方法,并會調用fireChannelRead方法進行ChannelHandler的傳遞 |
void fireChannelRead( ChannelHandlerContext ctx, List<Object> msgs, int numElements) | ByteToMessageDecoder | 該方法會對解析生成的JavaObject進行下層ChannelHandler的傳遞 |
decode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) | FixedLengthFrameDecoder | 實際進行decode的方法,會將ByteBuf按照固定長度進行拆分成一組Object |
1.2 channelRead方法分析
通過文章 Netty源碼分析-ChannelPipeline 的分析,channelRead
方法的調用是在ChannelPipeline中,入?yún)sg會是一個ByteBuf。
第一步:首先會實例化一個空的CodecOutputList
,用于存放一會將要解碼生成的對象。
第二步:核心在于賦值cumulation,cumulation的類型也是ByteBuf,它與入?yún)⒌膍sg會有什么不同呢?如果cumulation為null,會直接將msg的地址賦值給cumulation,否則會將cumulator.cumulate(ctx.alloc(), cumulation, data)
方法返回值賦值給cumulation,看一下方法描述“Cumulate the given {@link ByteBuf}s and return the {@link ByteBuf} that holds the cumulated bytes.
”,原來是將msg與cumulation進行一個merge。這就相當于將兩個TCP包的數(shù)據(jù)進行了一個數(shù)據(jù)上的銜接。
第三步:調用callDecode
對cumulation進行解碼。
@Override
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);
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
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);
}
}
1.3 callDecode方法分析
callDecode的核心流程就是對ByteBuf進行遍歷,遍歷的過程中不斷調用decode方法解析出Object對象,并對解析出的對象執(zhí)行fireChannelRead方法,保證Pipeline的往下傳遞。
代碼注釋中會對過程進行分析。
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
int outSize = out.size();
if (outSize > 0) {
//說明有解碼好的對象,對這些對象進行Pipeline的往下傳遞
fireChannelRead(ctx, out, outSize);
out.clear();
if (ctx.isRemoved()) {
break;
}
outSize = 0;
}
int oldInputLength = in.readableBytes();
//真正調用實際decode方法進行解碼的地方
decodeRemovalReentryProtection(ctx, in, out);
if (ctx.isRemoved()) {
break;
}
// 說明沒有解碼到新對象,這時候如果ByteBuf沒有移動,說明此次ByteBuf內容不足以解碼,會直接break。
if (outSize == out.size()) {
if (oldInputLength == in.readableBytes()) {
break;
} else {
continue;
}
}
//解碼到了對象,但是卻沒有移動ByteBuf,說明有問題
if (oldInputLength == in.readableBytes()) {
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Throwable cause) {
throw new DecoderException(cause);
}
}
1.4 fireChannelRead方法分析
fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements)
方法的入?yún)⑹且呀?jīng)解碼后產(chǎn)生的List<Object> ,會遍歷這些Object,分別調用ctx.fireChannelRead(final Object msg)
進行ChanelPipeline的往下傳遞。
下邊也列出了ctx.fireChannelRead(final Object msg)
的代碼實現(xiàn),findContextInbound()會找到ctx的下一個AbstractChannelHandlerContext,將ChannelPipeline進行往后傳遞。
static void fireChannelRead(ChannelHandlerContext ctx, List<Object> msgs, int numElements) {
if (msgs instanceof CodecOutputList) {
fireChannelRead(ctx, (CodecOutputList) msgs, numElements);
} else {
for (int i = 0; i < numElements; i++) {
ctx.fireChannelRead(msgs.get(i));
}
}
}
abstract class AbstractChannelHandlerContext{
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(), msg);
return this;
}
}
1.5 decode方法分析
下面是類FixedLengthFrameDecoder的代碼實現(xiàn)。可以看到非常簡單:判斷當前的ByteBuf長度夠不夠一個Frame的長度,如果不夠不處理,否則會解碼出一個Frame并添加至 List<Object> out 中,然后將ByteBuf指針前移frameLength長度。
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
protected Object decode(
@SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (in.readableBytes() < frameLength) {
return null;
} else {
return in.readRetainedSlice(frameLength);
}
}
2. 其他實現(xiàn)
上邊的例子中,我們講述了解決TCP粘包拆包的一個例子-分割成固定長度的Frame。在實際應用中,會根據(jù)應用層業(yè)務實體類型進行不同的decode解碼,比如Http應用中需要解碼出HttpRequest,thrift RPC調用中需要解碼出ThriftObject等等。
實際Netty已經(jīng)幫助使用者做了非常多的工作,像常用的HttpRequestDecoder可以幫助我們解碼出Http對象,XmlDecoder可以幫助我們解碼出XML對象,類似的還有json對象解碼、WebSocket對象解碼等等。
如果Netty已經(jīng)提供的Decoder無法滿足你的要求,你也可以實現(xiàn)自己的Decoder。過程非常簡單,只需要繼承ByteToMessageDecoder類并實現(xiàn)抽象方法decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
.