Microsoft/thrifty vs facebook/swift: TTransportException:Buffer doesn't

2019-05-25 21:46:58 浏览数 (1)

版权声明:本文为博主原创文章,转载请注明源地址。 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

代码语言:javascript复制
    @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位为消息类型):

代码语言:javascript复制
	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.TBinaryProtocolwriteMessageBegin方法实现:

当成员变量strictWrite_true时,协议报文首先写入一个32位整数(高16位为版本号,低8位为消息类型),与readMessageBegin方法要求的顺序一致。

成员变量strictWrite_false时,最先写入的是消息名(字符串),这种情况下,接收端收到报文解析肯定会抛出异常的。

代码语言:javascript复制
	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来控制报文头的格式。

代码语言:javascript复制
    @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.BinaryProtocolstrictWrite恒为false,而且没有提供外部修改其值的方法,而org.apache.thrift.protocol.TBinaryProtocolstrictWrite_的值则可以由构造方法传入。用于创建org.apache.thrift.protocol.TBinaryProtocol实例的工厂类org.apache.thrift.protocol.TBinaryProtocol.Factory的默认构造方法则将strictWrite_置为true

下面是org.apache.thrift.protocol.TBinaryProtocol.Factory类的实现代码:

代码语言:javascript复制
	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_trueTBinaryProtocol实例。

下面是com.facebook.swift.service.ThriftServer类中DEFAULT_PROTOCOL_FACTORIES常量的定义。

代码语言:javascript复制
    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)机制强制修改成员变量。实现如下:

代码语言:javascript复制
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。再次运行测试程序,则问题解决。

0 人点赞