使用Thrift传输二进制数据遇到的问题

最近使用 Thrift 传输图片数据,一开始使用 string 保存图片数据,创建的 service 如下:

enum RetCode
{
    F_Success = 0,
    F_NotFound,
    F_Failed,
    F_LastStatus
}

struct Response
{
    1:RetCode ret_code,
    2:string err_msg
}

service FileStorage
{
    Response WriteFile(1:string file_name, 2:string schema, 3:string write_buffer, 4:i32 length)
}

这里需要介绍以下几个问题:

一、 第一个问题:为什么可以通过 string 传输二进制数据?如果二进制数据中有 '\0' 字节的话,string 不会被截断吗?

1)先看 Thrift 生成的接口:

class FileStorageIf
{
    public:
        virtual ~FileStorageIf() {}
        virtual void WriteFile(Response& _return, const std::string& file_name, const std::string& schema, const std::string& write_buffer, const int32_t length) = 0;
};

2)Thrift客户端是将数据写到 socket 的:

客户端通过 FileStorage_WriteFile_args::write 将各个参数传给T BufferedTransport;

最后调用下面的函数将数据写到 socket:oprot_->getTransport()->flush();

void FileStorageClient::send_WriteFile(const std::string& file_name, const std::string& schema, const std::string& write_buffer, const int32_t length)
{
  int32_t cseqid = 0;
  oprot_->writeMessageBegin("WriteFile", ::apache::thrift::protocol::T_CALL, cseqid);

  FileStorage_WriteFile_pargs args;
  args.file_name = &file_name;
  args.schema = &schema;
  args.write_buffer = &write_buffer;
  args.length = &length;
  args.write(oprot_);

  oprot_->writeMessageEnd();
  oprot_->getTransport()->writeEnd();
  oprot_->getTransport()->flush();
}

uint32_t FileStorage_WriteFile_args::write(::apache::thrift::protocol::TProtocol* oprot) const {
  uint32_t xfer = 0;
  ……
  xfer += oprot->writeFieldBegin("write_buffer", ::apache::thrift::protocol::T_STRING, 3);
  xfer += oprot->writeString(this->write_buffer);
  xfer += oprot->writeFieldEnd();
  ……
  return xfer;
}

下面看 writeString 方法的实现:

首先通过 str.size() 得到 string 的长度,先将长度写入 TBufferedTransport 的 buffer;

然后将 str.data() 写入 buffer;

template 
uint32_t TBinaryProtocolT::writeString(const std::string& str) {
  uint32_t size = str.size();
  uint32_t result = writeI32((int32_t)size);
  if (size > 0) {
    this->trans_->write((uint8_t*)str.data(), size);
  }
  return result + size;
}

3)str.size() 和 str.data() 可以得到这个二进制字符串的长度和完整数据吗?

问题:如果二进制数据中有 '\0' 字节的话,string 不会被截断吗?

答案是 string 不会截断字符串,而 char* 会截断字符串。下面是测试程序:

#include <stdio.h>
#include <string>
#include <iostream>

using namespace std;

int main(int __argc, char* __argv[]) {
  char char_buf[] = {'a', 'b', 'c', 'd', '\0', 'e'};
  string str1 = char_buf;
  string str2;
  str2.assign(char_buf);
  string str3;
  str3.assign(char_buf, sizeof(char_buf));
  cout << "str1: " << str1.size() << endl;
  cout << "str2: " << str2.size() << endl;
  cout << "str3: " << str3.size() << endl;
  return 0;
}

运行结果如下:

guojun8@guojun8-desktop:~/test/string$ g++ -o test main.c
guojun8@guojun8-desktop:~/test/string$ ./test
str1: 4
str2: 4
str3: 6

string 类型中存放有数据的长度和数据(数据时 buffer 而不是字符串),当通过下面的方法设置数据时,会根据传递的长度复制数据并设置长度;

string& assign (const char* s, size_t n);

二、 当客户端使用java调掉并传入String时,会出现数据格式错误的问题。

我昨天遇到这个问题,用 java 调用这个接口并写入图片数据时,在服务端收到数据并检查是总是报数据不合法,不是图片数据,但是用 Python 和 PHP 写入数据都没有这个问题。我一起没有学过 java,但为了弄清这个问题还是看了一下,下面分析一下这个问题:

1) Thrift 文件生成的 Java 客户端代码:

使用 Thrift 生成的 Java 接口代码如下:

public class FileStorage {
    public interface Iface {
        public Response WriteFile(String file_name, String schema, String write_buffer, int length)
            throws org.apache.thrift.TException;
    }
}

2) 客户端的代码:

InputStream in = new FileInputStream("C:/temp/photo4l.jpg"); 
data = new byte[in.available()]; 
in.read(data); 
in.close(); 
String str = new String(data);
Response result = client.WriteFile(file_name, schema, str, in.available());

客户端的代码实现是从文件中读取图片数据并将数据转为 String,并传输到服务端。但是服务端收到数据解析永远是 bad_image_format。

3)问题分析

下面看 String 的构造函数说明:

/**
 * Constructs a new {@code String} by decoding the specified array of bytes
 * using the platform's default charset.  The length of the new {@code
 * String} is a function of the charset, and hence may not be equal to the
 * length of the byte array.
 *
 *  The behavior of this constructor when the given bytes are not valid
 * in the default charset is unspecified.  The {@link
 * java.nio.charset.CharsetDecoder} class should be used when more control
 * over the decoding process is required.
 *
 * @param  bytes
 *         The bytes to be decoded into characters
 *
 * @since  JDK1.1
 */
public String(byte bytes[]) {
    this(bytes, 0, bytes.length);
}

String 的构造函数用当前的默认字符编码解码字节数组,当字节数组不合法时没有处理。而图片数据的字节数组不可能满足任何一种字符集的解码操作,所以这里生成的 String 未定义,全是乱码。

另外 Java 中 TBinaryProtocol::writeString 的实现:

public void writeString(String str) throws TException {
    try {
        byte[] dat = str.getBytes("UTF-8");
        writeI32(dat.length);
        trans_.write(dat, 0, dat.length);
    } catch (UnsupportedEncodingException uex) {
        throw new TException("JVM DOES NOT SUPPORT UTF-8");
    }
}

str.getBytes("UTF-8"); 将String编码成 UTF-8 格式的 byte 数组,并写到 buffer 中,如果 String 的数据是乱码,编码生成的 data 也是乱码,所以传到服务端的数据也是乱码。

4)问题解决

使用 Python,PHP 和 C++ 调用这个接口时都不会出现这个问题,因为 string 类型默认不会对数据做编解码,保存的是原始的字节数组格式。

Thrift 中提供了 binary 类型用于解决这个问题,将 thrift 文件中的 write_buffer 改成 binary 类型,如下:

service FileStorage {
    Response WriteFile(1:string file_name, 2:string schema, 3:binary write_buffer, 4:i32 length)
}

生成的 Java 接口中 write_buffer 是 ByteBuffer:

public class FileStorage {
    public interface Iface {
        public Response WriteFile(String file_name, String schema, ByteBuffer write_buffer, int length)
            throws org.apache.thrift.TException;
    }
}
如果觉得这对你有用,请随意赞赏,给与作者支持
评论 0
最新评论