Linux基础(六)thrift

 
notion image

第六讲 thrift

我们写一个应用时,这个应用程序并不止一个服务,而且不同的服务分配到不同服务器(或者进程)上,也就是我们常说的微服务

简介

thrift官网 官网教程:进入官网->Tutorial->tutorial.thrift
thrift是一个rpc框架,rpc框架是指远程函数(进程)调用,服务器之间用thrift来通信
Apache Thrift软件框架用于可伸缩的跨语言服务开发,它将软件栈代码生成引擎结合在一起,以构建在C++、Java、Python、PHP、Ruby、Erlang、Perl、Haskell、C#、Cocoa、JavaScript、Node.js、Smalltalk、OCaml和Delphi等语言之间高效、无缝地工作的服务。
Thrift使用C++进行编写,在安装使用的时候需要安装依赖,windows安装方式见官网即可。安装方式:thrift官网介绍安装方式

Thrift IDL

Thrift 采用IDL(Interface Definition Language)来定义通用的服务接口,然后通过Thrift提供的编译器,可以将服务接口编译成不同语言编写的代码,通过这个方式来实现跨语言的功能。
  • 通过命令调用Thrift提供的编译器将服务接口编译成不同语言编写的代码。
  • 这些代码又分为服务端和客户端,将所在不同进程(或服务器)的功能连接起来。

如何创建一个Thrift服务?

  1. 定义服务接口(存放接口的文件夹就是thrift文件)
  1. 作为服务端的服务,需要生成server。
  1. 作为请求端的服务,需要生成client。
 
接下来写个栗子来熟悉thrift的使用

一个游戏的匹配服务分析

一般情况如图所示
notion image
分析图示内容 这个游戏的功能可能运行在一个或多个服务器(或进程)上,而thrift就是将不同服务器不同语言的功能连接起来。 图中的三个节点(功能)是完全独立的,既可以在同一个服务器上,也可以在不同服务器上。 每一个节点就是一个进程,每个进程可以使用不同的语言来实现。
  • 在GAME节点上实现客户端通过调用匹配系统的服务端中实现的两个服务接口函数获取功能,实现跨语言跨服务的工作。
  • 每个节点(功能)之间通过thrift定义的服务接口作为有向边进行连接。 弧尾所在的节点创建客户端,弧头所在的节点创建服务端。
  • 匹配系统节点实现服务端,其中有一个匹配池:不断的接收玩家和删除玩家,同时根据一定的规则给每个玩家安排一局游戏。
  • 匹配系统节点实现客户端,通过调用数据存储节点的服务端中实现的一个服务接口函数获取功能,实现跨语言跨服务的工作。
  • 每个功能(节点)之间通过thrift定义的服务接口作为有向边进行连接。 弧尾所在的节点创建客户端,弧头所在的节点创建服务端。
  • 数据存储节点实现服务端。别人已经将服务接口和服务端实现好了。
  • 服务接口功能介绍: add_user:向匹配池中添加玩家。 remove_user:从匹配池中删除玩家。 save_data:将匹配信息存储起来。
补充
  • 有向边也称弧,边的始点称为弧尾,终点称为弧头。
  • 当做项目时,可能有人已经将服务接口实现好了,即将服务端实现了,我们只需要创建客户端即可。

分析总结

在实现服务之前,最好先画个图分析,这样目标明确、思路清晰。
图中的要素
  1. 不同服务作为节点
  1. 每个服务是在哪个服务器上实现的
  1. 每个服务通过什么语言实现
  1. 服务之间通过怎样的服务接口进行连接。
  1. 通过业务逻辑确认每个服务需要创建哪些服务端和客户端。

实现一个游戏的匹配服务

这里为了方便我们需要创建两个文件夹表示game节点(game)和匹配服务节点(match_system),其实也可以放在不同的服务器上,不过条件不允许啊,而数据存储节点的服务端已经实现好了,只要调用服务接口实现的函数即可。
接下来创建一个thrift文件夹存储.thrift文件,.thrift文件定义服务接口。其中有两个.thrift文件分别表示两条有向边,一条有向边可以包含多个服务接口。
先定义服务接口。 定义添加玩家和删除玩家的两个接口。 在thrift文件夹中,创建match.thrift文件。然后进行接下来的内容。

1. 名字空间NameSpace

Thrift中的命名空间类似于C++中的namespace和java中的package,它们提供了一种组织(隔离)代码的简便方式。名字空间也可以用于解决类型定义中的名字冲突。
由于每种语言均有自己的命名空间定义方式(如:python中有module), thrift允许开发者针对特定语言定义namespace。
简单的demo:
转换成:
教程中的介绍:

2. 结构体struct

数据类型在结构体中定义。 struct有以下一些约束:
  1. struct不能继承,但是可以嵌套,不能嵌套自己。(0.12.0版本可以支持嵌套自己本身)
  1. 其成员都是有明确类型
  1. 成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用。
  1. 成员分割符可以是逗号(,)或是分号(;),而且可以混用
  1. 字段会有optional和required之分和protobuf一样,但是如果不指定则为无类型–可以不填充该值,但是在序列化传输的时候也会序列化进去,optional是不填充则部序列化,required是必须填充也必须序列化。
  1. 每个字段可以设置默认值
  1. 同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。
