Thrift
Thrift是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。
它被当作一个远程过程调用(RPC)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。
简单的说,thrift是在多个服务器之间搭建了有向边,使得在一服务器中可调用另一服务器的函数,达到传送数据,接受结果的目的。
RPC的简单理解为远程过程调用(远程函数调用),调用另一服务器上的函数;
Thrift:常用知识点:
一、命名空间:
命名空间thrift的命名空间相当于Java中的package的意思,主要目的是组织代码,thrift使用关键字namespace定义命名空间;
格式:namespace cpp 名字(其他语言类似,可在官网查看);
注意末尾没有分号;
二、数据类型:
-
基本类型:
thrift不支持无符号类型;
byte:有符号字节;
i16/i32/64:16位/32位/64位有符号整数;
double:64位浮点数;
string:字符串; -
容器类型:基本类型d
map<d1,d2> key/value对(key的类型是d1且key唯一,value类型是d2);
list d1类型的元素组成的有序表,元素可重复;
set d1类型的元素组成的无序表; -
Structs:结构体类型,各种数据类型的集合体,便于传输数据;
形式:
struct 结构体名 {
1: string s,
2: i32 a,
……
}
三、thrift使用:
(1).定义IDL接口描述文件(thrift文件)
a.为什么要定义IDL文件:
thrift可通过该文件,编译生成我们需要的语言的服务接口代码,即自动生成服务端骨架(Skeletons)和客户端桩(Stubs)(并未实现具体功能);
b.如何定义IDL文件:
描述数据类型和服务接口,即请求方与服务提供方之间传输的什么数据结构、调用的哪个服务接口(函数);
c.编译生成:
(2).生成server(提供函数的进程)
a. 需要按照服务骨架即接口,编写好具体的业务处理程序(Handler)即实现类即可。
(3).写client (客户端client)
a. 客户端:只需要拷贝IDL定义好的客户端桩和服务对象,然后就像调用本地对象的方法一样调用远端服务;
(4).生成指令:
thrift –r --gen cpp 文件名.thrift
生产者消费者模型:
一、什么是生产者消费者模型:
生产者消费者模型是一种多线程设计模式,就是在一个系统中,存在生产者和消费者两种角色,他们通过内存缓冲区进行通信,生产者生产消费者需要的资料,消费者把资料做成产品。
二、为什么使用它:
如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完数据才能够继续生产数据,因为生产过多的数据可能会导致存储不足;同理如果消费者的速度大于生产者那么消费者就会经常处理等待状态,所以为了达到生产者和消费者生产数据和消费数据之间的平衡,那么就需要一个缓冲区用来存储生产者生产的数据,所以就引入了生产者-消费者模式。
利用thrift框架,搭建项目简单的匹配机制:
一、整体逻辑:
1.游戏端:(client),向匹配系统发送请求,添加匹配信息add_user或删除匹配信息revome_user.
2.匹配系统(服务器端,一般用C++实现效率比较高):
(1). 接受并判断client的请求,add_user或remove_user;
(2). 将接受的匹配信息存储下来(匹配池),后并进行匹配;
(3). 把匹配好的信息发送(save_data)到数据存储服务器;
(4). 它既是游戏端(client)的Server,也是数据存储服务器的client;
3.数据存储服务器:
存储匹配系统发送的成功的匹配信息;
需要自己实现的部分为客户节点client(Python),和匹配系统Server(C++),放在terminal里(只是具有演示效果,并不是在真的服务器上),数据存储匹配系统在my server中;
二、步骤:
先申请库后与远程连接
1.定义一个添加用户和删除用户的thrift接口,存放接口的文件就是thrift文件。
~/thrift_lesson/thrift vim match.thrift (match/匹配)新建一个匹配接口
定义一个结构体存储用户信息:
struct User{
1: i32 id,
2:string name,
3.i32 score
}
定义函数:
方法定义类似于 C 代码。它有一个返回类型,参数
以及它可能引发的异常列表(可选)
service Match{
i32 add_user(1: User user, 2:string info) //info为额外信息,额外信息便于更改接口,好习惯!
i32 remove_user(1:User user,2: string info)
}
在完成好IDL文件后,在想要放置服务端的服务器或文件(在这里只是简单的演示过程,并非在真的服务器中)中使用生成指令thrift –r --gen cpp 路径/match.thrift,最好新建src(表示源文件)目录并把代码生成在其中;
2.为了方便,修改存放生成代码的目录(rm gen-cpp/ match_server
).
后再(mv match_server/Match_server.skeleton.cpp main.cpp
),在main.cpp的基础上完善server端的功能。
因为我们将main.cpp文件从下级目录拖动到了当前目录,路径就发生了改变,要修改main.cpp中的头文件。同时,给两个函数加上return,使其可通过编译(g++ -c main.cpp match_server/*.cpp
),.o文件是编译好的文件。编译好后再将文件进行链接(g++ *.o -o main -lthrift//利用了thrift的动态链接库
),运行后发现没有输出,可以对代码进行修改增加输出(using namespace std;(合作写代码最好不要加这个,可能会导致其他人变量名冲突) cout << Start Match Server << endl;
),修改后再次编译链接运行,加入到暂存区(只添加.cpp文件(在版本库中只存放源文件))后持久化,并上传到远程中;
3.在~/thrift_lesson/game创建src目录,生成python代码(thrift -r --gen py ../../thrift/match.thrift
)还是src目录;将gen-py改名为match_client并进入,打开隐藏文件后发现有Match-remote,这是python形成的服务器端,可删除;
将教程里的py client复制够来,修改后作为我们自己的客户端,新建client.py文件,粘贴复制内容。前四行是为了将当前文件加到环境变量里,无用可删除(Python中的知识);
再将代码中的路径进行修改(from match_client.match import Match
),再修改类型(from match_client.match.ttypes import User
),删除掉里面的无用教程代码,写业务代码。
为了方便一般会加(不加也可以,但加上是一个好习惯)
if__name__ == __main__:
main()
可以先在main()直接定义一个user用户方便待会编译运行观察结果;
user = User(1, ' yxc', 1500)
client.add_user(user, );
写好后,先开启服务端后可编译运行。成功后放入版本库中;
4.完善client.py ,添加输入输出,将原来的main 改为operate(op, user_id , username , score)
user = User(user_id, username,score);
if op == “add”:
client.add_user(user, “”)
else op == “remove”:
client.remove_user(user,“”)
from sys import stdin (引用类似头文件)
在新建main函数中读入终端的信息;
for line in stdin: (从标准输入读东西)
op, user_id, username, score = line.split(‘ ’)
operate(op , int(user_id), username, int(score))
在编译运行后client就完成了,将源文件放入暂存区后持久化,在传送到远程云端;
5.实现server端:
多线程:并行操作,有一个线程在接收到client的请求后,添加或删除信息;同时,匹配池要对其中的user信息进行匹配;且将匹配成功的信息传输到数据存储服务器中;
在这里涉及到了一个多线程模型—生产者消费者模型(见上)
首先给线程写一个消费者函数
void consume_task(){//本质是一个死循环,不停的匹配用户信息
while (true)
{
}
}
在主函数给消费者函数单开一个线程:thread 线程名 (函数);thread matching_thread(consume_task);
在生产者和消费者之间要有一个通信的媒介,即缓冲区;
可用很多种方式取实现,例如消费队列(想要实现需要一些锁)
定义一个锁 mutex m,有两个操作:p(m)表示获取锁,一旦争取到后,执行该操作时可以保证其他进程不会并行执行该段,其他锁被阻塞;v(m)表示在C++中,锁叫mutex,包含在#include<mutex>中,要实行消费队列,还需要使用到条件变量#include<condition_variable>
为了方便需要一个struct Task//任务
{
User user;//添加和删除用户;
string type;//操作类型;
};
定义一个消费队列struct MessgeQueue
{
queue<Task> q;//存取一系列任务
mutex m;
condition_varable cv; //条件变量
}message_queue;
void consume_task()
{
while(true)
{
if (message_queue.q.empty())//操作任务为空则退出循环;
{
continue;
}
else//不为空则弹出队头操作;
{
auto task = message_queue.q.front();
memage_queu.q.pop();
}
}
}
无论是删除还是添加操作都需要放到队列里,所以要在函数add_user中添加message_queue.q.push({user , “add”})
,同理在remove_use中添加message_queue.q.push({user , “remove”})
,且在该处加锁unique_lock<mutex> lck (message_queu.m)//(这样写不用显示解锁,在函数结束变量自动消失后,自动解锁)
,使得同一时间只能有一个线程对队列进行操作;
在consum_task()的while循环中也加上加锁操作;
(当队列为空后,先加锁再解锁会不断循环,所以当队列为空后要将给操作的锁阻塞掉,即在判空语句中加入 message_queue.cv.wait(lck)//(wait的意思是先将该锁解锁后卡死直到在别的地方唤醒)
,该进程就会被卡死;当队列又添加了操作不为空时,可以在别的地方唤醒该进程,即在add_user和函数中添加唤醒函数message_queue.cv.notify_all()
//唤醒所有被卡死的线程也可只唤醒一个cv.notify_one());
非空判断,完成出队操作后也要对进程解锁lck.unlock();(因为后面匹配操作时间较长,不解锁的话容易持有时间过长,导致其他进程堵塞);
void consume_task()
{
while (true)
{
unique_lock<mutex> lck(message_queue.m);
……
else
{
……
lck.unlock();
}
}
}
实现玩家池(即匹配池):
#include <vector>
class Pool
{
public:
void save_result(int a,int b){
printf(“Match Result:%d %d\n”,a,b);
}
void match()
{
while (users.size() > 1)//当匹配池中的人数大于1 时就匹配
{
auto a = users[0], b = users[1];//将前两个弹出匹配
users.erase(users.begin());//在动态数组中删除
users.erase(users.begin());
save_result(a.id,b.id);//传输匹配结果
}
}
void add(User user)
{
users.push_back(user);
}
void remove(User user)
{
for (uint32_t i = 0; i < users.size(); i ++ )
if (users[i].id == user.id)
{
users.erase(user.bein() + i );
break;
}
}
private:
vector<User> users;
}pool;
在consume_task() 的else中添加判断任务操作类型的语句:
if(task.type == “add”) pool.add(task.user);
else if (task.type == “remove”) pool.remove(task.user);
pool.match()
只改了main.cpp,所以只需要编译main.cpp(g++ -c main.cpp),编译后链接(g++ *.o –o main –lthrift -pthread)//因为该部分用到了线程,所以在链接时要加上线程的动态链接库 –pthread;
运行后,在客户端输入数据查看结果;
将main.cpp放入暂存区持久化后传入远程;
6.建立数据存储服务器端
在thrift中新建save.thrift,将接口文件复制过来后在match_system/src 中生成cpp文件,将其改名为save_client后进入,删除Save_server.skeleton.cpp(只需要客户端,不需要服务,要删除掉,不然会出现两个main函数,c++项目只能有一个main函数(py可以不删));
现在需要将save接口生成的代码加入到项目中,打开main.cpp后将apache c++样例中的Client头文件抄下来(抄缺失的部分,再加入生成的代码#include “save_server/Save.h”,以及接口件的命名空间using namespace :: save_service),将样例main函数里的东西复制粘贴在Pool的save_result中(格式化)。server节点的地址在本地上,改掉client.py中transport中的(‘localhost’)->(‘127.0.0.1’),save节点在myserver上,所以把main.cpp中的TTransport(‘localhost’)改为(‘服务器的hostname’),将改为Save,删掉教程transport->open()—transport->close里的东西后,加入调用给save传数据的函数save_data(“……”);
编译save_client里的cpp文件和main.cpp (g++ -c save_client/*.cpp),链接(g++ *.o main –lthrift -pthread);
将.h .cpp .thrift .py文件暂存持久化后传输到云端(save–client);
7 . 升级匹配系统;
a.实现每1秒匹配一次:
#include <unistd.h>
在consume_task的if语句中,删除阻塞锁的语句后,解锁(lck.unlock();),再调用函数pool.match(),sleep(1);//当队列为空时,解锁,等待1秒,让其他进程操作该部分后,再继续匹配操作;
void match()
{
while(users.size()>1)
{
sort(user.begin(), user.end(),[&](User& a, User& b){
return a.score <b.score; });
bool flag = true;//当遍历一遍找不到时就容易死循环,用flag代表是否找到;
for (unit32_t i = 1; i < users.end() ; i++){//遍历动态数组中相邻的两个找分数差小于50的;
auto a = users[i - 1], b = users[i];
if (b.score – a.score <= 50)
{
flag = false;
users.erase(users.begin() + i – 1, users.begin() + i +1)
save_result(a.id,b.id);
break;
}
}
if (flag) break;
}
}
// 编译链接main.cpp文件;
// 在pool文件中将传输匹配成功数据的函数修改一下,获取一个返回值可以看是否传输成功(int res = client.save_data(“acs_....”,“密码”,a,b); if(!res) put(“success”); else puts(“failed”););
编译链接后,暂存持久化后,传输远程(match server:3.0);
8.单线程服务器改为多线程服务器:
将apache中Server复制过来,将没有的头文件都写上后,将原本单线程的定义方式删除后复制粘贴多线程的定义方式(main中的内容),接着复制factory 后格式化一下,把样例名Calculator改为Match,把::shared::SharedService改为Match;
编译链接,发现有输出信息,将输出注释掉,编译链接暂存持久化后传输远程(match server:4.0);
9.终极优化版本出现!!!
如果有两个人分差较大,一直匹配不到一起,在经过一段时间后可以凑乎匹配一下;
修改pool中的动态数组,vector users; vector wt//表示等待时间秒数;
在Pool中的add中加上wt.push_back(0)//初始化表示刚添加的玩家等待时间为0,remove中erase时也要把等待时间删除(wt.erase(wt.begin() + i)),把排序和for循环中的东西删除后在其中再添加一个
for(uint32_t j = i + 1; j < users.size(); j ++ ),上一个循环从0开始; { if(check_match(i,j)) { auto a = users[i] , b = users[j]; users.erase(users.begin() + j);//先删后面的,避免影响前面的位置; users.erase(users.begin() + i); wt.erase(wt.begin() + j); wt.erase(wt.begin() + i); save_result(a.id,b.id); flag = false; break; } }
for(uint32_t i = 0; i < wt.size( ); i ++)加到Match内部的上面
{
wt[i] ++;//等待秒数
}
bool check_match(uint32_t i,uint32_t j)//在match的上面
{
auto a = users[i], b = user[j];
int dt = abs(a.score – b.score);
int a_max_dif = wt[i] * 50;
int b_max_dif = wt[j] * 50;
return dt <= a_max_dif && dt <= b_max_dif;//保证i,j都在彼此由等待时间延申的范围内;
}
删除consume_task()中else的pool.match()//不删相当于每来一次人都要匹配,保证每次等待都是1秒;
编译运行链接后传输远程main.cpp(match server:5.0);