Category Archives: Linux

about linux

关于thrift的一些探索——thrift序列化技术

作者: dplord, 访问量 1149





thrift的IDL,相当于一个钥匙。而thrift传输过程,相当于从两个房间之间的传输数据。

QQ20160426-3@2x

 

图-1

(因为Thrift采用了C/S模型,不支持双向通信:client只能远程调用server端的RPC接口,但client端则没有RPC供server端调用,这意味着,client端能够主动与server端通信,但server端不能主动与client端通信而只能被动地对client端的请求作出应答。所以把上图-1中的箭头,画为单向箭头更为直观)基于上图,Thrift的IDL文件的作用可以描述为,从房间A要传递一批数据给房间B,把数据装在箱子里,锁上锁,然后通过Transport Channel把带锁的箱子给房间B,而Thrift IDL就是一套锁跟钥匙。房间A跟房间B都有同样的一个thrift IDL文件,有了这个文件就可以生成序列化跟反序列化的代码,就像锁跟钥匙一样。而一旦没有了Thrift IDL文件,房间A无法将数据序列化好锁紧箱子,房间B没有钥匙打开用箱子锁好传输过来的数据。因此,IDL文件的作用就在此。

一、为什么要用Thrift序列化, 不用纯文本的协议

我能想到的用thrift提供的thrift binary protocol而不用json、xml等纯文本序列化协议的几个好处如下:

  • 序列化后的体积小, 省流量
  • 序列化、反序列化的速度更快,提高性能
  • 兼容性好,一些接口,涉及多种语言的int32、int64等等跟语言、机器、平台有关, 用纯文本可能有兼容性的问题
  • 结合thrift transport技术,可以RPC精准地传递对象,而纯文本协议大多只能传递数据,而不能完完全全传递对象

关于以上几点我个人认为的好处,下面作一下简单解答

1.1 序列化后的体积小, 省流量

给出测试的IDL Services_A.thrift内容如下,定义了一些数据格式,这个结构体数据复杂度一般,有list、也有list对象、也有一些基本的struct等等。

namespace php Services.test.A
namespace java Services.tets.A

struct student{
    1:required string studentName, #学生姓名
    2:required string sex,         #性别
    3:required i64 age,            #学生年龄
}
struct banji{
    1:required string banjiName, #班级名称
    2:required list<student> allStudents, #所有学生
}
struct school {
    1:required string schoolName,
    2:required i64    age,
    3:required list<string> zhuanye, #所有专业
    4:required list<banji> allBanji, #所有班级
}

分别把Services_A.thrift 序列化为json、跟thrift.bin文件,序列化到文件中。对比文件大小。

这里用php写了一个例子, 代码如下:

<?php
/**
 * Created by PhpStorm.
 * User: dengpan
 * Date: 16/4/21
 * Time: 12:39
 */

ini_set('memory_limit','10240M');

require_once __DIR__ . "/Thrift/ClassLoader/ThriftClassLoader.php";
use Thrift\ClassLoader\ThriftClassLoader;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\TFramedTransport;
use Thrift\Transport\TPhpStream;
use Thrift\Transport\TPhpStreamMy;
use Thrift\Transport\TBufferedTransport;

$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__);
$loader->registerNamespace('Services', __DIR__);
$loader->registerDefinition('Services',  __DIR__);
$loader->register();

require "Types.php";


$school = [];
$school['schoolName'] = "hahhaha";
$school['age'] = 60;
$school['zhuanye'] = [
    '专业1',
    '专业2',
    '专业3',
];


$nameArr = ["张三", "李四", "王五", "王菲", "张韶涵", "王祖贤", "范冰冰", "新垣结衣", "詹姆斯", "诺维茨基"];
$sexArr = ["男", "女"];

$allBanji = [];
for($i = 0; $i < 1000; $i ++)
{
    $banji = [];
    $banji['banjiName'] = "计算机" + $i + "班";
    for($j = 0; $j < 1000; $j ++) {
        $banji['allStudents'][] = new student(
            [
            'studentName' => $nameArr[rand(0, count($nameArr) - 1)],
            'sex' => $sexArr[rand(0, count($sexArr) - 1)],
            'age' => rand(0, 6) + 18,
            ]
        );
    }
    $allBanji[] = new banji($banji);
}


$school['allBanji'] = $allBanji;
$sc = new school($school);


$str = json_encode($school);
file_put_contents("/tmp/json_1", $str);

$transport = new TBufferedTransport(new TPhpStreamMy(null, '/tmp/thrift_1.bin'), 1024, 1024);
$sc->write(new TBinaryProtocol($transport, true, true));

最终的结果, json文件跟thrift binary文件大小如下图:

QQ20160512-0@2x

thrift binary file是json文件大小的 0.63。为什么文件小这么多,我认为有以下几个原因,① json是文本的,而thrift是二进制的,文本文件跟二进制文件的区别,可以参考我的另一篇文章, 二进制文件跟普通文本文件的区别。② thrift序列化过程中,由于IDL在client、servo端都有一套,所以没有传输一些字段比如field name等等,只需要传递field id, 这样也是一些节省字节的方法。另外,提醒一下,序列化跟压缩不是一回事,比如上面的文件压缩为xz看看大小如下图:

QQ20160512-5@2x

压缩后体积变为原来的3.3%、4.4%。可以先序列化再压缩传输,压缩的compress-level要指定合理,不然压缩跟解压缩占据系统资源而且耗时大。

1.2序列化、反序列化的速度更快,提高性能

下面给出一个序列化反序列化测试

分别用① java Thrift binary protocol跟java fastjson库去序列以上对应格式为json, ②用C++ Thrift binary protocol与c++ jsoncpp序列化,对比速度,

java 测试代码如下:

public class ThriftDataWrite3 {
    private static final Random sRandom = new Random();
    public static void main(String[] args) throws IOException, TException{

        //构造school对象
        String[] nameArr = {"张三", "李四", "王五", "赵6", "王祖贤", "赵敏", "漩涡鸣人", "诺维茨基", "邓肯", "克莱尔丹尼斯", "长门", "弥彦", "威少"};
        int nameArrLength = nameArr.length;
        
        school sc = new school();
        sc.setSchoolName("哈哈哈哈哈哈");
        sc.setAge(12);
        List<String> l = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            l.add("专业" + i);
        }
        sc.setZhuanye(l);


        List<banji> allBanji = new ArrayList<banji>();
        for (int i = 0; i < 1000; i++) {
            banji bj = new banji();
            bj.setBanjiName("班级" + i);
            List allStuents = new ArrayList<student>();
            for (int j = 0; j < 1000; j ++) {
                allStuents.add(
                        new student(
                                nameArr[sRandom.nextInt(nameArrLength)],
                                ((sRandom.nextInt(2) == 0) ? "男" : "女"),
                                (sRandom.nextInt(10) + 18)
                        )
                );
            }
            bj.setAllStudents(allStuents);
            allBanji.add(bj);
        }
        sc.setAllBanji(allBanji);


        //①序列化为thrift binary protocol
        final long startTime = System.currentTimeMillis();
        TSerializer serializer = new TSerializer(new TBinaryProtocol.Factory());
        for (int i = 0; i < 200; i++) {
            byte[] bytes = serializer.serialize(sc);
            //serializer.toString(sc);
        }
        final long endTime = System.currentTimeMillis();
        System.out.println("thrift序列化200次时间为" + (endTime - startTime) + "ms");


        //②序列化为json
        final long endTime7 = System.currentTimeMillis();
        for (int i = 0; i < 200; i++) {
            JSON.toJSONString(sc);
        }
        final long endTime2 = System.currentTimeMillis();
        System.out.println("json序列化200次时间为" + (endTime2 - endTime7) + "ms");


        //准备待序列化的数据
        byte[] bytes = serializer.serialize(sc);
        String jsonStr = JSON.toJSONString(sc);



        //③反序列thrift binary data
        final long endTime3 = System.currentTimeMillis();
        TDeserializer tDeserializer = new TDeserializer();
        for (int i = 0; i < 200; i++) {
            school sc1 = new school();
            tDeserializer.deserialize(sc1, bytes);
//            System.out.println(sc1.toString());
        }
        final long endTime4 = System.currentTimeMillis();
        System.out.println("thrift反序列化200次时间为" + (endTime4 - endTime3) + "ms");



        //④反序列化json
        final long endTime5 = System.currentTimeMillis();
        for (int i = 0; i < 200; i++) {
            JSON.parseObject(jsonStr, sc.getClass());
//            System.out.println(JSON.parseObject(jsonStr, sc.getClass()).toString());
        }
        final long endTime6 = System.currentTimeMillis();
        System.out.println("json反序列化200次时间为" + (endTime6 - endTime5) + "ms");



    }
}

java的序列化thrift、json,反序列化thrift跟json的结果为:

thrift序列化200次时间为82482ms
json序列化200次时间为167954ms
thrift反序列化200次时间为42919ms
json反序列化200次时间为207896ms

其中可以看出java中thrift的序列化速度是fastjson 序列化json的2倍多,thrift反序列化的速度是fastjson反序列化json的接近5倍。

再看C++测试同样的Case, C++代码如下:

①  头文件thriftSerializeTest.h

//
// Created by 邓攀邓攀 on 16/5/11.
//

#ifndef UNTITLED_THRIFTSERIALIZETEST_H
#define UNTITLED_THRIFTSERIALIZETEST_H

//my own add header start
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <sstream>
#include "service_a_types.h"
//my own add header end

using namespace std;


class thriftSerializeTest {
public:
    static school initializeSchool();
    static void jsonSerialize(school sc, string* str, int* time);
    static void thriftBinarySerialize(school sc,string* str, int* time);
    static void jsonDeserialize(string str, int* time);
    static void thriftBinaryDeserialize(string str, int* time);
};


#endif //UNTITLED_THRIFTSERIALIZETEST_H

② cpp 文件

//
// Created by 邓攀邓攀 on 16/5/11.
//

#include "thriftSerializeTest.h"
#include "service_a_types.h"
#include <sstream>
#include <sys/time.h>
#include "thrift/transport/TBufferTransports.h"
#include "thrift/protocol/TBinaryProtocol.h"
#include "json/json.h"


#define MAX_NUM 200

using apache::thrift::protocol::TBinaryProtocol;
using apache::thrift::transport::TMemoryBuffer;


school thriftSerializeTest::initializeSchool()
{
    school sc;
    vector<string> nameArr{"张三", "李四", "王五", "赵6", "王祖贤", "赵敏", "漩涡鸣人", "诺维茨基", "邓肯", "克莱尔丹尼斯", "长门", "弥彦", "威少"};
    int nameArrLength = nameArr.size();
    sc.__set_age(12);
    sc.__set_schoolName("哈哈哈哈哈哈");
    vector<string> v;
    for (int i = 0; i < 100; ++i) {
        std::ostringstream oss;
        oss << i;
        v.push_back("专业" + oss.str());
    }
    sc.__set_zhuanye(v);

    vector<banji> allBanji;
    for (int j = 0; j < 1000; ++j) {
        banji bj;
        std::ostringstream oss;
        oss << j;
        bj.__set_banjiName("班级" + oss.str());
        vector<student> allStudents;
        for (int i = 0; i < 1000; ++i) {
            student student1;
            student1.__set_age(18 + (rand() % 10));
            student1.__set_sex((rand() % 2) ? "男" : "女");
            student1.__set_studentName(nameArr.at(rand() % nameArr.size()));
            allStudents.push_back(student1);
        }
        bj.__set_allStudents(allStudents);
        allBanji.push_back(bj);
    }
    sc.__set_allBanji(allBanji);

    return sc;
}


