thrift
thrift简介
Thrift
是一种接口描述语言和二进制通讯协议,它被用来定义和创建跨语言的服务。它被当作一个远程过程调用(RPC,remote procedure call)框架来使用,是由Facebook为“大规模跨语言服务开发”而开发的。它通过一个代码生成引擎联合了一个软件栈,来创建不同程度的、无缝的跨平台高效服务,可以使用C#、C++
(基于POSIX兼容系统)、Cappuccino、Cocoa、Delphi、Erlang、Go、Haskell、Java、Node.js、OCaml、Perl、PHP、Python、Ruby
和Smalltalk。虽然它以前是由Facebook开发的,但它现在是Apache软件基金会的开源项目了。该实现被描述在2007年4月的一篇由Facebook发表的技术论文中,该论文现由Apache掌管
简单来说, Thrift
可以构建微服务,实现远程进程调用,使得不同服务之间、不同平台之间可以交互和互相调用程序
thrift官网,内有详细教程,可以作为查询资料。以下通过一个项目实践来介绍一下常用操作和流程
相比于直接用 socket
手撸,thrift
已经实现好了各种通信,不需要手写序列化反序列化函数,也不需要维护socket
C++ socket
可以找zeroMQ
,里面有封装好的socket
程序
项目实践:匹配系统
内容:常用的玩家匹配系统
- 业务逻辑
- 前端游戏,后端匹配系统
- 玩家进入游戏前端,通过前端操作向后端发送服务请求
- 开始匹配:调用
add_user()
函数,将个人信息传入匹配系统,匹配系统进行匹配并返回结果- 结果也可以调用
save_date()
函数传给另一个服务器
- 结果也可以调用
- 退出匹配:调用
remove_user()
函数,取消匹配请求,匹配系统将该玩家从匹配池中移出 - 中间的各种函数调用一般是跨平台的(
python
调用cpp
代码,或不同进程,不同服务器上函数的调用),需要用thrift
实现
- 需要三个节点
match_client
:接收玩家状态信息,并向match_server
发送匹配请求match_server
:接收来自match_client
的请求,并进行匹配save_client
:将匹配结果返回到另一个服务器上
- 步骤
- 新建
thrift
文件,给跨平台调用的函数定义统一接口 - 匹配系统作为提供函数的一方,需要根据
thrift
文件生成一个server
端 - 写游戏
client
端接收前端信息
- 新建
步骤
1.创建客户端 game
和服务端 match_system
(可以在不同服务器上也可以是同一服务器的不同文件夹,最终都是跑在不同进程上)
2.创建 thrift
文件夹用来定义和存储各种跨平台函数接口
先定义 add_user()
和 remove_user()
函数的接口
创建 match.thrift
文件,定义命名空间、用户信息结构体和函数接口( add_user()
和 remove_user()
)
写完后存到仓库里
3.创建 match_server
通过命令生成 thrift
文件配置下的 server
代码
thrift -r --gen cpp(language) (thrift文件路径)
生成之后会有一个新文件夹 gen-cpp
,里面有一个 skeleton
文件,这就是根据 thrift
文件生成的 cpp
服务代码骨架,我们只需要在里面填充具体逻辑即可
gen-cpp
改名为 match_server
,里面就是各种 thrift
生成的配置文件;把 skeleton
移动成 main.cpp
到 match_server
的同一级文件夹里
在 main.cpp
的函数里添加业务逻辑(推荐先把返回值写上,然后编译跑通,在逐步添加模块,添一个编译一次)
注:团队开发时最好不要加 using namespace std
,因为很容易导致变量名冲突
C++编译过程:
- 编译
.cpp
文件成.o
文件- Linux命令:
g++ -c *.cpp(all .cpp files)
- 已经编译通过的文件不用重复编译
- Linux命令:
- 链接
.o
文件- Linux命令:
g++ *.o -o main
,表示把所有编译好的文件链接进main
程序中,由main
程序执行 - 编译
thrift
的C++文件时,需要用到 thrift 动态链接库,因此还要在链接命令里添加-lthrift
- Linux命令:
- 链接完之后就可以
./main
运行程序了
4.创建 match_client
通过命令按照 thrift 文件配置生成 python 服务器端代码
服务器端代码里的 Match-remote
可执行文件相当于 C++ 里的 skeleton
代码,是写 python 服务端的骨架代码,但这里我们用 python 写客户端,所以可以删掉这个文件
python 客户端的骨架代码直接从官网 tutorial 里复制出来,然后修改添加成可以用的客户端代码
- 前四行是将路径添加到环境变量里的代码,可以删去
- 好习惯,在代码末尾加上
if __name__ == "__main__":
main()
运行 client
端代码时 server
端必须处于运行状态
写好之后就可以在 client
的 python
代码里调用 server
里的 C++
函数了
5.完善客户端代码
第一代:写死的用户信息,调用固定的函数
第二代:从终端持续读入用户信息和操作
6.编写与完善服务端匹配系统
业务模型:多线程并行
- 线程1:客户端调用
add_user()
和remove_user()
,分别要将对应用户加入系统或移出系统 - 线程2:不停地对系统中地用户进行匹配并将结果返回到
save
端 - 生产者-消费者模型
- 上述两个线程在加一个缓存区(匹配池)就可以构成生产者-消费者模型。
- 匹配池采用消费队列
- 互斥量(也叫锁,mutex)
- 线程1和2都需要操作消费队列,但是为了程序能正常运行,消费队列一次只能被一个线程操作。因此需要引入互斥量
- 为一个资源设定互斥量后,一个线程想操作它,必须先获取该资源的互斥量锁;而只有当这个线程释放互斥量锁之后,其他线程才有可能拿到锁,从而对资源进行操作。
- p 操作:获取锁
- v 操作:释放锁
- 互斥量的引入保证了该资源不会被多个线程并行操作。而对于生产者-消费者模型,给缓存区设互斥量
- 生产者先拿到锁。
- 当生产者无新产出或缓存区已满时,释放锁,消费者拿到锁;
- 当缓存区为空时,消费者释放锁,生产者拿到锁
- 不断重复
- 保证当生产者和消费者执行频率不对等时缓存区不会发生异常
- 条件变量 condition_variable
- 对锁进行了一个封装
C++ 多线程编程(最基本的操作)
#include <thread>
#include <mutex>
//给消费队列上锁
#include <condition_variable>
//引入条件变量,实现消费队列更容易
#include <queue>
//实现消费队列
struct Task
{
User user;
string type;
};
struct MessageQueue
{
queue<Task> q;
mutex m;
condition_variable cv;
}message_queue;
//用互斥量和条件变量把一个普通的队列包装成消费队列
//第一步:定义线程操作(就是函数)
//消费者
void consume_task()
{
while(true)
{
unique_lock<mutex> lck(message_queue.m);
//创建一个锁变量,获取消费队列的锁
//这样的写法有一个优点,就是当语句块执行结束后,局部变量会被自动释放,锁也就被自动释放了,而不需要主动解锁
//不停地消耗匹配池里的用户,一直占用线程
if(message_queue.q.empty())
{
//如果这里什么也不做,那么循环会接着回到这里,一直拿着消费队列的锁,消费队列会一直为空,程序就将陷入死循环
message_queue.cv.wait(lck);
//因此使用条件变量将锁暂时释放并将线程卡死在这。当另一处代码把锁交还时重新唤醒这个线程
//add_user和remove_user执行时会往消费队列插入元素,因此可以在它们插入元素后使用下面的代码唤醒所有(或一个)被改条件变量卡住的线程
//message_queue.cv.notify_all();
//message_queue.cv.notify_one();
}
else
{
auto task = message_queue.q.front();
message_queue.q.pop();
lck.unlock();
//得到队头元素后就不需要对消费队列操作了,因此及时解锁有利于提高程序执行效率
//do task
}
}
}
int main()
{
thread matching_thread(consume_task);
//matching_thread为线程名称
//consume_task传入函数地址(用该线程执行该函数)
}
多线程编译时要加上参数 -pthread
构建对象:匹配池
- 作用:维护当前进入匹配的所有玩家
- 私有成员
- 所有玩家
- 公有成员:
- 添加玩家函数
- 删除玩家函数
- 负责匹配的函数
- 记录匹配情况的函数
7.创建 save_client 端
接口已编写好,直接生成代码
- 数据要传给另一台服务器,需要验证身份。求密码的
md5sum
值的方法:终端输入md5sum
命令后回车,再输入密码,回车后按ctrl + d
就得到md5sum
值 - 注意把地址修改为要传过去的服务器地址
skeleton
里有一个 main
函数,与当前的 main
函数冲突,因此必须删掉
save_client
虽然是一个节点,但是直接受 save_result
函数的调用,因此 client
端的代码直接写在记录匹配情况的成员函数里面
对教程里的变量名要进行修改
8.匹配服务升级(先把基本功能写好跑通,在升级)
第一代:空
第二代:只要有两个人及以上就按序匹配
第三代:只有当两名玩家分差在50以内时才会匹配在一起
- 当消费队列为空时直接释放锁,每一秒匹配一次
第四代:多线程提高并发
client
端每发送一次请求或调用一次函数都单开一个线程来处理- 将
SimpleSever
换成Thread
,再复制一个factory
第五代:每名玩家只要等待时间足够长,最终都可以成功匹配,但匹配的分差范围会随时间变大
- 记录每名玩家等待的秒数
- 每等待一秒,玩家匹配的分差范围就扩大50