“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第6篇文章,点击查看活动详情”
海上月是天上月,眼前人是心上人。中秋假期大家过得怎么样,我是拍了剩下的婚纱照,看了看Netty相关知识,又去和木工对了下装修细节,余下精力,想了想Dubbo还剩下协议这部分还没和大家分享,今天就带着大家一起看下Dubbo网络协议设计与编解码实现。
一、概述
众所周知,在TCP协议中,每层协议都有自己的协议报文格式,OSI七层网络协议如下图:
(图片资源来源于网络)
Dubbo作为基于TCP协议的应用,其协议设计也是参考了TCP协议,由header和body两部分组成,其中header格式如下图:
如上图所示,header一共包含了16字节数据:
- 前两个字节为魔数,类似Java字节码的0xCAFEBABE,只不过这里分别是0xda与0xbb。
- 第三个字节为请求类型与序列化id。
- 第四个字节只有在响应报文里才涉及,为响应的status,成功为20。
- 后面八个字节为请求ID。
- 最后的四字节是body的大小。
二、温故知新
@Override
protected void doOpen() throws Throwable {
bootstrap = new ServerBootstrap();
bossGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("NettyServerBoss", true));
workerGroup = new NioEventLoopGroup(getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
new DefaultThreadFactory("NettyServerWorker", true));
final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
channels = nettyServerHandler.getChannels();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
.childOption(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// FIXME: should we use getTimeout()?
int idleTimeout = UrlUtils.getIdleTimeout(getUrl());
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyServer.this);
if (getUrl().getParameter(SSL_ENABLED_KEY, false)) {
ch.pipeline().addLast("negotiation",
SslHandlerInitializer.sslServerHandler(getUrl(), nettyServerHandler));
}
ch.pipeline()
.addLast("decoder", adapter.getDecoder())
.addLast("encoder", adapter.getEncoder())
.addLast("server-idle-handler", new IdleStateHandler(0, 0, idleTimeout, MILLISECONDS))
.addLast("handler", nettyServerHandler);
}
});
// bind
ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
}
前文讲解Dubbo服务端处理请求的过程中曾提到过,服务端通过NettyServer的doOpen方法启动Netty服务,流程如下:
- 启动了一个boss线程,多个worker线程(优先获取URL里的配置,默认为(Runtime.getRuntime().availableProcessors() + 1,最大值为32)。
- 指定用来编解码的decoder与encoder。
- 启动心跳检测IdleStateHandler。
- 指定用来处理Netty请求的handler,nettyServerHandler。
- 绑定端口。
以上和本文相关的则是编解码器的实现,下面就让我们看看Dubboo编解码器是如何工作的
三、编解码器详解
查看adapter.getEncoder()实现如下:
private final ChannelHandler encoder = new InternalEncoder();
public ChannelHandler getEncoder() {
return encoder;
}
可以看到,最终获取到的是InternalEncoder实例,查看InternalEncoder实现如下:
private final Codec2 codec;
private class InternalEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
... ...
codec.encode(channel, buffer, msg);
}
}
这里最终是通过codec.encode方法进行的编码,查看Codec2实现如下:
@SPI
public interface Codec2 {
@Adaptive({Constants.CODEC_KEY})
void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException;
@Adaptive({Constants.CODEC_KEY})
Object decode(Channel channel, ChannelBuffer buffer) throws IOException;
enum DecodeResult {
NEED_MORE_INPUT, SKIP_SOME_INPUT
}
}
这里的Codec2也是SPI扩展点,默认实现为DubboCodec,查看其实现如下:
public class DubboCodec extends ExchangeCodec {
... ...
}
可以看到DubboCodec其实是ExchangeCodec的子类,ExchangeCodec类的encode和decode方法则是今天分析的重点。
3.1. ExchangeCodec-encode
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
if (msg instanceof Request) {
encodeRequest(channel, buffer, (Request) msg);
} else if (msg instanceof Response) {
encodeResponse(channel, buffer, (Response) msg);
} else {
super.encode(channel, buffer, msg);
}
}
核心逻辑如上,分别为对请求信息进行编码,对响应信息进行编码,对其他信息进行编码。
- encodeRequest实现
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
// 获取序列化方式,Dubbo默认序列化方式为hession
Serialization serialization = getSerialization(channel);
// 创建Dubbo协议头数组,HEADER_LENGTH长度为16
byte[] header = new byte[HEADER_LENGTH];
// 把魔数0xdabb写入协议头
Bytes.short2bytes(MAGIC, header);
// 设置请求类型与序列化类型
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
if (req.isEvent()) {
header[2] |= FLAG_EVENT;
}
// 设置请求id
Bytes.long2bytes(req.getId(), header, 4);
// 使用获取到的序列化方式对数据部分进行序列化
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) {
encodeEventData(channel, out, req.getData());
} else {
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
// 刷新缓存
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
int len = bos.writtenBytes();
// 检查数据是否合法
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);
// 将协议头写入缓存
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
- encodeResponse encodeResponse实现与encodeRequest不同之点在于协议头的第四个字节要写入响应类型,其他则几乎一致。
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
int savedWriteIndex = buffer.writerIndex();
try {
... ...
// 设置响应的状态码,正常响应status=20
byte status = res.getStatus();
header[3] = status;
// 对响应的消息体进行序列化
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
// encode response data or error message.
if (status == Response.OK) {
if (res.isHeartbeat()) {
encodeEventData(channel, out, res.getResult());
} else {
encodeResponseData(channel, out, res.getResult(), res.getVersion());
}
} else {
out.writeUTF(res.getErrorMessage());
}
... ...
} catch (Throwable t) {
}
}
3.2. ExchangeCodec-decode
查看adapter.getDecoder()实现,获取到结果为InternalDecoder实例,查看其实现如下:
private class InternalDecoder extends SimpleChannelUpstreamHandler {
private org.apache.dubbo.remoting.buffer.ChannelBuffer buffer =
org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent event)
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.getChannel(), url, handler);
Object msg;
int saveReaderIndex;
... ...
try {
// decode object.
do {
saveReaderIndex = message.readerIndex();
try {
// 解码操作
msg = codec.decode(channel, message);
} catch (IOException e) {
buffer = org.apache.dubbo.remoting.buffer.ChannelBuffers.EMPTY_BUFFER;
throw e;
}
// 碰到了半包问题,重置缓存下标
// 这里要注意,当检测到半包问题后,指针已经偏移了,因此要重置
if (msg == Codec2.DecodeResult.NEED_MORE_INPUT) {
message.readerIndex(saveReaderIndex);
break;
} else {
// 读到了一个完整消息,将消息继续传递,交给下一个handler进行处理
if (msg != null) {
Channels.fireMessageReceived(ctx, msg, event.getRemoteAddress());
}
}
} while (message.readable());
} finally {
}
}
}
InternalDecoder继承了SimpleChannelUpstreamHandler,重写了messageReceived方法,顾名思义,NettyServer收到消息流时,触发改方法,核心步骤如下:
-
首先调用codec.decode解码获取到对应的消息,这里的codec即为ExchangeCodec。
-
解码的时候,涉及到了TCP半包问题的处理(不了解什么是半包问题可自行百度tcp粘包和拆包)。
-
最后将完整的消息,继续传递给下一个Handler,最终由nettyServerHandler进行消息处理。
-
decode源码
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
// 协议头解析
int readable = buffer.readableBytes();
byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
buffer.readBytes(header);
// body解析
return decode(channel, buffer, readable, header);
}
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// 魔数校验
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, length, readable - length);
}
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
buffer.readerIndex(buffer.readerIndex() - header.length + i);
header = Bytes.copyOf(header, i);
break;
}
}
return super.decode(channel, buffer, readable, header);
}
// 是否读取到了一个完整的协议头,如果不是,则发生了半包问题
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// 协议头最后四个字节获取body的大小
int len = Bytes.bytes2int(header, 12);
checkPayload(channel, len);
int tt = len + HEADER_LENGTH;
// 判断是否发生半包问题
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
// 解析协议数据部分
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
// 进行body的解析
return decodeBody(channel, is, header);
} finally {
... ...
}
}
- decodeBody实现
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
// 序列化类型
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
// 获取requestId
long id = Bytes.bytes2long(header, 4);
// 响应处理
if ((flag & FLAG_REQUEST) == 0) {
// decode response.
Response res = new Response(id);
if ((flag & FLAG_EVENT) != 0) {
res.setEvent(true);
}
// 响应体的状态码
byte status = header[3];
res.setStatus(status);
// 使用和客户端一致的序列化方式进行解码
try {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
if (status == Response.OK) {
Object data;
// 心跳数据
if (res.isHeartbeat()) {
data = decodeHeartbeatData(channel, in);
} else if (res.isEvent()) {
// 事件
data = decodeEventData(channel, in);
} else {
// 响应信息
data = decodeResponseData(channel, in, getRequestData(id));
}
res.setResult(data);
} else {
res.setErrorMessage(in.readUTF());
}
} catch (Throwable t) {
res.setStatus(Response.CLIENT_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
return res;
} else {
// 请求的处理
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
req.setTwoWay((flag & FLAG_TWOWAY) != 0);
if ((flag & FLAG_EVENT) != 0) {
req.setEvent(true);
}
try {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
Object data;
if (req.isHeartbeat()) {
data = decodeHeartbeatData(channel, in);
} else if (req.isEvent()) {
data = decodeEventData(channel, in);
} else {
data = decodeRequestData(channel, in);
}
req.setData(data);
} catch (Throwable t) {
// bad request
req.setBroken(true);
req.setData(t);
}
return req;
}
}
四、小节
本文主要为大家介绍了下Dubbo网络协议数据格式,以及Netty编解码的实现,其中提到了Dubbo处理TCP半包问题是通过自定义header + body的方式解决。需要注意的是,当发现半包问题后,此时message的读取指针已经后移,因此需要把读指针重置。
说些题外话,Dubbo的相关设计,在我们日常的开发中,还是很高的参考意义。以笔者正在做的一个Netty应用为例,虽然网络协议最终选型未采取Dubbo的协议设计,不过对于技术方案的完整性,还是提供了不错的参考。包括我们的Netty服务的Hanlder的设计、心跳的处理、路由算法的实现,其实都从Dubbo框架中获取到了很多灵感,正所谓万变不离其宗,吃透一个成熟的开源架构设计,对我们日常的工作还是有很大的帮助的。