版权声明:本文为博主原创文章,转载请注明源地址。 https://cloud.tencent.com/developer/article/1433550
基于thrift的RPC系统中,如果service端是基于facebook的swift开源框架实现的,而client是基于Microsoft的thrifty开源框架实现的,那么在client向service端发送请求时,service端就可能会抛出本文标题所说的异常。
结论
经过层层溯源,找到问题的原因:swift和thrift的在底层的默认通讯协议都是使用相同的二进制数据格式,也是100%支持thrift框架的,但它们默认的报文格式却不一样,swift的实现二进制协议的org.apache.thrift.protocol.TBinaryProtocol
类默认将消息名(RPC调用方法名,字符串类型)为报文的首字段,
而thrifty的实现二进制协议的com.microsoft.thrifty.protocol.BinaryProtocol
类默认将通讯协议版本号(version,32位整数)为报文的首字段。
因为协议报文的格式不同,导致服务端收到client端数据后因为解析错误而抛出异常。
这么说还是太抽象,那么我们就一层层分析原因。
问题溯源
报文接收
下面是抛出异常的的代码位置com.facebook.nifty.core.TNiftyTransport
,
@Override
public int readAll(byte[] bytes, int offset, int length) throws TTransportException {
if (read(bytes, offset, length) < length) {
throw new TTransportException("Buffer doesn't have enough bytes to read");
}
return length;
}
com.facebook.nifty.core.TNiftyTransport.readAll
方法是在被org.apache.thrift.protocol.TBinaryProtocol.readMessageBegin
方法调用时抛出异常的。下面是readMessageBegin
方法的实现代码,可以看出,swift在解析报文协议时,首先就是读取32位整数来判断协议版本号(高16位为版本号,低8位为消息类型):
public TMessage readMessageBegin() throws TException {
int size = this.readI32();
if (size < 0) {
int version = size & -65536;
if (version != -2147418112) {
throw new TProtocolException(4, "Bad version in readMessageBegin");
}
return new TMessage(this.readString(), (byte) (size & 255), this.readI32());
}
if (this.strictRead_) {
throw new TProtocolException(4, "Missing version in readMessageBegin, old client?");
}
return new TMessage(this.readStringBody(size), this.readByte(), this.readI32());
}
报文发送
再来看看org.apache.thrift.protocol.TBinaryProtocol
的writeMessageBegin
方法实现:
当成员变量strictWrite_
为true
时,协议报文首先写入一个32位整数(高16位为版本号,低8位为消息类型),与readMessageBegin
方法要求的顺序一致。
成员变量strictWrite_
为false
时,最先写入的是消息名(字符串),这种情况下,接收端收到报文解析肯定会抛出异常的。
public void writeMessageBegin(TMessage message) throws TException {
if (this.strictWrite_) {
int version = -2147418112 | message.type;
this.writeI32(version);
this.writeString(message.name);
this.writeI32(message.seqid);
} else {
this.writeString(message.name);
this.writeByte(message.type);
this.writeI32(message.seqid);
}
}
再来看com.microsoft.thrifty.protocol.BinaryProtocol
的writeMessageBegin方法实现,与swift的实现逻辑是一样的,也有一个成员变量strictWrite
来控制报文头的格式。
@Override
public void writeMessageBegin(String name, byte typeId, int seqId) throws IOException {
if (strictWrite) {
int version = VERSION_1 | (typeId & 0xFF);
writeI32(version);
writeString(name);
writeI32(seqId);
} else {
writeString(name);
writeByte(typeId);
writeI32(seqId);
}
}
报文格式控制
从这里看,既然swift和thrifty对报文格式的控制逻辑是一样的,那么问题就出在这个控制报文头的格式的成员变量strictWrite
上了。
进一步的分析,可以发现com.microsoft.thrifty.protocol.BinaryProtocol
的strictWrite
恒为false
,而且没有提供外部修改其值的方法,而org.apache.thrift.protocol.TBinaryProtocol
的strictWrite_
的值则可以由构造方法传入。用于创建org.apache.thrift.protocol.TBinaryProtocol
实例的工厂类org.apache.thrift.protocol.TBinaryProtocol.Factory
的默认构造方法则将strictWrite_
置为true
下面是org.apache.thrift.protocol.TBinaryProtocol.Factory
类的实现代码:
public static class Factory implements TProtocolFactory {
protected boolean strictRead_ = false;
protected boolean strictWrite_ = true;
public Factory() {
this(false, true);
}
public Factory(boolean strictRead, boolean strictWrite) {
this.strictRead_ = strictRead;
this.strictWrite_ = strictWrite;
}
public TProtocol getProtocol(TTransport trans) {
return new TBinaryProtocol(trans, this.strictRead_, this.strictWrite_);
}
}
而在swift的服务端实现代码com.facebook.swift.service.ThriftServer
中对binary协议使用的正是strictWrite_
为true
的TBinaryProtocol
实例。
下面是com.facebook.swift.service.ThriftServer
类中DEFAULT_PROTOCOL_FACTORIES
常量的定义。
public static final ImmutableMap<String,TDuplexProtocolFactory> DEFAULT_PROTOCOL_FACTORIES = ImmutableMap.of(
"binary", TDuplexProtocolFactory.fromSingleFactory(new TBinaryProtocol.Factory()),
"compact", TDuplexProtocolFactory.fromSingleFactory(new TCompactProtocol.Factory())
);
好了,到这里真相大白了,就是swift的service端和thrifty的client端的binary协议默认报文格式不一致。
解决方法
service端和client端修改一端就可以了。在我的项目中,因为基于swift的service端和client端先完成,为了要支持android平台才基于thrifty设计了新的android client端。所以为了保持系统兼容性,我只能修改android client端。
但是com.microsoft.thrifty.protocol.BinaryProtocol
没有为修改私有成员变量strictWrite
提供方法,所以我只能使用java反射(reflection)机制强制修改成员变量。实现如下:
SocketTransport transport =
new SocketTransport.Builder(hostAndPort.getHost(),hostAndPort.getPort())
.connectTimeout((int) connectTimeout)
.readTimeout((int) readTimeout).build();
transport.connect();
Protocol protocol = new BinaryProtocol(transport);
/* force set private field 'strictWrite' to true */
Field field = BinaryProtocol.class.getDeclaredField("strictWrite");
field.setAccessible(true);
field.set(protocol, true);
即在创建com.microsoft.thrifty.protocol.BinaryProtocol
实例后立即将strictWrite
字段值为true
。再次运行测试程序,则问题解决。