教程中介绍:
规则:
  • 如果required标识的域没有赋值,Thrift将给予提示;
  • 如果optional标识的域没有赋值,该域将不会被序列化传输;
  • 如果某个optional标识域有缺省值而用户没有重新赋值,则该域的值一直为缺省值;
  • 如果某个optional标识域有缺省值或者用户已经重新赋值,而不设置它的__isset为true,也不会被序列化传输。

3. 服务定义类型Service

服务的定义方法在语义上等同于面向对象语言中的接口。比如java中的接口,只是参数需要加上编号。
教程中介绍:

4. 定义服务接口之后,先实现match-server

如何通过接口生成C++版本的服务端?
具体操作如图所示:
notion image
  • 做到这里我们发现:有了接口之后,通过命令就可以生成C++版本的服务端相关的代码。但是具体业务我们还是需要具体写哦!

5. 如何编译thrift生成的c++代码

因为ThriftC++编写,所以编译的过程与C++一样。 本质就是编译一堆C++源文件(.cpp)。 $\color{green}{C++编译过程:}$ $\color{red}{①编译}$ $\color{red}{②链接}$
  • 好习惯:可执行文件和编译好的文件最好不要加进去,只加.cpp和.h文件。
C++编译很慢,链接很快。所以每次修改项目,重新编译时,只需要编译修改过的.cpp文件即可,防止编译时间过长。 即修改哪个文件就编译哪个文件。 基于这一点考虑就有了make和cmake工具。但没啥用。
至此,game的client端就完成了。最后将其持久化作为最终版。
完善server端需要并利用C++实现多线程。视频定位:55:30
项目代码中有注解。
编译C++时,如果你用到了线程,需要加上线程的动态链接库的参数-pthread-lthrift参数将所有thrift动态连接文件连接起来。
因为一个节点(功能)只能由一个main方法作为程序的入口,所以匹配系统中的客户端和服务端写在同一个main方法中。我们这里根据逻辑将其实现在一个函数中。
  • 注意:复制教程中的代码时,一些细节需要更改。例如:其中带Calculator的字段都需要更改。
  • 一些thrift接口和服务端有人会帮我们实现,这时我们只需调用接口实现的函数即可。例如:这里使用已经实现好的save_data()接口。
匹配机制:等待时间越长,阈值越大。即匹配的范围随时间的推移而变大 故需要记录当前玩家在匹配池中等待的秒数。
至此,游戏的匹配服务就全部完成了。

知识点:

  • C 语言中 include <>include "" 的区别?
#include < >:引用的是编译器的类库路径里面的头文件。 #include " ":引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件。
C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。 由于namespace的概念,使用C++标准程序库的任何标识符时,可以有三种选择,具体看百度百科。 但是如果加上这段代码,就不用前缀。 比如: std::cout << std::endl;加上using namespace std后,就可以写成这样cout << endlstd::ios::sync_with_stdio(false);加上using namespace std后,就可以写成这样ios::sync_with_stdio(false);。(想了解ios::sync_with_stdio(false); 用法开这篇文章)

localhost,127.0.0.1 和 本机IP 三者的区别

如果把IP地址比作一间房子 ,端口就是出入这间房子的门。真正的房子只有几个门,但是一个IP地址的端口可以有65536(即:2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535(2^16-1)。 同一个端口只能由一个进程来监听。所以我们一旦启动了一个服务,那么这个服务就不能在被另一个进程启动了。 服务器的端口号要与客户端的端口号相同。
💡Tips:
每执行一个程序就是开了一个进程。每一个进程可以开一堆线程。 开多线程的开销是很小的,开多进程的开销是很大的。

#include <thread>

C++中有一个thread的库,可以用来开线程。 通过定义一个变量将函数名作为参数,就能开一个线程了。 首先定义线程的操作。 并行中经典的生产者和消费者模型。 生产者、消费者是两个线程。 生产者:add_user()、remove_user() 消费者:匹配用户的功能。 生产者和消费者之间需要一个媒介。 这个媒介可以有很多种方法。比如:消费队列。 很多语言都有自己实现的消费队列,也可以自己实现消费队列。 实现消费队列,就需要用到一些锁(mutex)。 并行编程的基本概念:锁。

互斥锁

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
锁有两个操作。一个P操作(上锁),一个V操作(解锁)。 定义互斥锁:mutex m; 锁一般使用信号量来实现的,mutex其实就是一个信号量(它特殊也叫互斥量)。互斥量就是同一时间能够分给一个人,即S=1。 信号量S:S=10表示可以将信号量分给10个人来用。
P操作的主要动作是: ①S减1; ②若S减1后仍大于或等于0,则进程继续执行; ③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。 V操作的主要动作是: ①S加1; ②若相加后结果大于0,则进程继续执行; ③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。
对于P和V都是原子操作,就是在执行P和V操作时,不会被插队。从而实现对共享变量操作的原子性。 特殊:S=1表示互斥量,表示同一时间,信号量只能分配给一个线程。
多线程为啥要用锁? 因为多线程可能共享一个内存空间,导致出现重复读取并修改的现象。

C++中类的定义

vector的size()是无符号整数类型。

求一个字符串的md5值。

输入命令"md5sum",输入字符串回车然后Ctrl + d就行了。 md5加密

英语知识

  • IDL:接口定义语言(Interface Definition Language)
  • RPC:远程过程调用(Remote Procedure Call)
  • client:客户,客户端
Loading...
目录