鱼香rose'Blog

Thrift

\(\Huge{Linux-Thrift}\)

目录

thrift 概述

thrift 是一种接口描述语言和二进制通讯协议,他被用来定义和创建跨语言的服务。

Thrift 是 Facebook 于 2007 年开发的跨语言的 RPC(Remote Procedure Call,远程过程调用)服务框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过 Thrift 的 IDL(接口定义语言)来描述接口函数及数据类型,然后通过 Thrift 的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码, 让不同语言构建的服务可以做到远程调用无缝对接。(它们的目的都是提供本地调用远程服务的能力)

RPC(Remote Procedure Call,远程过程调用)是一个计算机通信协议,此协议允许 进程间通信。简单来说,当机器 A 上的进程调用机器 B 上的进程时,A 上的调用进程被挂起,而 B 上的被调用进程开始执行。调用方可以通过参数将信息传送给被调用方,然后可以通过被调用方传回的结果得到返回
大部分的 RPC 框架都遵循如下三个开发步骤:

  1. 定义一个接口说明文件:描述了对象(结构体)、对象成员、接口方法等一系列信息;
  2. 通过 RPC 框架所提供的编译器,将接口说明文件编译成具体的语言文件;
  3. 在客户端和服务器端分别引入 RPC 编译器所生成的文件,即可像调用本地方法一样调用服务端代码;
    RPC 是一种 C/S 架构的服务模型,server 端提供接口供 client 调用,client 端向 server 端发送数据,server 端接收 client 端的数据进行相关计算并将结果返回给 client 端。

thrift 官网 -> Tutorial -> tutorial.thrift
Thrift 使用 C++进行编写,在安装使用的时候需要安装依赖,windows 安装方式见官网即可。
安装方式: 官网安装方式

  • windows 下载 thrift.exe – http://archive.apache.org/dist/thrift/0.9.1/thrift-0.9.1.exe

  • centos 执行 yum install thrift

  • 通过源码编译安装 https://github.com/apache/thrift

安装 thrift python 包:

pip install thrift

查看 thrift 版本: (推荐 0.16.0 版本)

thrift -version

下面以 Ubuntu20.04 为例展示安装过程:
(1)下载依赖库

sudo apt-get install automake bison flex g++ git libboost-all-dev libevent-dev libssl-dev libtool make pkg-config

(2)下载安装文件并解压

wget https://dlcdn.apache.org/thrift/0.16.0/thrift-0.16.0.tar.gz
tar -xvzf thrift-0.16.0.tar.gz

(3)安装

cd thrift-0.16.0
sudo ./configure
sudo make
sudo make install

thrift 的跨语言特型

thrift 通过一个中间语言 IDL(接口定义语言)来定义 RPC 的数据类型和接口, 这些内容写在以 .thrift 结尾的文件中, 然后通过特殊的编译器来生成不同语言的代码, 以满足不同需要的开发者, 比如 java 开发者, 就可以生成 java 代码, c++ 开发者可以生成 c++ 代码, 生成的代码中不但包含目标语言的接口定义, 方法, 数据类型, 还包含有 RPC 协议层和传输层的实现代码。

Thrift IDL

Thrift 采用 IDL(Interface Definition Language) 来定义通用的服务接口,然后通过 Thrift 提供的编译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能。

  • 通过命令调用 Thrift 提供的编译器将服务接口编译成不同语言编写的代码。
  • 这些代码又分为服务端和客户端,将所在不同进程(或服务器)的功能连接起来。

使用 IDL 对接口进行描述的 thrift 文件命名一般都是以 “.thrift” 作为后缀:XXX.thrift

thrift 服务分为服务提供方(server 端)和服务请求方(client 端)

通过 IDL 文件做到 serverclient 的解耦。

service DemoService {
    string say();
}

上面就是一个最简单的 IDL 文件,他表示服务名为:DemoService 的服务中提供了一个名为 say 的方法,这个方法无参数传入,返回 string 类型。

生成代码文件:

thrift -r --gen  

例如: thrift -r --gen py demoservice.thrift
demoservice 就是存储我们上述所说的 DemoService 的文件,如果要生成其他语言的代码,则将 py 换成对应的目标语言,如 java、cpp、php 等。

thrift 的协议栈结构

thrift2.png

thrift 是一种 c/s 的架构体系。在最上层是用户自行实现的业务逻辑代码.第二层是由 thrift 编译器自动生成的代码,主要用于结构化数据的解析,发送和接收。TServer 主要任务是高效的接受客户端请求,并将请求转发给 Processor 处理。Processor 负责对客户端的请求做出响应,包括 RPC 请求转发,调用参数解析和用户逻辑调用,返回值写回等处理。从 TProtocol 以下部分是 thirft 的传输协议和底层 I/O 通信。TProtocol 是用于数据类型解析的,将结构化数据转化为字节流给 TTransport 进行传输。TTransport 是与底层数据传输密切相关的传输层,负责以字节流方式接收和发送消息体,不关注是什么数据类型。底层 IO 负责实际的数据传输,包括 socket、文件和压缩数据流等。

thrift 应用

Thrift 类型系统包括预定义的基本类型(如 bool , byte, double, string)、特殊类型(如 binary: 未经过编码的字节流)、用户自定义结构体(看上去像 C 语言的结构体)、容器类型(如 listsetmap),以及异常和服务定义~

注意,thrift 不支持无符号整型,因为很多目标语言不存在无符号整型(如 java)。

thrift 语法

thrift 基础数据类型

//The first thing to know about are types. The available types in Thrift are:
bool        //Boolean, one byte               bool
i8 (byte)   //Signed 8-bit integer            byte
i16         //Signed 16-bit integer           short
i32         //Signed 32-bit integer           int
i64         //Signed 64-bit integer           long long
double      //64-bit floating point value     double
string      //String                          string
binary      //Blob (byte array)               ByteBuffer

//容器(Container)
map  //Map from one type to another
list    //Ordered list of one type
set     //Set of unique elements of one type

