<AcWing>-thrift

catalog

  • Basic
  • 匹配-服务1
    • main.cpp
  • 游戏-服务1
    • client.py
  • 匹配-服务2
    • C++线程
    • 内存池
    • Version1 代码
  • 匹配结果 存入DB
  • 多线程

Basic

一个应用, 包含有 多个 服务, 不同的服务 在不同的服务器上 (也可以在同个服务器)


比如说一个游戏, 一些玩家可以匹配到一起, 然后开始游戏.
(游戏 主服务) (匹配系统 服务)

主服务里, 一个玩家 点击开始匹配, 那么此时: 主服务 会调用 匹配服务的 一个函数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端

匹配-服务1

创建 ~/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

main.cpp

#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里, 把编译结果文件放进去 不太好

游戏-服务1

' 进入 ~/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

client.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

对于匹配系统:

  • 他一边要: 接收新的玩家进来 即一个死循环
  • 同时还要: 进行匹配算法 只要有玩家, 就进行算法; 其实也是个死循环

两个工作, 需要并行的进行.
两个死循环 都需要同步的进行, 即涉及到线程的概念;
即一个线程 不断的接收进来的玩家, 一个线程 不停的进行匹配算法

C++线程

一个程序, 里面可能会有很多的线程; 上百个都有可能

这么多个线程, 分为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满足一定容量后 进行匹配.

Version1 代码

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

此时我们要新加一个功能: 即当两名玩家成功匹配后, 我们将这个匹配结果 存到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();	' 这也是个线程 '

你可能感兴趣的:(计算机知识,服务器,thrift)