由laruence 于2020年4月1日发布
本文地址:https ://www.laruence.com/2020/04/01/5726.html
Yar支持HTTP和TCP俩种Transporter,HTTP的是基于CURL,PHP中的Yar就是走的HTTP Transporter,这个大家应该都不陌生,但是基于TCP的,可能大家会用的少一些。
实际上,我6年前也写过一个C的Yar服务器框架,叫做Garthub上的Yar-c,代码地址在Yar-C,它提供了服务启动,worker进程管理,Yar打包协议等。框架,实现了高效的微博白名单等服务,以供PHP端使用Yare Client来调用。
只不过,Yar C需要用C来写Handle,可能对于多余的PHPer来说,会有点有点陌生,那今天我们尝试用PHP来写一个TCP的Server,来介绍下如何实现对Yar RPC协议的处理,这个例子可以方便的结合Swoole等异步PHP框架,实现一个高性能的亚尔TCP服务器。这个过程中,会让大家了解亚尔的RPC通信协议,以及捎带了解下的Socket编程。
我们今天还是用“白名单”服务作为例子,我们提供一个接口,接受RPC客户端的请求,参数是一个用户ID,返回bool,表示是否在白名单:
函数 查询(int $ id ) : bool ;
首先,我们建立一个文件yar_server,为了方便的直接执行,我们在文件写道:
#!/ bin / env php7
<?php
类 WhiteList {
}
然后,通过chmod a + x给这个文件增加重新的权限。
第一步我们需要处理服务的启动参数处理,接受一个参数S表示要监听的IP和端口,值的格式是host:port,我们使用PHP的getopt函数来处理命令行参数:
类 WhiteList {
受保护的 $ host;
公共 功能 __construct () {
$ options = getOpt (“ S:” ) ;
if (!isset ($ options [ “ S” ])) {
$ this- > 用法() ;
}
}
受保护的 功能 用法() {
exit (“用法:yar_server -S主机名:端口 n” );
}
}
这样,当用户启动yar_server的时候,没有指定S参数,我们就退出,并提示用法。我们还需要另外一个配置,就是指向一个词表文件,词表文件中每一行是一个在白名单中的用户ID,我们用F表示:
类 WhiteList {
受保护的 $ host;
受保护的 字典 ;
公共 功能 __construct () {
$ options = getOpt (“ S:F:” ) ;
if (!isset ($ options [ “ S” ]) || !!setset ($ options [ “ F” ])) {
$ this- > 用法() ;
}
$ this- > host = $ options [ “ S” ] ;
$ this- > dicts = $ options [ “ F” ] ;
}
受保护的 功能 用法() {
exit (“用法:yar_server -F path_to_dict -S主机名:端口 n” );
}
}
好了,现在启动参数处理完成,当然为了简单,我省去了对输入参数的有效检查。
接下来,我们需要完成俩个函数,第一个是读取-F指定的词表文件,把所有的用户ID读入到一个数组中,因为我们的这个服务会是常驻进行,所以不用担心性能,它只会在启动阶段处理这个词表文件:
受保护的 函数 loadDict () {
$ this- > ids = array () ;
$ fp = fopen ($ this- > dicts,“ r” ) ;
while (!feof ($ fp )) {
$ line = trim (fgets ($ fp )) ;
如果 ($ line ) {
$ this- > ids [ $ line ] = true ;
}
}
fclose ($ fp ) ;
回声 “成功加载字典,”,计数($ this- > ids ),“已加载 n”;
返回 $ this ;
}
因为用户ID是整型,所以我们把它当作Hashtable的键,这样在将来查找的时候,使用isset会非常高效。需要注意的是因为文件处理不是我们今天要讲的重点,也就省去了对文件存在行,意图性,合法性的检查。
好了,接下来是重点了,我们要启动一个IPV4 TCP Socket服务,监听在$ host指定的地方,为了方便大家了解Socket API,我们不采用PHP的Stream系列函数,而是采用PHP直接包装的Socket系列API,首先我们用socket_create创建一个套接字专有:
受保护的 函数 listen () {
$ socket = socket_create ( AF_INET,SOCK_STREAM,SOL_TCP ) ;
如果 ($ socket == false ) {
抛出 新 异常(“ socket_create()失败:原因:” 。 socket_strerror (socket_last_error ())) ;
}
}
然后,我们需要使用socket_bind绑定这个套接字到我们需要监听的地址,并使用socket_listen来监听请求:
受保护的 函数 listen () {
$ socket = socket_create ( AF_INET,SOCK_STREAM,SOL_TCP ) ;
如果 ($ socket == false ) {
抛出 新 异常(“ socket_create()失败:原因:” 。 socket_strerror (socket_last_error ())) ;
}
list ($ hostname,$ port ) = 爆炸(“:”,$ this- > host ) ;
如果 (socket_bind ($ socket,$ hostname,$ port ) == false ) {
抛出 新的 异常(“ socket_bind()失败:原因:” 。 socket_strerror (socket_last_error ()));
}
如果 (socket_listen ($ socket,64 ) === false ) {
抛出 新的 异常(“ socket_listen()失败:原因:” 。 socket_strerror (socket_last_error ()));
}
echo “在{$ this-> host}处启动Yar_Server n按Ctrl + C退出 n”;
$ this- > socket = $ socket ;
返回 $ this ;
}
好了,如果一切没问题,接下来我们就可以socket_accept来监听请求了,默认的socket是分段模式,如果没有请求,进程会一直一直等待,对于高效的服务来说,最好采用非捆绑+ select或者epoll的模式来同时处理多个请求,但是我们的这个示例主要是为了介绍Yar的协议,所以还是采用简单的分段模式。
接下来,我们来编写真正的RPC处理部分,首先我们通过accept接受一个请求,然后读取请求的内容,分析请求头中的Yar RPC Header信息,Yar RPC的协议头定义如下:
typedef struct _yar_header {
uint32_t id; //交易编号
uint16_t 版本; //协议版本
uint32_t magic_num; //默认值为:0x80DFEC60
uint32_t 保留;
未签名的 char 提供程序[ 32 ]; //要求谁
未签名的 char 令牌[ 32 ]; //请求令牌,用于身份验证
uint32_t body_len; //请求正文len
}
其中,magic_num是为验证验证有效的一个特殊值,合法的Yar RPC请求都会设置这个变量0x80DFEC60(我很想告诉你为啥是这个值,但我真不记得当时我为啥用这个数字了),这个头部是82个字节,可能有同学会问,不对啊一看这个Struct不应该是82啊,那是因为头部申明的时候采用pack模式,也就是不对齐,所以确实是82个字节。
包装(“ H *”,“ 80DFEC60” ) ;
provider是一个字符串,标明了客户端的名字,例如对于Yar扩展的Yar_Client就是“ Yar PHP Cient-xxx”
token在设计的最初是为了做API key验证的,但是后来没用上,因为大部分都是内网应用,可以有多种方法来保证请求来源的合法性。
id是一个唯一请求id,这个是为了排查请求问题的版本,版本为0,或者1,目前我没有升级过协议头,所以这个暂时我们也不用关心,保留的可以使用传递一些请求参数,例如客户端可以说明是否保持连接。
body_len是我们需要关心的,这个指出表明了这次请求,请求体一共多大(不包括Yar协议标题)。
所有的这些数字,都是以网络字节序传递的,我们采用PHP处理二进制流的unpack函数来解析读取进来的二进制流:
受保护的 函数 parseHeader ($ header ) {
返回
解压(“ Nid / nversion / Nmagic_num / Nreserved / A32provider / A32token / Nbody_len”,$ header ) ;
}
这个函数会返回一个上面说到的头部结构体的层叠。
对应的我们也需要使用pack来实现生成Yar Header的方法:
const YAR_MAGIC_NUM = 0x80DFEC60 ;
受保护的 函数 genHeader ($ id,$ len ) {
$ bin = pack (“ NnNNA32A32N”,$ id,0,self :: YAR_MAGIC_NUM,0,“ Yar PHP TCP Server”,“”,$ len ) ;
返回 $ bin ;
}
如刚才说的,我们需要在接受一个请求以前,验证请求的合法性:
受保护的 函数 validRequest ($ header ) {
if ($ header [ “ magic_num” ] != self :: YAR_MAGIC_NUM ) {
返回 false ;
}
返回 true ;
}
所以大概请求的处理整个逻辑框架是:
受保护的 函数 accept () {
while ((($ conn = socket_accept ($ this- > socket ))) {
$ buf = socket_read ($ conn,self :: HEADER_SIZE,PHP_BINARY_READ ) ; 复制代码
如果 ($ buf === false ) {
socket_shutdown ($ conn ) ;
继续 ;
}
if (!$ this- > validHeader ($ header = $ this- > parseHeader ($ buf ))) {
$ output = $ this- > response (1,“非法Yar RPC请求” );
转到响应;
}
$ buf = socket_read ($ conn,$ header [ “ body_len” ],PHP_BINARY_READ ) ;
如果 ($ buf === false ) {
$ output = $ this- > response (1,“请求主体不足” );
转到响应;
}
if (!$ this- > validPackager ($ buf )) {
$ output = $ this- > response (1,“不受支持的打包程序” );
转到响应;
}
$ buf = substr ($ buf,8 ) ; / *跳过打包信息的8个字节* /
$ request = $ this- > parseRequest ($ buf ) ;
如果 ($ request == false ) {
$ this- > response (1,“格式错误的请求正文” );
转到响应;
}
$状态 = $这- > 手柄($请求,$ RET ) ;
$输出 = $这- > 响应($状态,$ RET ) ;
回应:
socket_write ($ conn,$ output,strlen ($ output )) ; 复制代码
socket_shutdown ($ conn ) ; / *关闭写* /
}
}
现在整体的框架就算完成了,我们需要完成handle,response方法就可以了,handle是要根据用户的请求中的m,来调用指定的方法
保护 功能 句柄($请求,&$ RET ) {
if ($ request [ “ m” ] == “ query” ) {
$ RET = $这- > 查询(... $请求[ “P” ]) ;
} 其他 {
$ ret = “不支持的方法'” 。 $ request [ “ m” ]。 “'”;
返回 1 ;
}
返回 0 ;
}
现在来实现query方法本身,这个会很简单,就检查下id是不是在白名单数组:
受保护的 函数 查询($ id ) {
返回 isset ($ this- > ids [ $ id ]) ;
}
好了,接下来我们要完成response方法,这个方法是打包一个符合Yar协议的返回体,包括82个字节的头部,8个字节的打包信息,以及序列化后的响应体,我们需要根据状态不同,来选择设置响应体中的r还是e细分:
受保护的 函数 响应($ status,$ ret ) {
$ body = array () ;
$ body [ “ i” ] = 0 ;
$ body [ “ s” ] = $ status ;
如果 ($ status == 0 ) {
$ body [ “ r” ] = $ ret ;
} 其他 {
$ body [ “ e” ] = $ ret ;
}
$ packed = 序列化($ body ) ;
$ header = $ this- > genHeader (0,strlen ($ packed ) + 8 ) ; 复制代码
返回 $ header 。 str_pad (“ PHP”,8,“ 0” ) 。 $包装 ;
}
好了,马上就要大功告成,我们最后完成启动方法和析构函数(关闭套接字):
公共 功能 运行() {
$ this- > loadDict ()-> 听()-> accept () ;
}
公共 功能 __destruct () {
如果 ($ this- > socket ) {
socket_close ($ this- > socket ) ; 复制代码
}
}
现在一切就绪,我们最后在文件末尾加入:
(新的白名单)-> 运行() ;
在测试之前,我们先准备一个测试词表,例如1到1000的id:
seq 1,1,10000> user_id.dict
然后启动服务,监听在本机的9000端口:
$ ./yar_server -F user_id.dict -S127.0.0.1:9000
成功加载dict,已加载1000
在127.0.0.1:9000启动Yar_Server
按Ctrl + C退出
不错,服务启动成功,然后我们使用Yar扩展来编写客户端(你需要首先安装好Yar扩展),测试下用户id 999和99999的调用效果:
<?php
$ yar = 新的 Yar_Client (“ tcp://127.0.0.1:9000” ) ;
var_dump ($ yar- > query (“ 999” )) ;
var_dump ($ yar- > 查询(“ 99999” )) ;
?>
和调用HTTP的Yar服务不同,此处我们应该使用tcp://做地址头,表示这是一个TCP的服务。
来,运行一下看看:
php7 client.php
布尔值(true)
布尔值(false)
你也可以尝试故意构造一些错误的可能,称为调用不存在的方法之类的,来看看服务器的反应,这个例子的代码你可以在这里找到。
到这里我就算介绍完了如何采用PHP来编写Yar的TCP服务,大家应该可以很方便的把这个例子修改完善成自己希望的格式,或者嵌入Swoole(可以参考Swoole作者写的:这里)。
还是要再次说明,因为本文的主要目的是为了介绍RPC通讯协议,所以在服务管理这块并没有做的很完善,套接字接受,套接字读/写等都被采用了某种模式,也没有加入超时设计,服务进程也只有一个,这个如果真的想用做实际服务的话,还是需要一些功课的,不过我相信你有兴趣的话,都是可以搞定的。:)
当然,最简单的是,你可以直接使用Yar-C服务框架来编写C Yar TCP服务。
在这里也有一个亚尔-C服务器的例子yar_server用C。
以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要戳下方
PHP进阶架构师>>>视频、面试文档免费获取docs.qq.com点击观看上方视频或者关注咱们下面的知乎专栏
PHP大神进阶zhuanlan.zhihu.com