List :一系列 t1 类型的元素组成的有序表,元素可以重复
Set :一系列 t1 类型的元素组成的无序表,元素唯一
Mapkey/value 对(key 的类型是 t1key 唯一,value 类型是 t2

例子:

struct Test{
    1: list intList;
    2: map users
}

映射类型赋值时,键值用冒号 : 隔开

map users = {1: "xxx", 2: "yyy"}

可像 c++typedef 给结构体取别名

typedef map xxx

声明要转化的语言

Thrift 中的命名空间同 C++ 中的 namespacejava 中的 package 类似,它们均提供了一种组织(隔离)代码的方式。因为每种语言均有自己的命名空间定义方式(如 python 中有 module),thrift 允许开发者针对特定语言定义 namespace

声明命名空间的必要性:防止不同空间里的变量名重复。

/**
 * Thrift files can namespace, package, or prefix their output in various
 * target languages.
 */

namespace cl tutorial
namespace cpp tutorial
namespace d tutorial
namespace dart tutorial
namespace java tutorial
namespace php tutorial
namespace perl tutorial
namespace haxe tutorial
namespace netstd tutorial

//匹配系统我们用C++实现。
//语法:namespace 使用的语言 空间名称
namespace cpp match_dao

namespace java com.xtxxtx.test 转换成 package com.xtxxtx.test

结构体定义

Thrift 结构体在概念上同 C 语言结构体类型—-一种将相关属性聚集(封装)在一起的方式。在面向对象语言中,thrift 结构体被转换成类,在 Java 语言中,这等价于 JavaBean 的概念

注意: 在 struct 定义结构体时需要对每个结构体成员用序号标识:“序号: ”

/**
 * Structs are the basic complex data structures. They are comprised of fields
 * which each have an integer identifier, a type, a symbolic name, and an
 * optional default value.
 *
 * Fields can be declared "optional", which ensures they will not be included
 * in the serialized output if they aren't set.  Note that this requires some
 * manual management in some languages.
 */
struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}

异常(Exception)

异常在语法和功能上类似于结构体,只不过异常使用关键字 exception 而不是 struct 关键字声明。但它在语义上不同于结构体—当定义一个 RPC 服务时,开发者可能需要声明一个远程方法抛出一个异常。如:

exception MyException {
    1: string code;
    2: string message;
}

服务 Service 定义

一个服务包含一系列命名函数,每个函数包含一系列的参数以及一个返回类型。
在语法上,服务等价于定义一个接口或者纯虚抽象类~

注意:参数需要加上编号

格式如下,

service  {
    ()
  [throws ()]
...
}

如:

service  UserService {  
  // service中可以定义若干个服务,相当于Java Interface中定义的方法
  string sayHello(1:string name);
}

Typedef

C++ 类似

typedef i32 MyInteger

Include

Thrift 允许 thrift 文件包含,相当于 CPP 中的 includeJava 中的 import,如:

#include "user.thrift"

namespace java com.xxx.tutorial.thrift.service  

service  UserService {  

  string sayHello(1:string name),

  bool saveUser(1:user.User user)
}

说明:
a. thrift 文件名要用双引号包含,末尾没有逗号或者分号
b. 注意 user 前缀

例子 2:

IDL 文件中对所有接口函数的描述都放在 service 中,service 的名字可以自己指定,该名字也将被用作生成的特定语言接口文件的名字,接口函数需要对参数使用序号标号,除最后一个接口函数外,要以“”结束对函数的描述。

例如,下面一个 IDL 描述的 Thrift 文件(该 Thrift 文件的文件名为:test_service.thrift)的全部内容:

namespace java com.test.service

include "thrift_datatype.thrift"

service TestThriftService
{

    /*
      * value 中存放两个字符串拼接之后的字符串
    */
    thrift_datatype.ResultStr getStr(1:string srcStr1, 2:string srcStr2),

    thrift_datatype.ResultInt getInt(1:i32 val)

}

这里的 TestThriftService 就被用作生成的特定语言的文件名,例如我想用该 Thrift 文件生成一个 java 版本的接口文件,那么生成的 java 文件名就是:TestThriftService.java

枚举 Enum

可以像 C/C++ 那样定义枚举类型,如:

enum Gender {
    MALE,
    FEMALE,
    UNKONWN
}

thrift 项目实战

thrift 是一种接口描述语言,用简便的 thrift 语言写完以后,可以用命令快速生成任何语言的文件

thrift 实现一个基本的 匹配系统 接口 。
thrift1.jpg

  • 服务分为三部分:分别是 gamematch_systemsave_server
  • gamematch_client 端,通过 match.thrift 接口向 match_system 完成添加用户和删除用户的操作。
  • match_system 由两部分组成,分别为 match_server 端和 save_client 端。match_server 端负责接收 match_client 端的操作,将用户添加进匹配池,并且在匹配结束之后通过 save.thrift 接口将用户数据上传至另一个服务器。
  • save_server 用于接收 match_system 上传的匹配成功的用户信息。

这个游戏的功能可能运行在一个或多个服务器(或进程)上,而 thrift 就是将不同服务器不同语言的功能连接起来。
图中的三个节点(功能)是完全独立的,既可以在同一个服务器上,也可以在不同服务器上。
每一个节点就是一个进程,每个进程可以使用不同的语言来实现。

