基于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。再次运行测试程序,则问题解决。

探讨Swift和Microsoft Thrifty框架下Thrift RPC系统的兼容性问题,因默认报文格式差异导致服务端异常,通过调整客户端报文格式设置解决。
267

被折叠的 条评论
为什么被折叠?