void thriftSerializeTest::jsonSerialize(school sc, string *str, int *time) {
    Json::Value root;
    root["studentName"] = Json::Value(sc.schoolName);
    root["age"] = Json::Value((int)sc.age);
    for(auto v : sc.zhuanye)
    {
        root["zhuanye"].append(v);
    }

    for(auto v: sc.allBanji)
    {
        Json::Value banji;
        banji["banjiName"] = Json::Value(v.banjiName);
        for(auto k : v.allStudents)
        {
            Json::Value student;
            student["studentName"] = Json::Value(k.studentName);
            student["age"] = Json::Value((int)k.age);
            student["sex"] = Json::Value(k.sex);
            banji["allStudents"].append(student);
        }
        root["allBanji"].append(banji);
    }
    Json::FastWriter fw;
    struct timeval start, end;
    gettimeofday(&start, NULL);
    for (int i = 0; i < MAX_NUM; ++i) {
        fw.write(root);
    }
    gettimeofday(&end, NULL);
    int t = 1000*(end.tv_sec - start.tv_sec);
    *time = t;
    *str = fw.write(root);
}


void thriftSerializeTest::thriftBinarySerialize(school sc,string *str, int *time) {
    struct timeval start, end;
    gettimeofday(&start, NULL);
    for (int i = 0; i < MAX_NUM; ++i) {
        boost::shared_ptr<TMemoryBuffer> buffer(new TMemoryBuffer());
        boost::shared_ptr<TBinaryProtocol> binaryProtcol(new TBinaryProtocol(buffer));

        sc.write((binaryProtcol.get()));
        buffer->getBufferAsString();
    }
    gettimeofday(&end, NULL);
    int t = 1000*(end.tv_sec - start.tv_sec);


    *time = t;
    boost::shared_ptr<TMemoryBuffer> buffer(new TMemoryBuffer());
    boost::shared_ptr<TBinaryProtocol> binaryProtcol(new TBinaryProtocol(buffer));
    sc.write((binaryProtcol.get()));
    *str = buffer->getBufferAsString();
}


void thriftSerializeTest::jsonDeserialize(string str, int *time) {
    Json::Reader reader;
    Json::Value root;
    int num = MAX_NUM;

    struct timeval start, end;
    gettimeofday(&start, NULL);
    while (num > 0) {
        //确保成功解析
        if (reader.parse(str.c_str(), root)) {
            num --;
        }
    }
    gettimeofday(&end, NULL);
    int t = 1000*(end.tv_sec - start.tv_sec);
    *time = t;
}



void thriftSerializeTest::thriftBinaryDeserialize(string str, int *time) {
    struct timeval start, end;
    gettimeofday(&start, NULL);
    boost::shared_ptr<TMemoryBuffer> buffer(new TMemoryBuffer());
    boost::shared_ptr<TBinaryProtocol> binaryProtcol(new TBinaryProtocol(buffer));
    school sc;
    for (int i = 0; i < MAX_NUM; ++i) {
        buffer->resetBuffer((uint8_t*)str.data(), str.length());
        sc.read(binaryProtcol.get());
    }
    gettimeofday(&end, NULL);
    int t = 1000*(end.tv_sec - start.tv_sec);
    *time = t;
}

int main() {


    //获取school对象
    school sc = thriftSerializeTest::initializeSchool();
    string thriftStr, jsonStr;
    int time;


    //①序列化为thrift binary protocol
    thriftSerializeTest::thriftBinarySerialize(sc, &thriftStr, &time);
    cout << "thrift序列化" << MAX_NUM << "次时间为" << time << "ms\n";

    //②反序列thrift binary data
    thriftSerializeTest::thriftBinaryDeserialize(thriftStr, &time);
    cout << "thrift反序列化" << MAX_NUM << "次时间为" << time << "ms\n";


    //③jsoncpp序列化为json
    thriftSerializeTest::jsonSerialize(sc, &jsonStr, &time);
    cout << "jsoncpp序列化json" << MAX_NUM << "次时间为" << time << "ms\n";

    //④jsoncpp反序列化json
    thriftSerializeTest::jsonDeserialize(jsonStr, &time);
    cout << "jsoncpp反序列化json" << MAX_NUM << "次时间为" << time << "ms\n";

    return 0;
}

编译cpp的命令为:

g++ service_a_constants.cpp service_a_types.cpp  thriftSerializeTest.cpp -std=c++11 -I/home/dengpan/opt/thrift-0.9.3/include -L/home/dengpan/opt/thrift-0.9.3/lib64 -lthrift -ljsoncpp -O3

C++代码测试结果为:

thrift序列化200次时间为23000ms
thrift反序列化200次时间为48000ms
jsoncpp序列化json200次时间为330000ms
jsoncpp反序列化json200次时间为494000ms

其中可以看出c++中thrift的序列化速度是jsoncpp 序列化json的14.3倍,thrift反序列化速度是jsoncpp反序列化json的10.2倍。其中c++序列thrift比java快,c++ jsoncpp严重不如java的fastjson,可能选取更好的c++ json库,可能结果好看点,但是java的fastjson还是很优秀的,用了大量的ASM写法,应该比java、c++绝大多数的json库要好。具体的,有空看看其他的c++ json库来测试一下。读者有更好的库,也可以推荐一下。

thrift对比json,流量更小、序列化反序列化更快,甚至快上5-10倍,这两个特点,在移动端跟实时性要求很高的游戏上,显得非常重要,也吸引人们在这些场景去使用像thrift这样的RPC、序列化的工具。

1.3兼容性好

Thrift Types定义了一些基本的Base Type,分别在源代码各个语言中都有一一映射。但是每个语言中,不是所有的定义的类型都支持,Thrift的一些绝大多数语言都支持的base Type有void、bool、i16、i32、i64、double、string。然后thrift通过一一映射,将IDL里面的base Type与各种编程语言里面做了对应,这样可以保证兼容性。而且不会存在java提供的一个接口,一个json字段是i64的,到了C++调用http接口的时候,拿int去取这个字段,丢失了数字的高位。这种情况在跨语言调用,由于每个语言的int、long、short的位数与范围都不同,开发者涉及多语言需要对每个语言的类型范围、位数很清楚才可以避免这样的极端情况的丢失数据,而用了Thrift就不用担心了,它已经帮我们映射好了一些基准类型。后面的thrift code generator也是生成基于TBase类型的对象,用到的也都是thrift base types。关于thrift base type可以参考官方文档: Thrift Types。具体的每个语言支持哪些Types定义,可以看源代码,比如thrift-src中,看到lib/rb/ext/thrift_native.c 中TType constants相关的为:

QQ20160512-1@2x看到thrift跟ruby的类型支持情况,没看到binary, 说明ruby不支持binary类型。c++支持thrift的binary类型。其实基准类型,已经足够使用了,建议不要使用不属于i16、i32、i64、double、string、list、map之外的,非通用的TType,不然可能没事找事,碰到一些兼容问题。

1.4可传递对象

由于IDL的存在,可以在IDL里面定义一些表示层级关系,数据结构,把我们要传递的对象,翻译成IDL里面的struct, 然后再把IDL编译成对应文件,这样就可以传递对象了。json等文本协议,会丢失部分信息。比如php里面$a =[“name” => 123], json_encode后,跟 $a= new stdClass();$a->name = 123; json_encode之后,是一样的。一个是数组,一个是对象。类似的这种序列化中类型丢失或者改变的,其实还有其他的例子。要我们小心的去使用。(可能这个例子举得并不充分,因为php里面数组太灵活了,可以干绝大多数事情)。而Thrift,只要我们定义好IDL,就可以放心的去传递对象了。

二、序列化方法

任何一个Struct,Thrift code generator都为它生成了一个对应的class,该类都包含write和read方法,write就是serialie过程, read方法就是unserialize过程。由于Thrift是连带Client调用Service的代码整套生成的,因此想单独拿Thrift序列化一个对象官方没给什么例子,不过各种语言把struct序列化为binary的方法大同小异。我这里研究了下各种语言怎么把对象单独序列化为string,这里一并贴出来。

①   C++ 序列thrift对象为string

boost::shared_ptr<TMemoryBuffer> buffer(new TMemoryBuffer());
boost::shared_ptr<TBinaryProtocol> binaryProtcol(new TBinaryProtocol(buffer));

youThriftObj.write((binaryProtcol.get()));
buffer->getBufferAsString();

② java序列化thrift对象为string

TSerializer serializer = new TSerializer(new TBinaryProtocol.Factory());
serializer.toString(bb); //就是序列后之后的string

③ php序列化thrift对象为string

$memBuffer = new \Thrift\Transport\TMemoryBuffer();
$protocol = new TBinaryProtocol($memBuffer);
$bbb->write($protocol);
$str = $memBuffer->getBuffer();

④ php序列化thrift对象到File

由于php中,没有TFileTransport, 因此改写了一下TPhpStream, 改成TPhpStreamMy,这样可以比较方便序列化到文件跟从文件中反序列化,这里也一并给出,TPhpStreamMy的github gist地址。放到php Thrift lib的Thrift/Transport/TPhpStreamMy.php位置。

序列化thrift对象到file

$transport = new TBufferedTransport(new TPhpStreamMy(null, ‘your-thrift-binary-file-path’), 1024, 1024);
$yourThriftObj->write(new TBinaryProtocol($transport, true, true));

从文件中读取thrift对象

$transport = new TBufferedTransport(new TPhpStreamMy(‘your-thrift-binary-file-path’, null), 1024, 1024);
$yourThriftObj->read(new TBinaryProtocol($transport));

其他语言的thrift序列化过程,也是类似步骤。

三、反序列化方法

反序列化方法跟序列化方法步骤类似,主要是把write操作改为read操作即可,下面给出一些语言的反序列化方法:

① C++ 反序列string为Object

boost::shared_ptr<TMemoryBuffer> buffer(new TMemoryBuffer());
boost::shared_ptr<TBinaryProtocol> binaryProtcol(new TBinaryProtocol(buffer));
buffer->resetBuffer((uint8_t*)str.data(), str.length()); //str为thrift对象序列化之后的string
yourThriftObj.read(binaryProtcol.get());

②  java反序列byte[]为Object

TDeserializer tDeserializer = new TDeserializer();
tDeserializer.deserialize(yourThriftObj, bytes);  //bytes等于thrift对象序列化之后的byte[]

③ php反序列化string为Object

$memBuffer = new \Thrift\Transport\TMemoryBuffer($str); //str为thrift对象序列化之后的string
$protocol = new TBinaryProtocol($memBuffer);
$yourThriftObj->read($protocol);

其他语言的thrift反序列化过程,也是类似步骤。

四、序列化代码解读-字节探究

这里用一个更简单的thrift service完整例子,用c++实现thrift的THttpServer, php实现thrift的THttpClient, 用wireshark抓包,看看整个通信过程,与字节发包的情况。

示例thrift/service_b.thrift IDL如下:

struct student {
    1:optional string name,
    2:optional i64    age,
}

service My_Services {
    student getStudent(1:student st);
}

C++实现thrift THttpServer代码如下:

// This autogenerated skeleton file illustrates how to build a server.
// You should copy it to another filename to avoid overwriting it.

#include "My_Services.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/transport/THttpServer.h>
#include <thrift/server/TNonblockingServer.h>



using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;

using boost::shared_ptr;

class My_ServicesHandler : virtual public My_ServicesIf {
 public:
  My_ServicesHandler() {
    // Your initialization goes here

  }

  void getStudent(student& _return, const student& st) {
    // Your implementation goes here


    _return.__set_age(st.age * 2);
    _return.__set_name(st.name + st.name + "哈哈哈哈");

    printf("getStudents \n");
  }

};

int main(int argc, char **argv) {

  int port = 9090;
  shared_ptr<My_ServicesHandler> handler(new My_ServicesHandler());
  shared_ptr<TProcessor> processor(new My_ServicesProcessor(handler));
  shared_ptr<TServerTransport> serverTransport(new TServerSocket("127.0.0.1", port));
  shared_ptr<TTransportFactory> transportFactory(new THttpServerTransportFactory());
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

  TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);


  server.serve();
  return 0;
}