基本框架

  1. 游戏应用端(Python3
    • 客户端(match_client):与 匹配系统服务器 的服务端交互
  2. 匹配系统服务器(C++, match_system
    • 服务端(match_server):与 游戏应用端 的客户端交互
    • 客户端(save_client):与 数据存储服务器 的服务端交互
  3. 数据存储服务器(已经实现, save_server
    • 服务端:与 匹配系统服务器 的客户端交互(save.thrift)

游戏匹配服务项目流程

  • 构建 match.thrift 接口
  • 通过 match.thrift 接口构建服务端和客户端
  • 先将服务端和客户端跑通,能完成基本的连接通信
  • 完成 match.thrift 的客户端需求
  • 构建 save.thrift 接口
  • 通过 save.thrift 接口构建服务端和客户端
  • 将客户端业务添加到 match_system 当中,将 save.thrift 服务端完成(本项目 save.thrift 服务端已经提前做好了)
  • 根据业务需求完善 match_system

实现流程

初始化 git 仓库

mkdir thrift_lesson
cd thrift_lesson
git init

mkdir thrift        # 存放 thrift 源文件
mkdir game          # 实现 游戏应用端 的 客户端 功能
mkdir match_system  # 实现 匹配系统服务器 的 服务端 和 客户端 功能

touch readme.md
# 远程创建好git上的repo
git remote add origin git@git.acwing.com:YumeMinami/thrift_lesson.git
git add .
git commit -m "init repo"
git push -u origin master

实现 游戏应用端 与 匹配系统服务器 的交互

创建 match.thrift 接口文件
用于实现 游戏应用端 与 匹配系统服务器 交互的 service

namespace cpp match_service //声明转换为C++语言

struct User {   //定义结构体 User
    1: i32 id, 
    2: string name,
    3: i32 score
}

service Match { //定义service服务
    /** 
     * user: 添加的用户信息
     * info: 附加信息
     * 在匹配池中添加一个名用户
     */  

    i32 add_user(1: User user, 2: string info),

    /** 
     * user: 删除的用户信息
     * info: 附加信息
     * 从匹配池中删除一名用户
     */  

    i32 remove_user(1: User user, 2: string info),
}

实现匹配系统服务器的服务端(match_server)

运行 match.thrift 接口文件在匹配系统服务器端生成 C++ 源文件

进入 match_system 中新建文件夹 src (之后所有的 匹配服务器 源文件放在 src 下)

并在 src 文件夹中运行 thrift 脚本生成 C++ 版本的文件

# 原始的thrift生成文件语法:thrift -r --gen  
mkdir match_system/src
cd match_system/src
thrift -r --gen cpp ../../thrift/match.thrift

# 将该文件夹重命名为 match_server(区别于之后要此处生成的client server)
# match_server 与 游戏应用端交互 ; client_server 与 数据存储服务器交互
mv gen-cpp match_server

# 把 Match_server.skeleton.cpp 移动到当前 src 目录下并重命名为 main.cpp
# 方便之后调试 main.cpp 文件,其他的源文件仍被存放在 src 文件夹下
mv match_server/Match_server.skeleton.cpp main.cpp

# 由于移动了 main.cpp 故需要修改一下 main.cpp 中头文件里关于 Math.cpp 的引用路径: match_server/Match.h
# 之后写 client_server 同理

具体操作如下所示:
thrift3.png

thrift 服务的时候,先编译成功,然后再逐步向文件中添加模块。

编译并运行 cpp 文件

CPP 编译流程: 1. 编译(g++ -c .cpp) 2. 链接(g++ -o *.o -o 可执行文件名 -动态链接库)

编译只需要编译 .cpp 文件,头文件(.h)不需要编译

将所有想要编译的文件,放在 g++ -c 命令后面即可
编译完成后会生成 .o 文件,有多少个 .cpp 文件就有多少个 .o 文件
最后将所有 .o 文件链接起来生成可执行文件即可。

# 1. 编译 所有的 .cpp 文件生成 .o 文件
# g++ -c [文件1.cpp] [文件2.cpp] ...
g++ -c main.cpp match_server/*.cpp

# 2. 链接 所有的 .o 文件生成可执行文件 .exe
# g++ [文件1.o] [文件2.o] ... -o [需要额外添加的动态库]
# 此处需要额外添加 -lthrift 的 thrift 动态库
g++ *.o -o main -lthrift

# 3. 运行 可执行文件
./main

非编译文件非可执行文件 提交到 git 中去(好的工程习惯)

git add .
git restore --stage *.o     # .o文件是编译文件,不加入暂存区里
git restore --stage main    # main是可执行文件,不加入暂存区里
git commit -m "add match server"
git push

实现游戏应用端的客户端(match_client)

同上,在 game 文件夹下实现 游戏应用端 的 客户端(match_client)

# 原始的thrift生成文件语法:thrift -r --gen  
mkdir game/src
cd game/src
thrift -r --gen py ../../thrift/match.thrift

# 将该文件夹重命名为 match_client
# 不过这里不改也无所谓,游戏应用端只有匹配的客户端
mv gen-py match_client

# 删掉 Match_remote 
# 该文件是用 py 实现 服务端 时用的文件
# 此处我们只需要实现 客户端 功能,因此他没有作用,不妨删掉,让文档简洁一点
rm match_client/match/Match-remote

thrift4.png

利用官网提供的模板(官网模板),编写 客户端文件 client.py

  • 删除前 4 行代码
  • Match 替代 Calculator
  • match_client.match 替代 tutorial,修改成实际路径
  • 删掉教学代码——transport.open()transport.close() 之间的代码,替换成自己的业务代码
  • 加入调试部分代码 __main__

一些课外知识 Python 实现 Thrift Server

客户端文件 client.py

# 先删去开头前四行代码,该代码的作用只是将当前路径加入到环境变量里,我们用不到
# 此处修改路径,以及Calculator 为 Match
# 此处修改 ttypes 路径 以及 User 类
from match_client.match import Match  		## match.thrift 里实现的Service服务 Match
from match_client.match.ttypes import User 	## match.thrift 里定义的struct结构User

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

def main():
    # Make socket
    transport = TSocket.TSocket('localhost', 9090) # 客户端需要修改成服务端所在的IP地址

    # Buffering is critical. Raw sockets are very slow
    # 增加缓存区,提高socket速度
    transport = TTransport.TBufferedTransport(transport)

    # Wrap in a protocol 
    # 创建协议
    protocol = TBinaryProtocol.TBinaryProtocol(transport)

    # Create a client to use the protocol encoder 
    # 创建客户端
    client = Match.Client(protocol)  ## 从模板上粘贴下来时记得要修改客户端的名字,这里是Match

    # Connect!
    # 启动客户端
    transport.open()

    # 调试语句(其实是要在这里写具体的业务逻辑代码)
    user = User(1, 'yxc', 1500)
    client.add_user(user, "") 

    # Close!
    transport.close()

 # 调用 main 函数
if __name__ == "__main__":
    main()

把文件保存到 game/src

要先开启匹配服务端(match_server)后运行 client.py 开启客户端(match_client)才不会报错!

acs@ff884a1dd578:~/thrift_lesson/match_system/src$ ./main 
acs@ff884a1dd578:~/thrift_lesson/game/src$ python3 client.py

如果服务端出现 add_user,说明客户端创建成功。

这样我们就成功地用 python 调用了一个 C++ 代码了 QAQ
实际上是 client.py 中调用了原 Match_server.skeleton.cpp(现 main.cpp)中 MatchHandler 类的 add_user 函数

非编译文 件 和 非可执行文件 提交到 git 中去(好的工程习惯)

git add .
git restore --stage *.pyc   # .pyc文件是编译文件,不加入暂存区里
git restore --stage *.swp   # .swp文件是缓存文件,不加入暂存区里(vim没关时会生成)
git restore --stage "*.o"   # .o文件是编译文件,不加入暂存区里
git commit -m "add match_client"
git push

实现客户端业务逻辑

重写 client.py 使之能不断从终端里读入信息

client.py

# 利用 python 在终端读入信息需要引入 stdin
from sys import stdin

 # 将原来的通信 main 函数改写成operate函数,每次需要的时候调用一次建立通信传递信息
 # 目的是可以一直不断处理信息
 # 然后重写 main 函数,使之能不断从终端读入信息
def operate(op, user_id, user_name, score):
    # ...........................

    # 针对 op 参数,分别进行 "增加" 与 "删出" 操作
    user = User(user_id, user_name, score)

    if op == "add":
        client.add_user(user, "")
    elif op == "remove":
        client.remove_user(user, "")

    # ...........................

def main():
    for line in stdin:
        op, user_id, user_name, score = line.split(' ')
        operate(op, int(user_id), user_name, int(score))

此时运行客户端后,可在控制台读入数据 op user_id username score,例如 add 233 yxc 1000

实现服务端业务逻辑

写 server 端需要分析业务模型。

需要进行监控(读用户进来),支持增加用户和删除用户。同时还要有一个线程去不停地进行匹配,将信息传回给服务器端。

这个时候,我们发现需要实现两个功能,添加删除用户和匹配用户,根据微服务的概念需要开两个进程实现两个功能。

响应客户端请求和处理客户端请求可以拆分成两个独立过程,可使用 多线程 提高其效率。

假设使用两个线程完成服务端响应和处理过程:

  • 一个线程负责 响应客户端请求,接收客户端指令;
  • 另一个线程负责处理指令,完成匹配

生产者-消费者模型

这里需要运用到操作系统 里的 PV 原语 以及 生产者-消费者 模型

在本项目中,请求 主要指 客户端指令:添加用户 add_user() 和删除用户 remove_user(),因此可用一个结构体 Task 描述该指令,其中 type 用于区分指令类型。

struct Task {
    User user;
    string type;        // "add"或"remove"
};

响应客户端请求的线程可看做 生产者,它创建若干个 Task 对象;
处理客户端请求的线程可看做 消费者,按照一定的规则删除 Task 对象,因此可用 生产者消费者模型 实现该过程。生产者消费者模型需要通信媒介,常用的一种实现方式是 消费队列

在代码实现中,消费队列 是生产者进程和消费者进程的 共享变量,多个线程同时修改它可能会导致结果出错,因此需要引入 锁机制。当某个线程拿到消费队列的锁 mutex 后,消费队列只能由该线程使用,当另一个线程想使用时,会发现消费队列已上锁并进入 阻塞 状态,直到锁 mutex 被释放。

一些 os 概念:

互斥锁(S = 1):一个 P 操作(上锁),一个 V 操作(解锁)

P 操作的主要动作是:
①S 减 1;
② 若 S 减 1 后仍大于或等于 0,则进程继续执行;
③ 若 S 减 1 后小于 0,则该进程被阻塞后放入等待该信号量的 等待队列 中,然后转进程调度。

V 操作的主要动作是:
①S 加 1;
② 若相加后结果大于 0,则进程继续执行;
③ 若相加后结果小于或等于 0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。

对于 P 和 V 都是原子操作,就是在执行 P 和 V 操作时,不会被插队。从而实现对共享变量操作的原子性。
特殊: S = 1 表示互斥量,表示同一时间,信号量只能分配给一个线程。

多线程为啥要用锁? 因为多线程可能共享一个内存空间,导致出现重复读取并修改的现象。

消费者线程可看做是一个 while(True) {...} 的程序,它在 main 方法中创建。当没有 task 可消费时,会不停占用 CPU 资源,消耗系统资源,影响生产者线程接收数据。为了解决这个问题,可以使用条件变量 condition_variable。条件变量可以让线程 主动 进入阻塞状态,直到被另一个线程的 notify 相关方法唤醒。

因此当 消费者 线程发现消费队列为空时,主动进入 阻塞 状态,直到生产者进程接收客户端指令,修改消费者队列后再唤醒消费者线程消费 task。因此消费队列可按如下方式设计:

// 使用互斥锁的消费队列
struct MessageQueue {
    queue q;          		// 消费队列
    mutex m;                // 互斥锁
    condition_variable cv;	// 条件变量,用于阻塞所在线程
} message_queue;

为了实现上述描述的生产者消费者模型,需要引入以下库文件:

#include <thread>				//多线程
#include <mutex>				//锁
#include <condition_variable>	//条件变量
#include <queue>
#include <vector>

消费队列保存的是一个个待处理的任务 task,而不是用户列表,因此需要创建一个类 Pool,记录当前匹配池的情况以及定义匹配池的操作。匹配池 Pool 的操作主要包括:添加用户、删除用户、匹配、保存匹配结果。

每次匹配选取用户列表里最靠前的两名用户来匹配,保存匹配记录后,移除这两名用户。

综上所述,可得到如下完整的服务端代码:

match_system/main.cpp

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

#include "match_server/Match.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

#include<iostream>
#include <thread>               // 需要线程,引入头文件
#include <mutex>                // 互斥信号量
#include <condition_variable>   // 条件变量,用于 阻塞和唤醒 线程
#include <queue>                // 用于模拟消息队列
#include <vector>

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

using namespace  ::match_service;
using namespace std;

struct Task {	// 消息队列中的元素
    User user;
    string type;
};

struct MessageQueue {		// 消息队列
    queue<Task> q;			// 消息队列本题
    mutex m;				// 互斥信号量
    condition_variable cv;	// 条件变量
} message_queue;

class Pool {	// 模拟匹配池
    public:
    void savs_result(int a, int b) {			// 记录成功的匹配信息
        printf("Match Result: %d %d\n", a, b);
    }

    void match() {								// 匹配池中前两个用户进行匹配
        while(user.size() > 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) {			// 删除用户
        vector<User>::iterator it;
        for(it = users.begin(); it != users.end(); it ++ ) {
            if(it->id == user.id) {
                users.erase(it);
                break;
            }
        }
    }
    private:
    vector<User> users;
} pool;


class MatchHandler : virtual public MatchIf {
    public:
    MatchHandler() {
        // Your initialization goes here
    }

    /**
   * user: 添加的用户信息
   * info: 附加信息
   * 在匹配池中添加一个名用户
   * 
   * @param user
   * @param info
   */
    int32_t add_user(const User& user, const std::string& info) {
        // Your implementation goes here
        printf("add_user\n");

        unique_lock<mutex> lck(message_queue.m);    // 执行完之后会自动解锁
        message_queue.q.push({user, "add"});
        message_queue.cv.notify_all();			// 唤醒阻塞的线程


        return 0;
    }

    /**
   * user: 删除的用户信息
   * info: 附加信息
   * 从匹配池中删除一名用户
   * 
   * @param user
   * @param info
   */
    int32_t remove_user(const User& user, const std::string& info) {
        // Your implementation goes here
        printf("remove_user\n");

        unique_lock<mutex> lck(message_queue.m);	// 访问临界区(消息队列),先上锁
        message_queue.q.push({user, "remove"});
        message_queue.cv.notify_all();			// 唤醒阻塞的线程

        return 0;
    }
};

void consume_task() {
    while(true) {
        unique_lock<mutex> lck(message_queue.m);		// 访问临界区(消息队列),先上锁

        if(message_queue.q.empty()) {
            // 这里要阻塞进程,直到被其它线程的nofity方法唤醒;避免队列为空时,一直反复运行该线程,导致一直占用临界区,而不能加入新消息
            message_queue.cv.wait(lck);
        }
        else {
            auto task = message_queue.q.front();
            message_queue.q.pop();
            // 临界区访问结束,直接解锁(处理完共享变量后及时解锁);避免后续没用到临界区信息,而长时间占用临界区的情况发生
            lck.unlock();			

            //do task
            if(task.type == "add") pool.add(task.user);
            else if(task.type == "remove") pool.add(task.user);

            pool.match();
        }
    }
}

int main(int argc, char **argv) {
    int port = 9090;
    ::std::shared_ptr<MatchHandler> handler(new MatchHandler());
    ::std::shared_ptr<TProcessor> processor(new MatchProcessor(handler));
    ::std::shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
    ::std::shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

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

    std::cout << "start Match Server" << std::endl;

    thread matching_thread(consume_task);	// 调用一个线程运行 consume_task

    server.serve();
    return 0;
}

由于服务端使用了线程库,因此在链接时,需要加参数 -pthread 链接线程相关库文件。
编译,运行,上传

g++ -c main.cpp
g++ *.o -o main -lthrift -pthread

git add main.cpp
git commit -m "match-server ver:2.0"  #(傻瓜匹配版)
git push

实现数据存储 (数据存储服务器 与 匹配系统服务器 的交互)

thrift 生成 Save 客户端 cpp 代码 (save.thrift)

save.thrift (现成)

namespace cpp save_service;

service Save {
    /**
     * username: myserver的名称
     * password: myserver的密码的md5sum的前8位;终端:md5sum >> password >> enter >> ctrl+d
     * 用户名密码验证成功会返回0,验证失败会返回1
     * 验证成功后,结果会被保存到myserver:homework/lesson_6/result.txt中
     */
    i32 save_data(1: string username, 2: string password, 3: i32 player1_id, 4: i32 player2_id)
    //acs_server的用户名,acs_server的密码的md5哈希值的前八位,匹配用户1,匹配用户2
}

在匹配系统服务器(match_server) 利用 thrift 生成 save.thriftC++ 文件。

删掉 save.thrift 生成的 C++ 文件中不必要 服务端 文件(Save_server.skeleton.cpp),因为在该交互功能里,数据存储服务器 是作为 客户端(save_client)的;而且一个服务器里只能有一个 main 函数执行,因此只保留 save_client 里的 客户端 文件即可。

不同的是,python 即使作为客户端,也可保留服务端的文件(如上文提到的 Match-Remote 文件),可删可不删,而 C++必须删掉服务端文件。因为一个节点(功能)只能由一个 main 方法作为程序的入口,所以匹配系统中的客户端和服务端写在同一个 main 方法中。我们这里根据逻辑将其实现在一个函数中。

thrift5.png

cd match_system/src
thrift -r --gen cpp ../../thrift/save.thrift
mv gen-cpp save_client
rm save_client/Save_server.skeleton.cpp

save_client 端的代码写在 match_server

thrift 官网 复制 Client 端的模板到 main.cpp 下与 数据存储服务器 交互的函数中,然后开始修改模板里的一些参数
match_system/src的main.cpp

① 加入模板中需要,但 main.cpp 没有的头文件

#include <thrift/transport/TTransportUtils.h>
#include <thrift/transport/TSocket.h>

② 引入生成的 Save.h

#include "save_client/Save.h"

③ 添加 save.thrift 定义的命名空间,保证代码正确引用 Save.h 的内容

using namespace ::save_service;

④ 把模板 main 方法里的内容拷贝到 match_system/src/main.cppPool 类的 void save_result(int a, int b) 方法的 printf(...) 后边,并用 gg=G 格式化代码,然后按如下修改

  • 把粘贴代码里 new TSocket("localhost", 9090)localhost 改成第 4 讲配置的 myserverIP
  • CalculatorClient 换成 SaveClient
  • 删除 try 语句块里的 transport->open();transport->close(); 之间的教学语句,然后加入语句 client.save_data("myserver_username", "密码md5前八位", a, b);,注意 myserver_username 指第 4 章配置的 myserver 的用户名,可通过 homework 4 getinfo 查看,例如 acs_1234
    为了防止密码泄露风险,校验采用密码 md5 码的前八位,可通过命令 echo your_password | md5sum | cut -c 1-8 获得,其中 your_password 是你的明文密码。
std::shared_ptr socket(new TSocket("xx.xx.xx.xx", 9090));       // xx.xx.xx.xx为自己myserver的IP
std::shared_ptr transport(new TBufferedTransport(socket));
std::shared_ptr protocol(new TBinaryProtocol(transport));
SaveClient client(protocol);

try {
    transport->open();

    client.save_data("acs_1234", "12345678", a, b);     // 替换换成自己myserver的用户名和密码mk5前八位

    transport->close();
} catch (TException& tx) {
    cout << "ERROR: " << tx.what() << endl;
}

修改后的 main.cpp 代码如下:

#include "match_server/Match.h"
#include "save_client/Save.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/transport/TTransportUtils.h>
#include <thrift/transport/TSocket.h>

#include<iostream>
#include <thread>               // 需要线程,引入头文件
#include <mutex>                // 互斥信号量
#include <condition_variable>   // 条件变量,用于 阻塞和唤醒 线程
#include <queue>                // 用于模拟消息队列
#include <vector>

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

using namespace ::match_service;
using namespace ::save_service;
using namespace std;

struct Task {   // 消息队列中的元素
    User user;
    string type;
};

struct MessageQueue {       // 消息队列
    queue<Task> q;          // 消息队列本题
    mutex m;                // 互斥信号量
    condition_variable cv;  // 条件变量
} message_queue;

class Pool {    // 模拟匹配池
    public:
    void savs_result(int a, int b) {            // 记录成功的匹配信息
        printf("Match Result: %d %d\n", a, b);

        std::shared_ptr socket(new TSocket("123.57.67.128", 9090));
        std::shared_ptr transport(new TBufferedTransport(socket));
        std::shared_ptr protocol(new TBinaryProtocol(transport));
        SaveClient client(protocol);

        try {
            transport->open();
			
            int status = client.save_data("acs_1234", "12345678", a, b);
            
            puts((status ? "fail" : "success"));

            transport->close();
        } catch (TException& tx) {
            cout << "ERROR: " << tx.what() << endl;
        }
    }

    void match() {                              // 匹配池中前两个用户进行匹配
        while(user.size() > 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) {            // 删除用户
        vector<User>::iterator it;
        for(it = users.begin(); it != users.end(); it ++ ) {
            if(it->id == user.id) {
                users.erase(it);
                break;
            }
        }
    }
    private:
    vector<User> users;
} pool;


class MatchHandler : virtual public MatchIf {
    public:
    MatchHandler() {
        // Your initialization goes here
    }

    /**
   * user: 添加的用户信息
   * info: 附加信息
   * 在匹配池中添加一个名用户
   * 
   * @param user
   * @param info
   */
    int32_t add_user(const User& user, const std::string& info) {
        // Your implementation goes here
        printf("add_user\n");

        unique_lock<mutex> lck(message_queue.m);    // 执行完之后会自动解锁
        message_queue.q.push({user, "add"});
        message_queue.cv.notify_all();          // 唤醒阻塞的线程


        return 0;
    }

    /**
   * user: 删除的用户信息
   * info: 附加信息
   * 从匹配池中删除一名用户
   * 
   * @param user
   * @param info
   */
    int32_t remove_user(const User& user, const std::string& info) {
        // Your implementation goes here
        printf("remove_user\n");

        unique_lock<mutex> lck(message_queue.m);    // 访问临界区(消息队列),先上锁
        message_queue.q.push({user, "remove"});
        message_queue.cv.notify_all();          // 唤醒阻塞的线程

        return 0;
    }
};

void consume_task() {
    while(true) {
        unique_lock<mutex> lck(message_queue.m);        // 访问临界区(消息队列),先上锁

        if(message_queue.q.empty()) {
            // 这里要阻塞进程,直到被其它线程的nofity方法唤醒;避免队列为空时,一直反复运行该线程,导致一直占用临界区,而不能加入新消息
            message_queue.cv.wait(lck);
        }
        else {
            auto task = message_queue.q.front();
            message_queue.q.pop();
            // 临界区访问结束,直接解锁(处理完共享变量后及时解锁);避免后续没用到临界区信息,而长时间占用临界区的情况发生
            lck.unlock();           

            //do task
            if(task.type == "add") pool.add(task.user);
            else if(task.type == "remove") pool.add(task.user);

            pool.match();
        }
    }
}

int main(int argc, char **argv) {
    int port = 9090;
    ::std::shared_ptr<MatchHandler> handler(new MatchHandler());
    ::std::shared_ptr<TProcessor> processor(new MatchProcessor(handler));
    ::std::shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
    ::std::shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

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

    std::cout << "start Match Server" << std::endl;

    thread matching_thread(consume_task);   // 调用一个线程运行 consume_task

    server.serve();
    return 0;
}

⑤ 编译及链接代码

g++ -c main.cpp save_client/*.cpp match_server/*.cpp
g++ *.o -o main -lthrift -pthread

⑥ 检验

然后在 tmux 开启两个 bash,分别在 match_system/src 执行 ./maingame/src 执行 python3 client.py

在客户端输入若干指令,观察服务端的匹配情况。

最后登录保存数据的服务器 ssh myserver,查看 ~/homework/lesson_6/result.txt 是否存在,是否有匹配信息。

-------------
# 游戏应用端
add 1 yxc 1500
add 2 zxc 1500
# 匹配系统服务器
add_user
add_user
Match Result: 1 2
success
# 数据存储服务器
## homework/lesson_6/result.txt
result: 1 2
141da30f
-------------

# 上传到 git 服务器
git add main.cpp
git add ../../thrift/save.thrift
git commit -m "implement save-client"
git push

实现匹配系统 ver3.0 功能:每次只匹配分差小于 50 的用户

编写匹配逻辑:修改 match_system/src/main.cpp

修改线程中 消息队列 为空时,不再是 阻塞 直到 唤醒 为止

让其可以每经过 1 秒就进行一次 match() 调用

这样做的目的是:由于我们的匹配策略发生了变化(即每次只匹配分差小于 50 的用户),可能匹配池中仍然有用户在等待匹配(当前匹配池各个用户分差都大于 50),而消息队列此时仍为空

如果仍然采用先前的策略,可能会导致进程卡死(新用户不进去,老用户永远匹配不了)

如果不加入 “随时间扩大匹配域” 的功能,无论改不改上述情况都会可能发生 但是为了后续实现 “随时间扩大匹配域” 功能,先做一个铺垫

(1)改成每 1 秒匹配一次

① 去掉消费者进程方法 consume_task 的阻塞代码,让它解锁后直接匹配,然后休眠 1s

...
//修改 "生产者 - 消费者模型" 的线程中,关于消息队列为空时的处理
if (message_queue.q.empty())
{
    // 此处修改为每 1 秒进行一次匹配,而不是等到被唤醒时才匹配
    lck.unlock();   // 直接解锁临界区资源
    pool.match;     //调用math()
}
...

② 引入 sleep() 需要的头文件

#include <unistd.h>

(2)编写匹配逻辑

首先按分值升序排序,依次检查相邻用户的分值差的绝对值是否小于 50,如果满足立即匹配这两名用户。

修改 Pool 类中 match() 方法的匹配逻辑:

//重写匹配池Pool类中的match函数,使之可以匹配分数差在 50 以内的两个用户
void match()    // 匹配池中的第一、第二个用户进行匹配
{
    while (users.size() > 1)
    {
        // 按照 rank分 排序
        sort(users.begin(), users.end(), [&](User &a, User &b){
            return a.score < b.score;
        });
        bool flag = true;
        for (uint32_t i = 1; i < users.size(); i ++ )
        {
            User a = users[i - 1], b = users[i];
            // 两名玩家分数差小于50时进行匹配
            if (b.score - a.score <= 50)
            {
                users.erase(users.begin() + i - 1, users.begin() + i); //删掉用户a,b
                save_result(a.id, b.id);

                flag = false;
                break;
            }
        }
        if (flag) break;    // 一轮扫描后,发现没有能够匹配的用户,就停止扫描,等待下次调用
    }
}

(3)验证匹配逻辑

g++ -c main.cpp
g++ *.o -o main -lthrift -pthread
./main

-------------
# 游戏应用端
add 1 yxc 2000
add 2 yume 1500
add 3 minami 1549
# 匹配系统服务器
add_user
add_user    # 此处用户 yxc 与用户 yume 没有匹配,因为分差超过50
add_user
Match Result: 2 3   # 用户 yume 与用户 minami 分差在 50 以内,进行匹配
success
# 数据存储服务器
## homework/lesson_6/result.txt
result: 2 3
b6281235
-------------

# 上传到 git 服务器
git add main.cpp
git add ../../thrift/save.thrift
git commit -m "match server:3.0"
git push

实现匹配系统 ver4.0 功能:多线程并发

我们之前的是 TSimpleServer 是单线程的,效率比较慢,我们接下来将它升级为多线程 TThreadedServer
参考官网 C++服务端 模板,修改 match_system/src/main.cpp

(1)添加缺少的头文件

#include <thrift/concurrency/ThreadManager.h>
#include <thrift/concurrency/ThreadFactory.h>
#include <thrift/server/TThreadedServer.h>
#include <thrift/TToString.h>

(2)替换掉 main() 方法里的服务器构建过程

TThreadedServer server(
    std::make_shared(std::make_shared()),
    std::make_shared(9090), //port
    std::make_shared(),
    std::make_shared()
);

(3)复制工厂代码到 main 方法上边,注释掉输出信息

/*
  CalculatorIfFactory is code generated.
  CalculatorCloneFactory is useful for getting access to the server side of the
  transport.  It is also useful for making per-connection state.  Without this
  CloneFactory, all connections will end up sharing the same handler instance.
*/
class CalculatorCloneFactory : virtual public CalculatorIfFactory {
    public:
    ~CalculatorCloneFactory() override = default;
    CalculatorIf* getHandler(const ::apache::thrift::TConnectionInfo& connInfo) override
    {
        std::shared_ptr sock = std::dynamic_pointer_cast(connInfo.transport);
        // cout << "Incoming connection\n";
        // cout << "\tSocketInfo: "  << sock->getSocketInfo() << "\n";
        // cout << "\tPeerHost: "    << sock->getPeerHost() << "\n";
        // cout << "\tPeerAddress: " << sock->getPeerAddress() << "\n";
        // cout << "\tPeerPort: "    << sock->getPeerPort() << "\n";
        return new CalculatorHandler;
    }
    void releaseHandler( ::shared::SharedServiceIf* handler) override {
        delete handler;
    }
};

Calculator 替换成 Match 命令:

:1,$s/Calculator/Match/g

(4)修改参数 releaseHandler

void releaseHandler( MatchIf* handler) override {
    delete handler;
}

整合一下得到:

// 引入新的多线程头文件
#include <thrift/concurrency/ThreadManager.h>
#include <thrift/concurrency/ThreadFactory.h>
#include <thrift/server/TThreadPoolServer.h>
#include <thrift/server/TThreadedServer.h>
#include <thrift/TToString.h>

// 复制模板的类 CalculatorCloneFactory 然后改一改,把所有的 Calculator 改为 Match
class MatchCloneFactory : virtual public MatchIfFactory {
    public:
        ~MatchCloneFactory() override = default;
        MatchIf* getHandler(const ::apache::thrift::TConnectionInfo& connInfo) override
        {
            std::shared_ptr sock = std::dynamic_pointer_cast(connInfo.transport);
            /*
            cout << "Incoming connection\n";
            cout << "\tSocketInfo: "  << sock->getSocketInfo() << "\n";
            cout << "\tPeerHost: "    << sock->getPeerHost() << "\n";
            cout << "\tPeerAddress: " << sock->getPeerAddress() << "\n";
            cout << "\tPeerPort: "    << sock->getPeerPort() << "\n";
            */
            return new MatchHandler;
        }
        void releaseHandler(MatchIf* handler) override {    //改为MatchIf*  
            delete handler;
        }
};
// 重写main函数,启用多线程服务器
int main(int argc, char **argv) {
    TThreadedServer server(
            std::make_shared(std::make_shared()),
            std::make_shared(9090), //port
            std::make_shared(),
            std::make_shared());

    cout << "Start Match Server" << endl;

    thread matching_thread(consume_task);   // 调用一个线程运行 consume_task

    server.serve();

    return 0;
}

实现匹配系统 ver5.0 功能:随时间扩大匹配域

思想

如果匹配池有两个人不满足分值差不超过 50,按之前的逻辑这两个人永远不会被匹配,但这样体验不好,因此引入动态匹配。每个人允许的分值差是动态变化的,它等于 \(等待时间 * 50\)。如果两个人的分值差都在各自允许的分值差范围内,则匹配这两人。

例如甲的分值为 1000 分,乙为 1500 分。甲已经等待了 11 秒,其允许分值差为 550 分;乙等待了 9 秒,其运输分值差为 450 分。尽管二者分数差在甲当前的容忍范围内,但不在乙的容忍范围内,因此不匹配。再过 1 秒后,时间差也在乙的容忍范围内了,可以匹配了。

实现

(1)修 改match_system/src/main.cppPool

① 引入等待时间成员变量 vector wt;

② 让 add_user()remove_user() 支持 wt 的添加与删除

③ 修改 match() 方法并引入 check_match() 方法

得到的 Pool 类如下:

class Pool {
    public:
        void save_result(int a, int b) {
            printf("Match Result: %d %d\n", a, b);

            std::shared_ptr socket(new TSocket("123.57.47.211", 9090));
            std::shared_ptr transport(new TBufferedTransport(socket));
            std::shared_ptr protocol(new TBinaryProtocol(transport));
            SaveClient client(protocol);

            try {
                transport->open();

                client.save_data("acs_3929", "6df6b19d", a, b);

                transport->close();
            } catch (TException& tx) {
                cout << "ERROR: " << tx.what() << endl;
            }
        }

        bool check_match(uint32_t i, uint32_t j) 
        {
            auto a = users[i], b = users[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;
        }

        void match()
        {
            for (uint32_t i = 0; i < wt.size(); i++)
                wt[i]++;                // 更新等待时间

            while(users.size() > 1)
            {
                bool flag = true;
                for (uint32_t i = 0; i < users.size(); i++)
                {
                    for (uint32_t j = i + 1; j < users.size(); j++) 
                    {
                        if (check_match(i, j))
                        {
                            auto a = users[i], b = users[j];
                            users.erase(users.begin() + j);             // 使用erase删除时,要先删后边的
                            wt.erase(wt.begin() + j);
                            users.erase(users.begin() + i);             // 再删前边的
                            wt.erase(wt.begin() + i);        
                            save_result(a.id, b.id);
                            flag = false;
                            break;
                        }
                    }
                }
                if (flag) break;                // 匹配成功后立即停止,防止进入死循环
            }
        }

        void add(User user) {
            users.push_back(user);
            wt.push_back(0);
        }

        void remove(User user) {
            for (uint32_t i = 0; i < users.size(); i++)
                if (users[i].id == user.id) {
                    users.erase(users.begin() + i);
                    wt.erase(wt.begin() + i);
                    break;
                }
        }
    private:
        vector users;
        vector wt;  // 等待时间

}pool;
(2)删除 consume_task()else 里的 pool.match(),保证先匹配,且等待时间正确。

修改完后的 match_servermain.cpp

// This autogenerated skeleton file illustrates how to build a server.
// You should copy it to another filename to avoid overwriting it.
#include "match_server/Match.h"
#include "save_client/Save.h"
#include <thrift/concurrency/ThreadManager.h>
#include <thrift/concurrency/ThreadFactory.h>
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/server/TThreadedServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>
#include <thrift/transport/TTransportUtils.h>
#include <thrift/transport/TSocket.h>
#include <thrift/TToString.h>

#include <iostream>
#include <thread>               // 需要线程,引入头文件
#include <mutex>                // 互斥信号量
#include <condition_variable>   // 条件变量,用于 阻塞和唤醒 线程
#include <queue>                // 用于模拟消息队列
#include <vector>
#include <unistd.h>             //// 用于调用 sleep 函数


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

using namespace ::match_service;
using namespace ::save_service;
using namespace std;

struct Task {           //消息队列中的元素
    User user;
    string type;
};

struct MessageQueue {   //消息队列
    queue<Task> q;          //消息队列
    mutex m;                //互斥信号量
    condition_variable cv;  //条件变量,用于阻塞唤醒进程
} message_queue;

class Pool {            //模拟匹配池
    public:
        void save_result(int a, int b) {            // 记录成功匹配的信息
            printf("Match Result: %d %d\n", a, b);

            std::shared_ptr<TTransport> socket(new TSocket("123.57.67.128", 9090));
            std::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
            std::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));
            SaveClient client(protocol);

            try {
                transport->open();
                //调用接口,把信息存储“数据存储服务器”中
                int res = client.save_data("acs_13002", "7c0e4f02", a, b);

                puts((res ? "failed" : "success"));

                transport->close();
            } catch (TException& tx) {
                cout << "ERROR: " << tx.what() << endl;
            }

        }

        bool check_match(uint32_t i, uint32_t j) {
            auto a = users[i], b = users[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);
        }

        void match() {      // 将匹配池中前两个用户进行匹配
            for(uint32_t i = 0; i < wt.size(); i ++ ) {
                wt[i] ++;       // 等待秒数 + 1
            }

            while(users.size() > 1) {
                bool flag = true;
                for(uint32_t i = 0; i < users.size(); i ++ ) {
                    for(uint32_t j = i + 1; j < users.size(); j ++ ) {
                        if(!check_match(i, j)) continue;
                        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;
                    }
                    if(!flag) break;
                }
                if(flag) break;    // 一轮扫描后,发现没有能够匹配的用户,就停止扫描,等待下次调用
            }
        }

        void add(User user) {
            users.push_back(user);
            wt.push_back(0);
        }
        void remove(User user) {
            vector<User>::iterator it;
            vector<int>::iterator it2;
            for(it = users.begin(), it2 = wt.begin(); it != users.end(); it ++, it2 ++ ) {
                if(it->id == user.id) {
                    users.erase(it);
                    wt.erase(it2);
                    break;
                }
            }
        }

    private:
        vector<User> users;
        vector<int> wt;     // 等待时间,单位:s
} pool;

// 复制模板的类 CalculatorCloneFactory 然后改一改,把所有的 Calculator 改为 Match
class MatchHandler : virtual public MatchIf {
    public:
        MatchHandler() {
            // Your initialization goes here
        }

        /**
         * user: 添加的用户信息
         * info: 附加信息
         * 在匹配池中添加一个名用户
         * 
         * @param user
         * @param info
         */
        int32_t add_user(const User& user, const std::string& info) {
            // Your implementation goes here
            printf("add_user\n");

            unique_lock<mutex> lck(message_queue.m);    //执行完之后会自动解锁
            message_queue.q.push({user, "add"});
            message_queue.cv.notify_all();              // 唤醒阻塞的线程

            return 0;
        }

        /**
         * user: 删除的用户信息
         * info: 附加信息
         * 从匹配池中删除一名用户
         * 
         * @param user
         * @param info
         */
        int32_t remove_user(const User& user, const std::string& info) {
            // Your implementation goes here
            printf("remove_user\n");

            unique_lock<mutex> lck(message_queue.m);
            message_queue.q.push({user, "remove"});
            message_queue.cv.notify_all();

            return 0;
        }
};

// 复制模板的类 CalculatorCloneFactory 然后改一改,把所有的 Calculator 改为 Match
class MatchCloneFactory : virtual public MatchIfFactory {
    public:
        ~MatchCloneFactory() override = default;
        MatchIf* getHandler(const ::apache::thrift::TConnectionInfo& connInfo) override {
            std::shared_ptr<TSocket> sock = std::dynamic_pointer_cast<TSocket>(connInfo.transport);
            /*cout << "Incoming connection\n";
              cout << "\tSocketInfo: "  << sock->getSocketInfo() << "\n";
              cout << "\tPeerHost: "    << sock->getPeerHost() << "\n";
              cout << "\tPeerAddress: " << sock->getPeerAddress() << "\n";
              cout << "\tPeerPort: "    << sock->getPeerPort() << "\n";*/
            return new MatchHandler;
        }
        void releaseHandler(MatchIf* handler) override {
            delete handler;
        }
};

// 基于 "生产者-消费者模型" 的线程
void consume_task() {
    while(true) {
        unique_lock<mutex> lck(message_queue.m);        // 访问临界区(消息队列),先上锁

        if(message_queue.q.empty()) {
            //message_queue.cv.wait(lck);
            lck.unlock();
            pool.match();
            sleep(1);
        }
        else {
            auto task = message_queue.q.front();
            message_queue.q.pop();
            lck.unlock();           // 临界区访问结束,直接解锁
            // 避免后续没用到临界区信息,而长时间占用临界区的情况发生


            //do task
            if(task.type == "add") pool.add(task.user);
            else if(task.type == "remove") pool.remove(task.user);

        }
    }
}

int main(int argc, char **argv) {
    TThreadedServer server(
            std::make_shared<MatchProcessorFactory>(std::make_shared<MatchCloneFactory>()),
            std::make_shared<TServerSocket>(9090),           //port
            std::make_shared<TBufferedTransportFactory>(),
            std::make_shared<TBinaryProtocolFactory>());


    cout << "Start Match Server" << endl;

    thread matching_thread(consume_task);    // 调用一个线程运行 consume_task

    server.serve();
    return 0;
}

编译,运行, 验证,上传

g++ -c main.cpp
g++ *.o -o main -lthrift -pthread
./main

-------------
# 游戏应用端
add 1 yxc 2000
add 2 zxc 1500
# 匹配系统服务器
add_user
add_user
# 等待了 10 s
Match Result: 1 2
success
# 数据存储服务器
## homework/lesson_6/result.txt
result: 1 2
141da30f
-------------

# 上传到 git 服务器
git add main.cpp
git commit -m "match server:5.0"
git push

完整框架

.
|-- game
|   `-- src
|       |-- client.py
|       `-- match_client
|           |-- __init__.py
|           |-- __pycache__
|           |   `-- __init__.cpython-38.pyc
|           `-- match
|               |-- Match.py
|               |-- __init__.py
|               |-- __pycache__
|               |   |-- Match.cpython-38.pyc
|               |   |-- __init__.cpython-38.pyc
|               |   `-- ttypes.cpython-38.pyc
|               |-- constants.py
|               `-- ttypes.py
|-- match_system
|   `-- src
|       |-- Match.o
|       |-- Save.o
|       |-- main
|       |-- main.cpp
|       |-- main.o
|       |-- match_server
|       |   |-- Match.cpp
|       |   |-- Match.h
|       |   |-- match_types.cpp
|       |   `-- match_types.h
|       |-- match_types.o
|       `-- save_client
|           |-- Save.cpp
|           |-- Save.h
|           `-- save_types.h
|-- readme.md
`-- thrift
    |-- match.thrift
    `-- save.thrift
posted @ 2026-01-15 22:12  鱼香_rose  阅读(4)  评论(0)    收藏  举报