使用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;
}
}