这一篇我们主要来讲Dubbo
在网络中如何进行通信的。由于请求都是基于TCP
的,那么Dubbo
中是如何处理粘包和拆包的问题。
dubbo
协议采用固定长度的消息头(16字节)和不定长度的消息体来进行数据传输,消息头定义了底层
框架(netty
)在IO
线程处理时需要的信息,协议的报文格式如下:
1、协议详情
Magic - Magic High & Magic Low (16 bits)
标识协议版本号,Dubbo 协议:0xdabb
Serialization ID (5 bit)
标识序列化类型:比如 fastjson 的值为6。
Event (1 bit)
标识 ,例如,心跳事件。如果这是一个事件,则设置为1。
2 Way (1 bit)
仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器 的返回值,则设置为1。
Req/Res (1 bit)
标识是请求或响应。请求: 1; 响应: 0。
Status (8 bits)
仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
20 - OK
30 - CLIENT_TIMEOUT
31 - SERVER_TIMEOUT
40 - BAD_REQUEST
50 - BAD_RESPONSE
60 - SERVICE_NOT_FOUND
70 - SERVICE_ERROR
80 - SERVER_ERROR
90 - CLIENT_ERROR
100 - SERVER_THREADPOOL_EXHAUSTED_ERROR
Request ID (64 bits)
标识唯一请求。类型为long。
Data Length (32 bits)
序列化后的内容长度(可变部分),按字节计数。int类型。
Variable Part
被特定的序列化类型(由序列化 ID 标识)序列化后,每个部分都是一个 byte [] 或者 byte
如果是请求包 ( Req/Res = 1),则每个部分依次为:
Dubbo version
Service name
Service version
Method name
Method parameter types Method arguments Attachments
如果是响应包(Req/Res = 0),则每个部分依次为:
返回值类型(byte),标识从服务器端返回的值类型:
返回空值:RESPONSE_NULL_VALUE 2
正常响应值: RESPONSE_VALUE 1
异常:RESPONSE_WITH_EXCEPTION 0
返回值:从服务端返回的响应bytes
注意:对于(Variable Part)变长部分,当前版本的Dubbo 框架使用json序列化时,在每部分内容间
额外增加了换行符作为分隔,请在Variable Part的每个part后额外增加换行符, 如:
Dubbo version bytes (换行符)
Service name bytes (换行符)
...
2、优点
3、可以改进的点
com.alibaba.middleware.hsf.guide.api.param.ModifyOrderPriceParam
,转换为Lcom/alibaba/middleware/hsf/guide/api/param/ModifyOrderPriceParam;
,理论上是不必这里我们来看 ExchangeCodec
类,这个也是Dubbo
在进行数据传输中的数据协议类。
1、我们先来看看他的常量定义。
// 消息头的长度
protected static final int HEADER_LENGTH = 16;
// 标示为0-15位
protected static final short MAGIC = -9541;
protected static final byte MAGIC_HIGH = Bytes.short2bytes((short)-9541)[0];
protected static final byte MAGIC_LOW = Bytes.short2bytes((short)-9541)[1];
// 消息头中的内容
protected static final byte FLAG_REQUEST = -128;
protected static final byte FLAG_TWOWAY = 64;
protected static final byte FLAG_EVENT = 32;
protected static final int SERIALIZATION_MASK = 31;
2、这个类中 encode
和 decode
分别用于将数据发送到 ByteBuffer
中,还有就是将其反向的转换为对象。encode
中的Request
就是我们之前所讲的Request
对象。
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
// 处理请求对象
if (msg instanceof Request) {
this.encodeRequest(channel, buffer, (Request)msg);
} else if (msg instanceof Response) {
// 处理响应
this.encodeResponse(channel, buffer, (Response)msg);
} else {
// 其他的交给上级处理,用于telnet模式
super.encode(channel, buffer, msg);
}
}
3、查看 encodeRequest
方法。这里也验证了我们之前所讲的header
内容。
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
// 请求的序列化类型
Serialization serialization = this.getSerialization(channel);
// 写入header信息
byte[] header = new byte[16];
// 模数0-15位
Bytes.short2bytes((short)-9541, header);
// 标记为请求
header[2] = (byte)(-128 | serialization.getContentTypeId());
// 是否是单向还是双向的(异步)
if (req.isTwoWay()) {
header[2] = (byte)(header[2] | 64);
}
// 是否为事件(心跳)
if (req.isEvent()) {
header[2] = (byte)(header[2] | 32);
}
// 写入当前的请求ID
Bytes.long2bytes(req.getId(), header, 4);
// 保存当前写入的位置,将其写入的位置往后面偏移,保留出写入内容大小的位置,先进行写入body 内容
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + 16);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
// 按照数据内容的不同,来写入不同的内容
if (req.isEvent()) {
this.encodeEventData(channel, out, req.getData());
} else {
this.encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable)out).cleanup();
}
bos.flush();
bos.close();
// 记录body中写入的长度
int len = bos.writtenBytes();
checkPayload(channel, (long)len);
// 将其写入到header中的位置中
Bytes.int2bytes(len, header, 12);
// 发送到buffer中
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header);
buffer.writerIndex(savedWriteIndex + 16 + len);
}
4、真正的 encodeRequestData
在子类 DubboCodec
中
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation)data;
// 写入版本
out.writeUTF(version);
// 接口全名称
out.writeUTF(inv.getAttachment("path"));
// 接口版本号
out.writeUTF(inv.getAttachment("version"));
// 写入方法名称
out.writeUTF(inv.getMethodName());
// 调用参数描述信息
out.writeUTF(inv.getParameterTypesDesc());
// 所有的请求参数写入
Object[] args = inv.getArguments();
if (args != null) {
for(int i = 0; i < args.length; ++i) {
out.writeObject(CallbackServiceCodec.encodeInvocationArgument(channel, inv, i));
}
}
// 写入所有的附加信息
out.writeAttachments(inv.getObjectAttachments());
}
5、下面我们再来看看 encodeResponse
方法实现。一样的,这里可以看到和写入request
相似。
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
int savedWriteIndex = buffer.writerIndex();
try {
Serialization serialization = this.getSerialization(channel);
// 和之前的参数一致
byte[] header = new byte[16];
Bytes.short2bytes((short)-9541, header);
header[2] = serialization.getContentTypeId();
if (res.isHeartbeat()) {
header[2] = (byte)(header[2] | 32);
}
// 写入状态码
byte status = res.getStatus();
header[3] = status;
// 写入内容
Bytes.long2bytes(res.getId(), header, 4);
// 和Request一样的内容写入方式,先写入内容,再写入长度
buffer.writerIndex(savedWriteIndex + 16);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (status == 20) {
if (res.isHeartbeat()) {
this.encodeEventData(channel, out, res.getResult());
} else {
this.encodeResponseData(channel, out, res.getResult(), res.getVersion());
}
} else {
// 这里不太一样的地方在于,如果错误的时候,则直接将错误信息写入,不需要再交由序列化
out.writeUTF(res.getErrorMessage());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable)out).cleanup();
}
bos.flush();
bos.close();
// 一样的写入模式
int len = bos.writtenBytes();
checkPayload(channel, (long)len);
Bytes.int2bytes(len, header, 12);
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + 16 + len);
} catch (Throwable var13) {
Throwable t = var13;
// 写入出现异常
buffer.writerIndex(savedWriteIndex);
// send error message to Consumer, otherwise, Consumer will wait till timeout.
if (!res.isEvent() && res.getStatus() != 50) {
Response r = new Response(res.getId(), res.getVersion());
r.setStatus((byte)50);
// 如果是超过内容长度则重新设置内容大小并写入
if (var13 instanceof ExceedPayloadLimitException) {
logger.warn(var13.getMessage(), var13);
try {
r.setErrorMessage(t.getMessage());
channel.send(r);
return;
} catch (RemotingException var12) {
logger.warn("Failed to send bad_response info back: " + var13.getMessage() + ", cause: " + var12.getMessage(), var12);
}
} else {
logger.warn("Fail to encode response: " + res + ", send bad_response info instead, cause: " + var13.getMessage(), var13);
try {
r.setErrorMessage("Failed to send response: " + res + ", cause: " + StringUtils.toString(t));
channel.send(r);
return;
} catch (RemotingException var11) {
logger.warn("Failed to send bad_response info back: " + res + ", cause: " + var11.getMessage(), var11);
}
}
}
if (var13 instanceof IOException) {
throw (IOException)var13;
} else if (var13 instanceof RuntimeException) {
throw (RuntimeException)var13;
} else if (var13 instanceof Error) {
throw (Error)var13;
} else {
throw new RuntimeException(var13.getMessage(), var13);
}
}
}
6、在encode
中我们再来看看真正encode
的内容。 encodeResponseData
同样位于 DubboCodec
中。
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
Result result = (Result)data;
// 是否支持返回attachment参数
boolean attach = Version.isSupportResponseAttachment(version);
Throwable th = result.getException();
if (th == null) {
// 如果没有异常信息,则直接写入内容
Object ret = result.getValue();
if (ret == null) {
out.writeByte((byte)(attach ? 5 : 2));
} else {
out.writeByte((byte)(attach ? 4 : 1));
out.writeObject(ret);
}
} else {
// 否则的话则将异常信息序列化
out.writeByte((byte)(attach ? 3 : 0));
out.writeThrowable(th);
}
// 支持写入attachment,则写入
if (attach) {
// returns current version of Response to consumer side.
result.getObjectAttachments().put("dubbo", Version.getProtocolVersion());
out.writeAttachments(result.getObjectAttachments());
}
}
7、解码 decode
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
// 可读字节数
int readable = buffer.readableBytes();
// 选取可读字节数 和 HEADER_LENGTH 中小的
byte[] header = new byte[Math.min(readable, 16)];
buffer.readBytes(header);
return this.decode(channel, buffer, readable, header);
}
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
int len;
int i;
// 检查魔数
if ((readable <= 0 || header[0] == MAGIC_HIGH) && (readable <= 1 || header[1] == MAGIC_LOW)) {
// check length. 不完整的包 需要继续读取
if (readable < 16) {
return DecodeResult.NEED_MORE_INPUT;
} else {
// 获取数据长度
len = Bytes.bytes2int(header, 12);
checkPayload(channel, (long)len);
i = len + 16;
// 需要继续读取
if (readable < i) {
return DecodeResult.NEED_MORE_INPUT;
} else {
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
Object var8;
try {
// 解码数据
var8 = this.decodeBody(channel, is, header);
} finally {
if (is.available() > 0) {
try {
if (logger.isWarnEnabled()) {
logger.warn("Skip input stream " + is.available());
}
StreamUtils.skipUnusedStream(is);
} catch (IOException var15) {
logger.warn(var15.getMessage(), var15);
}
}
}
return var8;
}
}
} else {
len = header.length;
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, len, readable - len);
}
for(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);
}
}
8、这时候我们再来看看解析响应中的信息处理。
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2];
byte proto = (byte)(flag & 31);
// 获取请求ID
long id = Bytes.bytes2long(header, 4);
// 判断是请求还是响应
if ((flag & -128) == 0) {
// 说明是响应
Response res = new Response(id);
// 是否是event事件
if ((flag & 32) != 0) {
res.setEvent(true);
}
// 获取请求的状态码
byte status = header[3];
res.setStatus(status);
try {
// 进行数据内容解析
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
if (status == 20) {
Object data;
// 根据不同的类型来进行解析
if (res.isHeartbeat()) {
data = this.decodeHeartbeatData(channel, in);
} else if (res.isEvent()) {
data = this.decodeEventData(channel, in);
} else {
data = this.decodeResponseData(channel, in, this.getRequestData(id));
}
res.setResult(data);
} else {
res.setErrorMessage(in.readUTF());
}
} catch (Throwable var12) {
res.setStatus((byte)90);
res.setErrorMessage(StringUtils.toString(var12));
}
return res;
} else {
// 解析为请求
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
req.setTwoWay((flag & 64) != 0);
if ((flag & 32) != 0) {
req.setEvent(true);
}
try {
// 与响应相同,进行内容解析
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
Object data;
if (req.isHeartbeat()) {
data = this.decodeHeartbeatData(channel, in);
} else if (req.isEvent()) {
data = this.decodeEventData(channel, in);
} else {
data = this.decodeRequestData(channel, in);
}
req.setData(data);
} catch (Throwable var13) {
// bad request
req.setBroken(true);
req.setData(var13);
}
return req;
}
}
当发生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尾部,那么在返回到InternalDecoder中message的方法readable返回 的是false,那么就会对buffer重新赋予EMPTY_BUFFER实体,而针对包含一个以上的dubbo协议栈,当然 也会解析出其中一个dubbo协议栈,但是经过ExchangeCodec解析之后,message的readIndex不在 message尾部,所以message的readable方法返回的是true。那么则会继续遍历message,读取下面的 信息。最终要么message刚好整数倍包含完整的dubbo协议栈,要不ExchangeCodec返回 NEED_MORE_INPUT,最后将未读完的数据缓存到buffer中,等待下次inbound事件,将buffer中的消息合 并到下次的inbound消息中,种类又回到了拆包的问题上。
dubbo在处理tcp的粘包和拆包时是借助InternalDecoder的buffer缓存对象来缓存不完整的dubbo协议 栈数据,等待下次inbound事件,合并进去。所以说在dubbo中解决TCP拆包和粘包的时候是通过buffer 变量来解决的。