一个应用, 包含有 多个 服务
, 不同的服务 在不同的服务器上 (也可以在同个服务器)
比如说一个游戏, 一些玩家可以匹配到一起, 然后开始游戏.
(游戏 主服务
) (匹配系统 服务
)
主服务
里, 一个玩家 点击开始匹配
, 那么此时: 主服务 会调用 匹配服务的 一个函数add_user
函数, 将这个玩家 加入到 匹配系统里
然后比如, 这个玩家 又点击了取消匹配
, 就对应为: 主服务 会调用 匹配服务的 remove_user
函数, 将这个玩家 从 匹配系统中, 移除掉
比如, 匹配系统, 成功的匹配了一些人, 然后调用 数据库系统 服务
的 save_match
函数, 将匹配的结果 存到数据库里, 做一些log记录
thrift, 就是上面的这些函数接口 add_user, remove_user, save_match
, 即 服务间的 通信
也就是: A服务器的x进程, 想要调用, B服务器的y进程 AB可以是同个服务器
, 这就是thrift
而且, 不同的服务, 使用的语言 可以是不同的! 比如, 游戏服务是 python语言, 匹配系统是 c++语言
thrift是: rpc框架 remote procedure call 远程函数调用
在这里, 比如A 调用了 B的一个函数add_user
, 那么: A称为client端
, B称为:server端
创建 ~/thrift_lession文件夹
, 表示, 我们整个项目的工作区, 进入该工作区
vim readme.md
mkdir game ' 游戏 服务 '
mkdir match ' 匹配 服务 '
mkdir thrift ' 放一些 thrift的函数接口 '
vim match.thrift ' 文件名是match, 说明这是 (针对 match匹配服务的 接口) '
内容如下 这是thrift语法
:
namespace cpp match_service
' 表示, 如果这个thrift去生成一个cpp文件, 则你这里定义的(结构体/函数), 都是在(match_service)的命名空间下的 '
struct User{ ' 一个结构体 '
1: i32 id,
2: string name,
3: i32 score
}
service Match{ ' 函数; match服务, 向外提供的接口 '
i32 add_user(1: User user, 2: string info),
i32 remove_user(1: User user, 2: string info),
}
' 进入 ~/thrift_lession/match 文件夹, 即当前是在 匹配服务(也就是一个项目)'
mkdir src ' 任何一个项目, 最好都要有一个src 存源文件 '
cd src/
thrift -r --gen cpp ~/thrift_lesson/thrift/match.thrift ' 根据之前写的thrift接口, 生成对应的cpp代码 '
此时, 在match/src/下, 有一个gen-cpp/的文件夹 ' 这就是上面生成的cpp代码 '
里面有: 'Match.cpp Match.h Match_server.skeleton.cpp match_types.cpp match_types.h'
mv gen-cpp/ match_server ' 改个名字. match_server表示: 这个是服务端的接口. 即是当前match 提供给外界来使用的接口 '
mv match_server/Match_server.skeleton.cpp main.cpp
此时src/里是:
|-- main.cpp
|-- match_server
|-- Match.cpp
|-- Match.h
|-- match_types.cpp
|-- match_types.h
#include "Match.h"
int32_t add_user(const User& user, const std::string& info) {
// Your implementation goes here
printf("add_user\n");
}
int32_t remove_user(const User& user, const std::string& info) {
// Your implementation goes here
printf("remove_user\n");
}
int main(){
serve();
return 0;
}
可以发现, 我们的这些接口的实现, 并没有返回值; 手动把函数加上返回值.
由于, 我们将main.cpp, 放到了src下, 自然#include "Match.h"
也是错的, 需要加一个match_server/
目录
在main函数里, 写一个: ::std::cout<< "test" << ::std::endl;
, 注意, 要写上endl
因为, main里有个serve函数, 程序是一直运行的. cout的缓冲区, 刷新到屏幕, 要么是程序结束, 要么你手动的调用endl
. 否则, 你看不到cout
的输出
先编译下, 让项目正常运行
g++ -c main.cpp ./match_server/*.cpp
: 编译, 生成.o
此时, src/下, 有很多的.o
g++ *.o -o main -lthrift
(链接dll)
./main
执行
假如你修改了某个cpp, 比如修改了main.cpp. 无需g++ *.cpp -o main
因为, 其他的.o文件
已经有了.
最好是: g++ main.cpp -c
, 单独去生成main.o
. 然后再: g++ *.o -o main -lthrift
在/src目录下:
git add .
git restore --staged *.o
git restore --staged main
此时, 所有非.o文件 非执行文件
, 就都在 暂存区里了 最好只是把一些源文件 放到git里, 把编译结果文件放进去 不太好
' 进入 ~/thrift_lession/game 文件夹 '
mkdir src ' 同样生成src, 存储源文件 '
thrift -r --gen py ../../thrift/match.thrift ' 根据接口, 生成python代码; 因为, game也需要调用这些接口 '
' 此时, 当前目录下多了: gen-py文件夹 '
mv gen-py/ match_client ' 改个名字. 表示, 这是客户端要使用的接口 '
tree ' 这是: src/match_client/ 的目录 '
|-- __init__.py
|-- match
|-- Match-remote ' 删除这个文件; 他是提供远程服务的, 而此时是客户端, 不是服务端; 删不删都可以 '
|-- Match.py
|-- __init__.py
|-- constants.py
|-- ttypes.py
' 在game/src目录下 (此时, 这个目录下 有一个match_client): '
vim client.py
' client.py 如下: '
def main():
# Make socket
transport = TSocket.TSocket('127.0.0.1', 9090)
# Buffering is critical. Raw sockets are very slow
transport = TTransport.TBufferedTransport(transport)
# Wrap in a protocol
protocol = TBinaryProtocol.TBinaryProtocol(transport)
# Create a client to use the protocol encoder
client = Match.Client(protocol)
# Connect!
transport.open()
user = User(1, 'wchang', 15000)
client.add_user( user, "me")
if __name__ == "__main__":
main()
此时, 先将我们的match服务里的 之前生成的./main 可执行文件
给运行起来. 他会一直在运行, 在监听
然后, 在这里game 服务里
, 执行这个client.py
文件: python3 client.py
对应的, 在match服务里, 这个正在运行的./main程序
, 会输出执行了add_user函数
即, game 服务
调用了 match 服务
里的add_user函数
对于匹配系统:
即一个死循环
只要有玩家, 就进行算法; 其实也是个死循环
两个工作, 需要并行的进行.
两个死循环 都需要同步的进行, 即涉及到线程
的概念;
即一个线程 不断的接收进来的玩家, 一个线程 不停的进行匹配算法
一个程序, 里面可能会有很多的线程; 上百个都有可能
这么多个线程, 分为2大类: 生产者: 产生任务Task
消费者: 处理任务Task
以我们这个match匹配系统为例, add_user函数, 会产生一个: 添加玩家的Task; remove_user函数, 会产生一个: 删除玩家的Task;
即, 这两个函数, 都会产生任务 比如任务1是: 删除某个玩家, 任务2是: 添加某个玩家
然后需要一个内存池, 存所有的任务Task
生产者 和 消费者, 这2者之间 需要进行通信
, 实现方式有: 消息队列
实现消息队列
时, 需要用到: 锁mutex
#include
比如, 生产者 和 消费者 多个线程, 同时去 操作一个内存, 如果无法保证原子性 就会发生冲突;
比如, 一个线程 在执行消息队列的push操作: message_queue.push(..);
,而一个push
函数, 他是对应有多个机器指令: 1 2 3 4 5 6 7
,
一旦发生: a线程执行了push
, 当机器指令执行到4
时, 执行权突然跳到了b线程, b线程也执行了push
操作, 这是非常危险的.
因为此时b线程, 面对的message_queue
是未知的. 因为push
操作没有完成
所以, 必须保证: push()
函数 (其对应的机器执行), 是完整执行结束的; 在执行push
函数的过程中, 执行权 不能被剥夺
所以, 有些操作 不能同时去执行; 这就是锁mutex
的用处, 必须保证, 对内存的操作, 同一时间 只能有1个线程在操作他
锁有2个操作:
p操作: 去争取这个锁; 一旦争取到了这个锁, 就可以去进行操作了; 而在进行操作时, 会保证 其他线程不会并行的执行这一段代码
即, 我拿到这个锁, 其他锁就阻塞掉了; 比如同时有100个线程在争取这个锁, 一旦一个线程拿到了这个锁, 那么99个线程 就会被阻塞
v操作: 等拿到锁的人, 执行完他的操作后, 执行v操作 释放锁
#include
c++里有个 条件变量, 他是对mutex锁
进行了一个封装
struct Message_Queue{
queue< Task> task_queue;
mutex mutex_;
condition_varible condition_var;
}message_queue;
void func1(){
std::unique_lock< mutex> _lock( message_queue.mutex_);
message_queue.task_queue.push(...);
}
void func2(){
std::unique_lock< mutex> _lock( message_queue.mutex_);
message_queue.task_queue.push(...);
}
这个_lock
, 就是对消息队列的 互斥量
, 进行上锁; 等到这个_lock
摧毁时, 就会自动的 解锁
即使func1 和 func2
同时执行, 比如func1先 获取到了锁
, 那么func2的lock 就会被阻塞, 获取不到锁
, 即, 保证了: func1的 push
操作, 是原子性的
void consume(){ ' 消费者, 监测 消息队列 '
while( true){
unique_lock< mutex> lock( message_queue.mutex); // 注意位置!! 是在while里面
if( message_queue.empty()){
}
else{
auto task = message_queue.task_queue.top();
message_queue.task_queue.pop();
lock.unlock();
' 处理这个task '
}
}
}
操作完message_queue
后, 必须立刻unlock
; 否则会导致: 在处理task时 这是个耗时的过程, 其他的线程 无法操作message_queue
这是不应该的, 因为在处理task时, consume线程 不会用到message_queue, 为什么不让别人使用呢?
一旦使用完共享变量
后, 一定要及时
解锁
这里有一个问题: 如果消息队列不是空的, 由于会访问消息队列里的元素, 此时上锁是应该的.
但是, 如果当消息队列是空的, 从上面的代码, 他还是会上锁 解锁, 每次的while死循环, 都是在重复(上锁 解锁), 就会陷入死循环 cpu占用达到100%
即此时这个consume线程, 他一直在运行态: 不断的上锁解锁, 导致其他线程无法使用消息队列.
这是不应该的. 因为你在执行NULL语句, 没有使用消息队列 反而还不让其他线程使用队列!!
正确的做法是: 当消息队列是空时, 这个线程应该被阻塞住 不能让他一直占用cpu 反而一直执行NULL语句, 让线程卡住
让consume线程堵塞住, 让其他线程去执行; 知道消息队列不是空时, consume线程才能恢复运行
-> 这就需要用到: 条件变量 condition_varible
要写成:
unique_lock< mutex> lock( message_queue.mutex);
if( message_queue.empty()){
message_queue.condition_var.wait( lock);
}
wait的作用是: 将锁lock 给释放掉, 然后当前consume线程 就被卡住了 (即堵塞);
当外界将这个条件变量condition_var给唤醒后, 当前被卡住的consume线程, 才解除卡住
即, 一旦消息队列是空, 该consume线程 就会被message_queue里的 condition_var 这个条件变量
所卡住 (即当前线程 被堵塞)
唤醒条件变量, 唤醒了message_queue里的 condition_var 这个条件变量
, 也就是: 唤醒了 `正在被堵塞了的 consume线程
' 其他线程 执行的函数func: '
void func(){
message_queue.push( ...);
' 执行完push后, 队列已经不空了! 也就是, consume线程, 应该被唤醒; '
message_queue.contion_var.notify_all();
' notify_all(): 通知所有被(message_queue.contion_var)卡住的线程, 唤醒他 '
}
把所有的Task, 存到一个pool里, 然后当pool满足一定容量后 进行匹配.
struct Task{
26 public:
27 User user;
28 ::std::string type; // 任务的类型: add_user 还是 remove_user?
29 };
30
31 struct Message_Queue{
32 public:
33 ::std::queue< Task> task_queue;
34 ::std::mutex mutex_;
35 ::std::condition_variable condition_var;
36 }Message_queue;
37
38 class Pool{
39 public:
40 void add( const User & _u){
41 ::std::cout << "pool - add: (id: )" << _u.id << ::std::endl;
42 users.push_back( _u);
43 }
44 void remove( const User & _u){
45 ::std::cout << "pool - remove: (id: )" << _u.id << ::std::endl;
46 for( uint32_t i = 0; i < users.size(); ++i){
47 if( users.at(i).id == _u.id){
48 users.erase( users.begin() + i);
49 break;
50 }
51 }
52 }
void save_matched_result( const User & _a, const User _b){
54 ::std::cout << "matched: id(" << _a.id << ", " << _b.id << ")" << ::std::endl;
55
56 }
57
58 void match(){
59 while( users.size() >= 2){
60 ::std::cout << "matched succ" << ::std::endl;
61 User a = users.at( 0);
62 User b = users.at( 1);
63 users.erase( users.begin(), users.begin() + 2);
64 save_matched_result( a, b);
65 }
66 }
67
68 private:
69 ::std::vector< User> users;
70 }pool;
71
72 void Consume_task(){
73
74 while( true){
75 ::std::unique_lock< ::std::mutex> _lock( Message_queue.mutex_);
' 注意这里, lock 是在while里面的!!!
76 if( Message_queue.task_queue.empty()){
77 Message_queue.condition_var.wait( _lock);
78 // continue;
79 }
80 else{
81 auto task = Message_queue.task_queue.front();
82 Message_queue.task_queue.pop();
83 _lock.unlock();
84
85 // todo task
86 if( task.type == "add"){
87 pool.add( task.user);
88 }
89 else if( task.type == "remove"){
90 pool.remove( task.user);
91 }
92
93 pool.match();
94 }
95 }
96 }
97
98 class MatchHandler : virtual public MatchIf {
99 public:
100 MatchHandler() {
101 // Your initialization goes here
102 }
103
104 int32_t add_user(const User& user, const std::string& info) {
105 // Your implementation goes here
106 printf("add_user (id: %d)\n", user.id);
107
108 // thread :: begin
109 ::std::unique_lock< ::std::mutex> _lock( Message_queue.mutex_);
111 Message_queue.task_queue.push( {user, "add"});
112
113 Message_queue.condition_var.notify_all();
114 // thread :: end
115
116
117 return 0;
118 }
119
120 int32_t remove_user(const User& user, const std::string& info) {
121 // Your implementation goes here
122 printf("remove_user\n");
123
124 // thread :: begin
125 ::std::unique_lock< ::std::mutex> _lock( Message_queue.mutex_);
126
127 Message_queue.task_queue.push( {user, "remove"});
128
129 Message_queue.condition_var.notify_all();
130 // thread :: end
131
132 return 0;
133 }
134
135 };
137 int main(int argc, char **argv) {
138
139 ::std::cout << "(match_system): enter main function" << ::std::endl;
140
141 int port = 9090;
142 ::std::shared_ptr<MatchHandler> handler(new MatchHandler());
143 ::std::shared_ptr<TProcessor> processor(new MatchProcessor(handler));
144 ::std::shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
145 ::std::shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
146 ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
147
148 ::std::thread match_thread( Consume_task);
151
152 TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
153 server.serve();
154 return 0;
155 }
此时我们要新加一个功能: 即当两名玩家成功匹配后, 我们将这个匹配结果 存到db里
[match服务] <-> [db服务]
' 在thrift/下, 添加: save.thrift '
1 namespace cpp save_service
2
3 service Save{
4
5 # username: myserver服务器的名称 acs_2357
6 # passwork: myserver服务器密码的MD5sum
7 # 密码验证失败 返回1, 成功会返回0 且匹配信息结果在存在myserver:homework/lesson_6/result.txt里
8 i32 save_data(1: string username, 2: string password, 3: i32 player1_id, 4: i32 player2_id)
9 }
这是我们的接口 (服务端接口的实现即db服务的代码
不用我们实现, 已经实现好了 )
我们只需要, 实现: 客户端接口的实现 即match服务里, 实现去连接 [db服务]
到match_system/src/
下, 执行thrift -r --gen cpp ../../thrift/save.thrift
, 此时在当前文件夹下, 就生成了: gen-cpp
, 将他改名为: save_client
进入save_client/
, 他里面有: Save.cpp Save.h Save_server.skeleton.cpp save_types.h
其中, Save_server.xx
是 服务端的代码, 我们这里是客户端, 所以要把他给delete掉
回到match_sysytem/src/main.cpp
里,
#include "./save_client/Save.h" // 刚生成的 [与db服务]通信的接口 的cpp实现
' 之前这个函数是空的, 此时在这里, 成功匹配后, 将他的结果 存到[db服务]里 '
void save_matched_result( const User & _a, const User _b){
' [db服务]的 服务器的 ip号'
::std::shared_ptr< TTransport> socket( new TSocket("123.57.47.211", 9090));
::std::shared_ptr< TTransport> transport( new TBufferedTransport( socket));
::std::shared_ptr< TProtocol> protocol( new TBinaryProtocol(transport));
' 这个, 就是./save_client/Save.h里, 生成的一个类 '
SaveClient client( protocol);
try{
transport->open();
' 登录密码, 是md5sum后的 '
auto ret = client.save_data( "acs_2357", "580fafec", _a.id, _b.id);
::std::cout << ret << ::std::endl; ' 0为成功 '
transport->close();
}catch( TException & _e){
::std::cout << "(ERROR): " << _e.what() << ::std::endl;
}
}
g++ ./save_client/Save.cpp -c ' 这里很重要!!! 与[db服务]的接口实现是在: save_client文件夹下 '
' 这个文件夹里有: Save.cpp Save.h save_types.h; 一定要记得编译这个cpp, 否则一会main.cpp 会就找不到实现了'
g++ ./main.cpp -c ' [匹配服务] 的项目 '
g++ *.o -o main -lthrift -pthread
然后就可以: ./main 启动匹配服务, ipython3 ./client.py 启动游戏服务
匹配成功后: ssh myServer 进入[db服务器]: cat ~/homework/lesson_6/resulte.txt
md5sum
根据 MD5值, 反过来去求 原串, 几乎不可能
' 输入md5sum命令: '
902976bc ' 输入你的文本, 然后: 回车, ctrl+d '
580fafec24c5922541e06f2539e830cb ' md5值 '
match_system里, 此时的服务端 (即当外界调用add_user/remove_user时, 此时的match_system就是[服务端])
是单线程的
即, 生产者, 是单线程的
TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
' Simple, 即简单server (一个线程, 去处理, 所有的 外界调用的 add_user/remove_user) '
server.serve();
如果我们想要提高并发量
的话, 可以使用 多线程
服务器; 即外部每调用一个函数, 就开一个线程
160 class MatchCloneFactory : virtual public MatchIfFactory{
161 public:
162 ~MatchCloneFactory() override = default;
163 MatchIf * getHandler( const ::apache::thrift::TConnectionInfo & connInfo) override{
' 外部每调用一个: add_user/remove_user, 就会进入这里, 开一个线程 '
164 ::std::shared_ptr< TSocket> sock = ::std::dynamic_pointer_cast< TSocket>( connInfo.transport);
165 ::std::cout << " geted connection \n";
166 ::std::cout << "Socket Info: "<< sock->getSocketInfo() << "\n";
167 ::std::cout << "PeerHost: " << sock->getPeerHost() << "\n";
168 ------------
169 ::std::cout << "Peer Addr: " << sock->getPeerAddress() << "\n";
170 ::std::cout << "Peer Port: " << sock->getPeerPort() << "\n";
171
172 return new MatchHandler;
173 }
174 void releaseHandler( MatchIf * handler) override{
175 delete handler;
176 }
177 };
void main(){
183 TThreadedServer server(
184 ::std::make_shared< MatchProcessorFactory>( ::std::make_shared< MatchCloneFactory>()),
185 ::std::make_shared<TServerSocket>( 9090),
186 ::std::make_shared<TBufferedTransportFactory>(),
187 ::std::make_shared<TBinaryProtocolFactory>());
' 把之前 (单线程的 TSimpleServer), 改为, (多线程的 TThreadedServer) '
188
189 // 开一个线程, 处理(所有的任务: add_user 和 remove_user)
190 ::std::thread match_thread( Consume_task); ' 这是一个线程 '
191
192 server.serve(); ' 这也是个线程 '