server实现很简单,就是接受student对象,然后把student对象中的student.age = student.age * 2, student.name = student.name + student.name + “哈哈哈哈”,然后返回这个student对象。

php实现Php THttpClient如下:

<?php
// 引入客户端文件
require_once __DIR__ . "/../Thrift/ClassLoader/ThriftClassLoader.php";


require "My_Services.php";
require "Types.php";


use Thrift\ClassLoader\ThriftClassLoader;
use Thrift\Protocol\TBinaryProtocol;
use Thrift\Transport\TSocket;
use Thrift\Transport\TFramedTransport;

$loader = new ThriftClassLoader();
$loader->registerNamespace('Thrift', __DIR__ . "/../");
$loader->registerNamespace('Swoole', __DIR__ . "/../");
$loader->registerNamespace('Services', __DIR__ . "/../");
$loader->registerDefinition('Services', __DIR__ . "/../");
$loader->register();

$transport = new Thrift\Transport\THttpClient("127.0.0.1", 9090);
$protocol = new TBinaryProtocol($transport);

$client = new My_ServicesClient($protocol);


$transport->open();
$res = $client->getStudent(new student(
    [
        'name' => '邓攀',
        'age' => 1222,
    ]
));
var_dump($res);
$transport->close();

用wireshark抓包查看http通信过程,注意这个是本地127.0.0.1通信的,要在wireshark抓包指定loopback监听QQ20160512-2@2x

然后在wireshark中指定过滤器

http.content_type == “application/x-thrift” && http.request.method==”POST”

监听到请求,

QQ20160512-3@2x

可以看到监听到的请求,Content-Length是69B。为了方便跟踪,follow 这个tcp流,

QQ20160512-6@2x

可以看到http ressquest发送了226字节,http response收到了257字节。我把发送的数据、接收的数据,去除http头,就是纯二进制数据,写进了文件/tmp/thttp_request、/tmp/thttp_response中。文件大小与hexdump的文件值如下:

QQ20160512-8@2x下面来分析下这个文件的字节内容。以thttp_response文件为例,我们收到了69个字节,解析出来了name:邓攀邓攀哈哈哈哈,age:2444的数据。

五、提高一些脚本语言中的序列化性能

一些脚本语言的序列化性能不太好,再特别注意性能的场景下,建议可以用C/C++实现thrift的序列化,然后写成语言的一个模块、module的形式,给脚本语言去调用,这样可以极大的提高性能。也能利用到thrift的优点。

参考文章:

  1. 让Thrift支持双向通信--董的博客
  2. Thrift: The Missing Guide
  3. Thrift 官方文档

opensuse安装gdb debug等包

作者: dplord, 访问量 297





今天用gdb调试一个segmentation fault的时候,本机的gdb需要安装debug包,国内的各大开源镜像站,ustc、hust 、tsinghua.tuna以及mirrors.souhu.com mirrors.163.com mirrors.aliyun.com 都没有opensuse的debug源,其他的update oss non-oss等源都有。debug官方源(http://download.opensuse.org/debug/distribution/leap/42.1/repo/oss/)实在是太慢了,基本不可用。不知道为什么都不同步debug源,可能觉得debug源一般人用不到把。找了半天找到了一个可用的debug国内源,记录一下: 浙大Mirrors 的opensuse debug源是可用的,http://mirrors.zju.edu.cn/opensuse/debug/distribution/leap/42.1/repo/oss/。

zypper ar http://mirrors.zju.edu.cn/opensuse/debug/distribution/leap/42.1/repo/oss/ zju-debug添加后,sudo zypper ref刷新源。

然后安装opensuse debug包:

zypper install krb5-debuginfo-1.12.1-19.9.x86_64 libapparmor1-debuginfo-2.10-3.2.x86_64 libbz2-1-debuginfo-1.0.6-30.4.x86_64 libcom_err2-debuginfo-1.42.11-10.2.x86_64 libcurl4-debuginfo-7.37.0-5.2.x86_64 libfreetype6-debuginfo-2.5.5-8.2.x86_64 libgcrypt20-debuginfo-1.6.1-18.7.x86_64 libgpg-error0-debuginfo-1.13-3.6.x86_64 libidn11-debuginfo-1.28-4.1.x86_64 libjpeg62-debuginfo-62.1.0-31.1.x86_64 libkeyutils1-debuginfo-1.5.9-4.3.x86_64 libldap-2_4-2-debuginfo-2.4.41-9.1.x86_64 libltdl7-debuginfo-2.4.2-16.6.x86_64 liblzma5-debuginfo-5.0.5-3.5.x86_64 libmcrypt-debuginfo-2.5.8-117.1.x86_64 libopenssl1_0_0-debuginfo-1.0.1i-4.1.x86_64 libpcre1-debuginfo-8.33-3.5.x86_64 libpng16-16-debuginfo-1.6.8-2.2.x86_64 libsasl2-3-debuginfo-2.1.26-8.1.x86_64 libselinux1-debuginfo-2.3-3.5.x86_64 libssh2-1-debuginfo-1.4.3-10.1.x86_64 libxml2-2-debuginfo-2.9.1-8.1.x86_64 libxslt1-debuginfo-1.1.28-8.1.x86_64 libz1-debuginfo-1.2.8-6.4.x86_64

就可以使用gdb debug了。

二进制文件跟普通文本文件的区别

作者: dplord, 访问量 636





任何文件都可以划分为二进制文件(binary file)跟文本文件(text file), 两种文件表面上看起来显示,但是两种文件编码数据的方式却有差异。两种文件都是用一系列的字节编码数据,在文本文件中,所编码的字节就是代表文本文件的内容,而二进制文件的编码,却代表自定义的数据格式,需要特殊的去decode文件内容。下面就用『ab12\n3』为代表写入两种文件,读取看看差异。(\n 是换行符)

写程序如下:

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

int main() {
	FILE *fp = fopen("data.text", "w+");
	FILE *fp1 = fopen("data.bin", "wb+");
	
	
	if((fp == NULL) || (fp1 == NULL)) 
	{
		fprintf(stderr, "can not open file...");
		return -1;
	}
		
	const char *str = "ab12\n3";
	int len = strlen(str);
	fwrite(str, len, 1, fp);
		
	const char *str1 = "ab";
	int a = 12;
	const char *str2 = "\n";
	int b = 3;
	int len1 = strlen(str1);
	int len2 = strlen(str2);
	
	fwrite(str1, len1, 1, fp1);
	fwrite(&a, 1, 1, fp1);
	fwrite(str2, len2, 1, fp1);
	fwrite(&b, 1, 1, fp1);
	
	fclose(fp);
	fclose(fp1);
	return 0;
}

查看文件大小,  如下

➜  ~ ll -h data.text data.bin
-rw-r--r--  1 dengpan  staff     5B  3 14 01:38 data.bin
-rw-r--r--  1 dengpan  staff     6B  3 14 01:38 data.text
➜  ~ hexdump data.text
0000000 61 62 31 32 0a 33
0000006
➜  ~ hexdump data.bin
0000000 61 62 0c 0a 03
0000005

二进制文件data.bin是5bit, 文本文件data.text是6bit。其中文本文件data.text中的6个bit,分别对应a、b、1、2、\n、3。其中二进制文件data.bin里面的5个bit分别对应a、b、12、\n、3。

其中data.text就是其中的文本字符串的anscii吗,一个字符一个字符对应的。具体个参照ascii-table。文本编辑器打开data.text会换行,是因为碰到了换行符0a,编辑器会自动做换行处理的。

二进制文件的每一个bit放什么数据完全可以自己控制,可以放int、short、char等等,也可以放struct数据。当时解析二进制file的时候,需要知道解析规则,不然也不可读。以下是data.bin根据写的顺序写的读出来输出内容的解析代码:

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

int main()
{
	FILE *fp = fopen("data.bin", "rb+");
	if(fp == NULL) 
	{
		fprintf(stderr, "can not open file...");
		return -1;
	}
	
	int len_a = 2;
	char *a = malloc(len_a);
	int num1 = 0;
	int len_b = 1;
	char *b = malloc(len_b);
	int num2 = 0;
	
	fread(a, len_a, 1, fp);
	fread(&num1, 1, 1, fp);
	fread(b, len_b, 1, fp);
	fread(&num2, 1, 1, fp);

	printf("%s%d%s%d\n", a, num1, b, num2);
	fclose(fp);
	return 0;
}

其实文本文件本身就是一个特殊的binary file, 只不过是按照字符串内容,依次按字节写内容而已。二进制文件是按照自己的编码格式来的,常见的二进制文件比如图片、文档、视频,遵循一定的约定,通常是约定头部字节等于一些固定开头的值,各个文件约定也不尽相同,比如jpg的文件的头4个字节是固定的FF D8 FF E0 或者 FF D8 FF E1 或者 FF D8 FF E8, png的头8个字节是89 50 4E 47 0D 0A 1A 0A。用hexdump可以查看一个文件的hex内容。比如:

QQ20160314-1@2x

具体的查看每种文件的头部字节约定可以查看,File signatures网站

Mac下Virtualbox使用小结

作者: dplord, 访问量 834





因为工作原因,使用Mac机器。但是以前一直用linux,熟悉上面的各种开发环境。Mac由于是Unix系列,挺多开发环境挺好的。但是还是蛮多库、项目、编译没有那么方便。自己在virtualbox下改了一个opensuse text mode虚拟机,并设置了share folders, 可以在mac下的IDE下编辑建立在mac跟virtualbox的linux虚拟机的share dir的项目,然后在terminal下进入linux虚拟机,命令行编译跟处理。这样可以一个机器,无缝使用多个环境。下面讲解下virtualbox的几个要设置的地方。

1、安装virtualbox以及准备几个需要用的文件

要下当前最新的同一版本、最新的软件。

我下载的为

  • VirtualBox-5.0.14-105127-OSX.dmg
  • Oracle_VM_VirtualBox_Extension_Pack-5.0.14-105127.vbox-extpack
  • VBoxGuestAdditions_5.0.14.iso

2、设置正确的网络链接方式

默认的网络连接方式是『网络地址转换(NAT)』, 我需要的是内部linux guest主机,需要既能连接外网,同时也可以在外部mac host机器上,ssh通过内网连接linux guest。默认网络如图

QQ20160310-0@2x内部网络如图

QQ20160310-1@2xip是nat出来的网址10.0.2.15, 跟我的mac host不在一个网段,无法ssh连接。

需要创建一个处于同一网段的网络,因此做了如下操作

① 在virtualbox偏好设置里面添加一个host-only网卡,名字为vboxnet0

② 给该linux host的第二网卡上,绑定刚创建的vboxnet0 的 host-only 网卡

③ 在linux-guest里面,ifconfig查看还是一个网卡,手动编辑

touch /etc/sysconfig/network/ifcfg-eth1

写入以下内容

BOOTPROTO='dhcp'
BROADCAST=''
ETHTOOL_OPTIONS=''
IPADDR=''
MTU=''
NAME=''
NETMASK=''
NETWORK=''
REMOTE_IPADDR=''
STARTMODE='auto'
DHCLIENT_SET_DEFAULT_ROUTE='yes'

然后reboot,查看后就可以看到有eth1的网卡了, ip是192.168.56.101,可以在mac host用ssh连接。因为这个网络是dhcp分配的ip的,为了方便把vboxnet0网络,dhcp ip地址段设置为一个固定ip, 如图:

QQ20160310-4@2x以后在mac host, 每次ssh dengpan@192.168.56.101, 即可连接该机器。可以在bashrc做一个alias。

3、设置共享文件夹

① 挂载VBoxGuestAdditions_5.0.14.iso 到linux guest

② mount /dev/cdrom /mnt

③ cd /mnt  && ./VBoxLinuxAdditions.run –nox11 (因为我的是opensuse text mode,没有图形界面的)

④ 在virtualbox的设置界面设置share dir并设置自动挂载, 然后reboot, 就可以再/media 看到你的文件夹了。

4、virtualbox的备份、恢复

virtualbox比较好用的一点是,可以备份整个硬盘,然后把几十G的「虚拟硬盘.vdi 」拷贝到移动硬盘留着,在任何系统安装完了virtualbox挂载该「虚拟硬盘.vdi 」即可开始使用了。后面可以开virtualbox-linux, 开始无缝使用自己要用的工具了。在MAC下的IDE编辑项目,ssh连接到linux shell下编译、处理,网络都是共享的。使用比较便捷,而且不卡,符合我的预期。

 

岁月如歌,那些在华中大网络中心当协管的日子(部署篇)

作者: dplord, 访问量 2127





伴着夜晚的一点点凉意,在这熟悉的黑暗的夜色中,冷笔勾勒我在华中科大网络中心当协管的那些年月。想想突然离开学校也很久了,那个曾经承载我几年的欢声笑语、伴我在孤寒岁月中成长的学校以及渐渐远离我生命中了,不过那些曾经的烙在我生命中的印记,却是那么的强而有力,挥之不去。不过想想我过段时间要回学校办手续的,欣喜跃上心头。

记得最初的跟网络中心的打交道是,大一的时候刚连校园网的时候,我的windows下自动开了bluetooth调制解调器模式,跟校园网的拨号冲突导致死活无法上网,最后几通电话之后无果只能带电脑去网络中心解决了,想想大一进校的时候自己是多么的青涩,这也许是跟网络中心打交道的最早的起源了。

开始切入正题了,我是从大二暑假开始跟网路中心有些业务、项目上的交道的。从我做的cs.hust.edu.cn为例开始,从网络中心申请了1个测试机器,开始开发项目。最后等项目上线后,自然作为cs.hust.edu.cn的作者跟维护者,自然成了网络中心的协管了,其实就是在网络中心看管3台服务器,保证安全与服务可靠,当然作为作者,我是一度那么想优化ta的性能,以前也不断的做了各种折腾,以前年轻真是好,永远那么有精力,为了心中一点点的想法而通宵达旦地验证, 追求那么丁点的问心无愧跟坦坦荡荡把。

cs.hust.edu.cn的技术结构跟开发过程,这里以前写在了本博客的另一篇文章中,这里不做赘述。一言以蔽之,cs.hust.edu.cn是目前上线的部署代码在1万4千2百行大概的php站点,(运维的、php library的没算,中间修改的、废弃的代码没算)。其中我管理有3台机器,但是真实情况下cs.hust.edu.cn是部署在一个新的单独的机器上。机器的配置跟我用的核心软件如下表:

CPU 2 physical cpu, 24 processor
内存 48G内存
硬盘 4T机械硬盘(做了raid5)
网络状况 教育网,华中科技大学网络中心托管
operating system Centos 6.3(Final), 64bit
linux kernel 2.6
web server nginx 1.7.4(compile from source)
database server mysql 5.6.21(compile from source)
php php 5.5.9(compile from source)
hhvm HipHop VM 3.5.0-dev+2014.12.11 (rel)
Compiler: heads/master-0-g546087bf1b0560c4a9e254fcad46a9212e42ccc2
Repo schema: cf1780b3cc3857e091e924935ae6267e9794de9c
Extension API: 20140829
其他软件 ruby 2.2.1、rust 1.1.0、erlang、fail2ban

maybe someone will feel strange about why I use Centos 6.3 which is so old, 我当时准备好了给ta去装个opensuse text mode 12.3的(我当时是opensuse铁粉, 12.3是当时的最新版的opensuse)了,为了试验opensuse text mode的操作(当时仅仅是opensuse桌面版很熟悉,opensuse的很多配置网络、防火墙有的跟debian、ubuntu、centos等等不同,要单独学习, 还有在纯粹的命令行下配置网络连接啊等等、专门抱了一个台式机过来装opensuse text mode学习试试), 我把一个台式机装了opensuse text mode用了一段时间, 感觉挺好的。都已经做好了准备等新服务器到达的时候,去网络中心亲自给ta装上opensuse text mode 12.3,都跟学院说好了,他们也同意了。结果等机器来的那天,是上午10点对来的, 我当时在上大三上学期的《软件工程》的最后一节课,不能不去,不去上最后这节课老师大概的意思是说就直接这门课不给过,结果没有亲自装机,网络中心的老师就随便装了个centos 6.3, 算了也不吐槽网络中心给装的这么旧的发行版了,因为上一个我管理的机器是readhat5的,估计还很多人没见过redhat5把,那古老的东西,放图大家感受下。

6

7

部署工作

当初cs.hust.edu.cn开发的时候是用的测试机器在自申请的一个网络中心的测试机器上,开发进行,对外服务,当初是用ip直接访问的。测试机器性能记得很渣,大概是1GHz CPU + 2G内存+50G机械硬盘的样子,还是在一个windows server上装vmvare虚拟出来的一个vm。测试机器系统是centos6.4,用的apache + mysql + php提供服务。项目部署需要web server提供rewrite支持,其他的也没什么很特殊的。就是centos6.3上面的所有东西太老了于是很自然就开始了我的编译nginx+mysql+php+hhvm之旅。

1编译nginx

nginx编译很简单,没什么依赖,编译参数如下。

./configure \
--prefix=/home/dengpan/opt/nginx-1.7.3 \
--sbin-path=/home/dengpan/opt/nginx-1.7.3/sbin/nginx \
--conf-path=/home/dengpan/opt/nginx-1.7.3/conf/nginx.conf \
--error-log-path=/home/dengpan/opt/nginx-1.7.3/var/log/error.log \
--http-log-path=/home/dengpan/opt/nginx-1.7.3/var/log/access.log \
--pid-path=/home/dengpan/opt/nginx-1.7.3/var/run/nginx.pid \
--lock-path=/home/dengpan/opt/nginx-1.7.3/var/run/nginx.lock \
--http-client-body-temp-path=/home/dengpan/opt/nginx-1.7.3/var/cache/client_temp \
--http-proxy-temp-path=/home/dengpan/opt/nginx-1.7.3/var/cache/proxy_temp \
--http-fastcgi-temp-path=/home/dengpan/opt/nginx-1.7.3/var/cache/fastcgi_temp \
--http-uwsgi-temp-path=/home/dengpan/opt/nginx-1.7.3/var/cache/uwsgi_temp \
--http-scgi-temp-path=/home/dengpan/opt/nginx-1.7.3/var/cache/scgi_temp \
--user=www \
--group=www \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-mail \
--with-mail_ssl_module \
--with-file-aio \
--with-ipv6 \
--add-module=../mod_strip

基本按照官方编译参数(http://nginx.org/en/docs/configure.html)来的,只是加了mod_strip这个非官方模块,当时是想把所有页面压缩成一行的html, 节省下行传输带宽,提高速度。后面发现这点点html相比图片(我把首页的大图已经内页的logo图换成了webp格式,browser支持webp就显示webp格式,不支持就用jpeg格式)简直不值一提,其实也是因为这个mod_strip压缩html处理有bug, 处理有些页面有问题,标签会闭合到一起去,无法正常展示。

2编译php

php编译比较麻烦,一堆的依赖,第一次编译的时候,我编译了2天时间。后面编译就容易多了。在我的archlinux(i5 cpu, 机械硬盘)编译php 5.5.9大概需要8min。服务器make -j24大概需要10min。编译参数如下:

./configure \
--prefix=/home/dengpan/opt/php-5.5.9 \
--with-config-file-path=/home/dengpan/opt/php-5.5.9/etc \
--with-config-file-scan-dir=/home/dengpan/opt/php-5.5.9/etc/php.d\
--with-libdir=lib64 \
--with-curl \
--with-freetype-dir=/home/dengpan/opt/freetype-2.4.2/  \
--with-gd \
--with-gettext \
--with-iconv-dir=/home/dengpan/opt/libiconv-1.14   \
--with-jpeg-dir  \
--with-kerberos \
--with-ldap \
--with-ldap-sasl \
--with-libxml-dir  \
--with-mcrypt=/home/dengpan/opt/libmcrypt-2.5.7 \
--with-mhash \
--with-mysql=mysqlnd  \
--with-mysqli=mysqlnd  \
--with-openssl \
--with-pcre-regex \
--with-pdo-mysql=mysqlnd \
--with-pdo-sqlite=shared \
--with-pear \
--with-png-dir \
--with-xmlrpc \
--with-xsl \
--with-zlib \
--enable-fpm \
--enable-bcmath \
--enable-libxml \
--enable-inline-optimization \
--enable-gd-native-ttf \
--enable-mbregex \
--enable-mbstring \
--enable-opcache \
--enable-pcntl \
--enable-shmop \
--enable-soap \
--enable-sockets \
--enable-sysvsem \
--enable-xml \
--enable-zip \
--with-mysql=mysqlnd \
--with-mysqli=mysqlnd \
--disable-rpath

其中编译的时候缺少很多lib请自行用yum安装, 但是centos6.3上编译php,其中的libmcrypt、libiconv、freetype2需要自己编译安装,有些bug, 时间久了我也没记下来,记得编译的时候一堆问题,其中libiconv还需要手动修改一些代码之类的,具体的若有人碰到了,请自行stackoverflow一下。

3编译mysql

mysql编译比较容易,没太多依赖。编译参数如下:

cmake \
-DCMAKE_INSTALL_PREFIX=/home/dengpan/opt/mysql-5.6.21 \
-DMYSQL_DATADIR=/home/dengpan/opt/mysql-5.6.21/data \
-DSYSCONFDIR=/home/dengpan/opt/mysql-5.6.21/etc \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_READLINE=1 \
-DMYSQL_UNIX_ADDR=/home/dengpan/opt/mysql-5.6.21/tmp/mysql.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci

编译完了之后mysql需要初始化用户名用户组与mysql目录。

  1.  检查mysql用户组
    cat /etc/passwd|grep mysql
    cat /etc/group|grep mysql

    没有就添加

    1 groupadd mysql
    2 useradd -g mysql mysql
    
  2. 给mysql目录设置权限
    chown -R mysql:mysql /home/dengpan/opt/mysql-5.6.21
    
  3. 初始化数据库
    scripts/mysql_install_db --basedir=/home/dengpan/opt/mysql-5.6.21 --datadir=/home/dengpan/opt/mysql-5.6.21/data --user=mysql
    
  4. 直接启动数据库
    /home/dengpan/opt/mysql-5.6.21/support-files/mysql.server start
    

4编译hhvm

hhvm,可能大家用的少,这里简介一下,hhvm是facebook开发的一个php jit解释器,在hhvm + nginx部署网站中,hhvm此时等同于php-fpm的角色,作用就是监听端口(一般是9000端口)或者本地unix sock,然后nginx通过fastcgi建立socket与之通信。centos 6.3编译hhvm有点麻烦,内核卡在那里,glibc上不去,一堆编译错误。编译过程按照github hhvm的官方wiki(Building and installing HHVM on CentOS 6.3)编译的,我在我的opensuse 12.3跟13.1跟后来的archlinux都编译成功了, 在centos6.3编译了3天没有成功只好安装了 facebook提供的prebuilt package 。hhvm有一点点不稳定,但是性能根据我当时的使用情况,的确惊人。

由于此篇文章可能太长, 太多感悟与踩坑经历要写,因而拆分几篇来写, 本篇是部署篇。

岁月如歌,那些在华中大网络中心当协管的日子(部署篇)

岁月如歌,那些在华中大网络中心当协管的日子(运维工作篇)

岁月如歌,那些在华中大网络中心当协管的日子(其他总结篇)