Yar支持HTTP和TCP俩种Transporter, HTTP的是基于CURL,PHP中的Yar默认就是走的HTTP Transporter, 这个大家应该都不陌生, 但是基于TCP的, 可能大家会用的少一些。

事实上,我6年前也写过一个C的Yar server框架,叫做Yar-c, 代码地址在Yar-C at Github, 它提供了服务启动,worker进程管理,Yar打包协议等。当时我们用这个框架,实现了高性能的微博白名单等服务,以供PHP端使用Yar Client来调用。

只不过,Yar C需要用C来写Handle, 可能对于不少PHPer来说,会稍微有点陌生,那今天我们尝试用PHP来写一个TCP的Server,来介绍下如何实现对Yar RPC协议的处理, 这个例子可以方便的结合Swoole等异步PHP框架,实现一个高性能的Yar TCP Server。 这个过程中, 会让大家了解Yar的RPC通信协议,以及捎带了解下Socket编程。

我们今天还是用“白名单”服务作为例子,我们提供一个接口,接受RPC客户端的请求,参数是一个用户ID,返回bool,表示是否在白名单:

function query(int $id) : bool;

首先,我们建立一个文件yar_server, 为了方便的直接执行,我们在文件写下:

#!/bin/env php7

class WhiteList {

}

然后,通过chmod a+x 给这个文件增加可执行的权限。

第一步我们需要处理服务的启动参数处理, 接受一个参数S表示要监听的IP和端口,值的格式是host:port, 我们使用PHP的getopt函数来处理命令行参数:

class WhiteList {

protected $host;

public function __construct() {

$options = getOpt("S:");

if (!isset($options["S"])) {

$this->usage();

}

}

protected function usage() {

exit("Usage: yar_server -S hostname:port\n");

}

}

这样,当用户启动yar_server的时候,没有指定S参数,我们就退出,并提示Usage。 我们还需要另外一个配置,就是指向一个词表文件,词表文件中每一行是一个在白名单中的用户ID, 我们用F表示:

class WhiteList {

protected $host;

protected $dicts;

public function __construct() {

$options = getOpt("S:F:");

if (!isset($options["S"]) || !isset($options["F"])) {

$this->usage();

}

$this->host = $options["S"];

$this->dicts = $options["F"];

}

protected function usage() {

exit("Usage: yar_server -F path_to_dict -S hostname:port\n");

}

}

好了, 现在启动参数处理完成, 当然为了简单,我省去了对输入参数的有效性检查。

接下来, 我们需要完成俩个函数, 第一个是读取-F指定的词表文件,把所有的用户ID读入到一个数组中,因为我们的这个服务会是常驻进行, 所以不用担心性能, 它只会在启动阶段处理这个词表文件:

protected function loadDict() {

$this->ids = array();

$fp = fopen($this->dicts, "r");

while (!feof($fp)) {

$line = trim(fgets($fp));

if ($line) {

$this->ids[$line] = true;

}

}

fclose($fp);

echo "Loading dict successfully, ", count($this->ids), " loaded\n";

return $this;

}

因为用户ID是整型,所以我们把它当作Hashtable的key,这样在将来查找的时候,使用isset会非常高效。 需要注意的是因为文件处理不是我们今天要讲的重点,也就省去了对文件存在行,可读性,合法性的检查。

好了, 接下来是重点了, 我们要启动一个IPV4 TCP Socket服务,监听在$host指定的地方, 为了方便大家了解Socket API,我们不采用PHP的Stream系列函数,而是采用PHP直接包装的Socket系列API, 首先我们用socket_create创建一个Socket套接字:

protected function listen() {

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if ($socket == false) {

throw new Exception("socket_create() failed: reason: " . socket_strerror(socket_last_error()));

}

}

然后,我们需要使用socket_bind绑定这个Socket到我们需要监听的地址, 并且使用socket_listen来监听请求:

protected function listen() {

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if ($socket == false) {

throw new Exception("socket_create() failed: reason: " . socket_strerror(socket_last_error()));

}

list($hostname, $port) = explode(":", $this->host);

if (socket_bind($socket, $hostname, $port) == false) {

throw new Exception("socket_bind() failed: reason: " . socket_strerror(socket_last_error()));

}

if (socket_listen($socket, 64) === false) {

throw new Exception("socket_listen() failed: reason: " . socket_strerror(socket_last_error()));

}

echo "Starting Yar_Server at {$this->host}\nPresss Ctrl + C to quit\n";

$this->socket = $socket;

return $this;

}

好了, 如果一切没问题,接下来我们就可以socket_accept来监听请求了, 默认的socket是阻塞模式,如果没有请求,进程会一直阻塞等待, 对于高性能的服务来说, 最好采用非阻塞+select或者epoll的模式来同时处理多个请求, 但是我们的这个例子主要是为了介绍Yar的协议, 所以还是采用简单的阻塞模式。

接下来,我们来编写真正的RPC处理部分,首先我们通过accept接受一个请求, 然后读取请求的的内容,分析请求头中的Yar RPC Header信息, Yar RPC的协议头定义如下:

typedef struct _yar_header {

uint32_t id; // transaction id

uint16_t version; // protocl version

uint32_t magic_num; // default is: 0x80DFEC60

uint32_t reserved;

unsigned char provider[32]; // reqeust from who

unsigned char token[32]; // request token, used for authentication

uint32_t body_len; // request body len

}

其中, magic_num是用来验证请求有效性的一个特殊值, 合法的Yar RPC请求都会设置这个值为0x80DFEC60(我很想告诉你为啥是这个值,但我真不记得当时我为啥用这个数字了),这个头部是82个字节,可能有同学会问,不对啊一看这个Struct不应该是82啊,那是因为头部申明的时候采用pack模式,也就是不对齐, 所以确实是82个字节.

这里有一个需要注意的是, 0x80DFEC60如果你是自在32位的系统上的话,这个值超过了PHP的最大有符号整数的表示范围,类似我之前的这篇文章介绍的PHP_INT_MIN, PHP会自动转换成浮点数,所以如果是在32位系统上,你不能直接定义0x80DFEC60, 而是需要这么写来定义这个值:

pack("H*", "80DFEC60");

provider是一个字符串,标明了客户端的名字, 比如对于Yar扩展的Yar_Client就是"Yar PHP Cient-x.x.x"

token在设计的最初是为了做API key验证的,但是后来没用上,因为大部分都是内网应用,可以有多种办法来保证请求来源的合法性。

id是一个唯一请求id,这个是为了排查请求问题的, version默认为0,或者1,目前我没有升级过协议头,所以这个暂时我们也不用关心,reserved可以用来传递一些请求参数, 比如客户端可以说明是否保持连接。

body_len是我们需要关心的, 这个字段表明了这次请求,请求体一共多大(不包括Yar协议头部)。

所有的这些数字, 都是以网络字节序传递的, 我们采用PHP处理二进制流的unpack函数来解析读取进来的二进制流:

protected function parseHeader($header) {

return

unpack("Nid/nversion/Nmagic_num/Nreserved/A32provider/A32token/Nbody_len", $header);

}

这个函数会返回一个上面说到的头部结构体的数组。

对应的我们也需要使用pack来实现生成Yar Header的方法:

const YAR_MAGIC_NUM = 0x80DFEC60;

protected function genHeader($id, $len) {

$bin = pack("NnNNA32A32N", $id, 0, self::YAR_MAGIC_NUM, 0, "Yar PHP TCP Server", "", $len);

return $bin;

}

如刚才说的,我们需要在接受一个请求以前, 验证请求的合法性:

protected function validRequest($header) {

if ($header["magic_num"] != self::YAR_MAGIC_NUM) {

return false;

}

return true;

}

所以大概请求的处理整个逻辑框架是:

protected function accept() {

while (($conn = socket_accept($this->socket))) {

$buf = socket_read($conn, self::HEADER_SIZE, PHP_BINARY_READ);

if ($buf === false) {

socket_shutdown($conn);

continue;

}

if (!$this->validHeader($header = $this->parseHeader($buf))) {

$output = $this->response(1, "illegal Yar RPC request");

goto response;

}

$buf = socket_read($conn, $header["body_len"], PHP_BINARY_READ);

if ($buf === false) {

$output = $this->response(1, "insufficient request body");

goto response;

}

if (!$this->validPackager($buf)) {

$output = $this->response(1, "unsupported packager");

goto response;

}

$buf = substr($buf, 8); /* 跳过打包信息的8个字节 */

$request = $this->parseRequest($buf);

if ($request == false) {

$this->response(1, "malformed request body");

goto response;

}

$status = $this->handle($request, $ret);

$output = $this->response($status, $ret);

response:

socket_write($conn, $output, strlen($output));

socket_shutdown($conn); /* 关闭写 */

}

}

现在整体的框架就算完成了,我们需要完成handle,response方法就可以了,handle是要根据用户的请求中的m, 来调用指定的方法

protected function handle($request, &$ret) {

if ($request["m"] == "query") {

$ret = $this->query(...$request["p"]);

} else {

$ret = "unsupported method '" . $request["m"]. "'";

return 1;

}

return 0;

}

现在来实现query方法本身, 这个会很简单,就检查下id是不是在白名单数组:

protected function query($id) {

return isset($this->ids[$id]);

}

好了,接下来我们要完成response方法,这个方法是打包一个符合Yar协议的返回体,包括82个字节的头部,8个字节的打包信息,以及序列化后的响应体, 我们需要根据status不同,来选择设置响应体中的r还是e字段:

protected function response($status, $ret) {

$body = array();

$body["i"] = 0;

$body["s"] = $status;

if ($status == 0) {

$body["r"] = $ret;

} else {

$body["e"] = $ret;

}

$packed = serialize($body);

$header = $this->genHeader(0, strlen($packed) + 8);

return $header . str_pad("PHP", 8, "\0") . $packed;

}

好了, 马上就要大功告成了,我们最后完成启动方法和析构函数(关闭socket):

public function run() {

$this->loadDict()->listen()->accept();

}

public function __destruct() {

if ($this->socket) {

socket_close($this->socket);

}

}

现在一切就绪, 我们最后在文件末尾加入:

(new Whitelist)->run();

在测试之前,我们先准备一个测试词表,比如1到1000的id:

seq 1 1 10000 > user_id.dict

然后启动服务, 监听在本机的9000端口:

$ ./yar_server -F user_id.dict -S127.0.0.1:9000

Loading dict successfully, 1000 loaded

Starting Yar_Server at 127.0.0.1:9000

Presss Ctrl + C to quit

不错,服务启动成功,然后我们使用Yar扩展来编写客户端(你需要首先安装好Yar扩展), 测试下用户id 999和99999的调用效果:

$yar = new Yar_Client("tcp://127.0.0.1:9000");

var_dump($yar->query("999"));

var_dump($yar->query("99999"));

?>

和调用HTTP的Yar服务不同,此处我们应该使用tcp://做地址头,表示这是一个TCP的服务。

来,运行一下看看:

php7 client.php

bool(true)

bool(false)

看起来不错, 符合预期!

你也可以尝试故意构造一些错误的可能,比如调用不存在的方法之类的,来看看服务器的反应, 这个例子的代码你可以在这里找到.

到这里我就算介绍完了如何采用PHP来编写Yar的TCP服务, 大家应该可以很方便的把这个例子修改完善成自己希望的格式,或者嵌入Swoole(可以参考Swoole作者写的:这里)。

还是要再次说明,因为本文的主要目的是为了介绍Yar RPC通信协议,所以在服务管理这块并没有做的很完善,比如socket_accept, socket_read/write等都默认采用了阻塞模式,也没有加入超时设计,服务进程也只有一个,这个如果真的想用做实际服务的话,还是需要一些功课的,不过我相信你有兴趣的话,都是可以搞定的。:)

当然,最简单的是,你可以直接使用Yar-C服务框架来编写C Yar TCP服务。

在这里也有一个Yar-C Server的例子yar_server in C.

enjoy!

php socket accept,使用PHP Socket开发Yar TCP服务相关推荐

  1. ESP8266--SDK开发(TCP服务端)

    文章目录 一.详细步骤 二.效果展示 三.详细代码 一.详细步骤 1.导入头文件 #include "espconn.h"//TCP连接需要的头文件 #include " ...

  2. 【python】基于Socket的聊天室Python开发

    基于Socket的聊天室Python开发 一.Socket简述 二.创建服务端Server 2.1 创建服务端初始化 2.2 监听客户端连接 2.3 处理客户端消息 三.创建客户端Client 3.1 ...

  3. confluence中org.apache.tomcat.util.net.NioEndpoint$Acceptor.run Socket accept failed的解决方法

    confluence中org.apache.tomcat.util.net.NioEndpoint$Acceptor.run Socket accept failed的解决方法 参考文章: (1)co ...

  4. C语言socket accept()函数(提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符)

    文章目录 名称 使用格式 功能参数描述 参数 sockfd addr addrlen 返回值 示例 man 2 文档中的accept解释 错误处理 名称 accept() 接收一个套接字中已建立的连接 ...

  5. Socket accept failed

    启动tomcat显示如下错误: java.net.SocketException: select failed at java.net.PlainSocketImpl.socketAccept(Nat ...

  6. python socket server accpet 时间_Python socket.accept非阻塞?

    你可能想要像select.select()(见 documentation).您提供select()和三个套接字列表:要监视的可用性,可写性和错误状态的套接字.当新客户端等待时,服务器套接字将可读. ...

  7. python3socket非阻塞_Python的socket.accept非阻塞吗?

    你可能想要类似的东西select.select().你提供select()了三个套接字列表:要监视的可读性,可写性和错误状态的套接字.当新客户端在等待时,服务器套接字将可读. 该select()功能将 ...

  8. W5500开发笔记 | 02 - 使用W5500 Socket API 建立TCP服务端、TCP客户端

    系列文章 W5500开发笔记 | 01- W5500 Socket API的说明 一.实现思路 W5500内部是硬件TCP/IP协议栈,对外(MCU)只是提供了操作socket的能力,内部支持8个独立 ...

  9. 2-3 建立简易TCP服务端、客户端【socket server/client】【socket、bind、listen、accept、send、closesocket】【conect、recv】

    2-3 建立简易TCP服务端.客户端 文章目录 2-3 建立简易TCP服务端.客户端 0-前言 1-服务端简易功能 2-客户端简易功能 3-代码逻辑 4-服务端 4-1 建立socket 4-2 绑定 ...

  10. TCP accept返回的socket会在服务端新开一个端口嘛?服务端TCP连接数限制

    as you know,一个socket是由一个五元组来唯一标示的,即(协议,server_ip, server_port, client_ip, client_port).只要该五元组中任何一个值不 ...

最新文章

  1. PHP程序员的学习路线
  2. 2017-2018-2 20179216 《网络攻防与实践》 第四周总结
  3. java中,在一个有序数组中插入元素,使得数组保持有序排列
  4. mysql5.6.33安装教程_Linux下mysql5.6.33安装配置教程
  5. datastage 重启 续
  6. What's the QPSK?
  7. 荒野行动系统推荐观战榜_荒野行动 观战延迟投票结果公示 更新计划抢先看!...
  8. ES10新特性_Object.fromEntries---JavaScript_ECMAScript_ES6-ES11新特性工作笔记057
  9. python怎么打开程序管理器_python进程管理工具supervisor的安装与使用教程
  10. 索引sql server_SQL Server索引设计的五个主要注意事项
  11. vc++中进程通信之剪贴板常用函数
  12. 微服务学习之服务治理、服务注册与发现、Eureka【Hoxton.SR1版】
  13. 有没有大佬无偿提供一下 华为HCNA-Cloud Service-题库H13-811
  14. Markdown的使用心得
  15. myeclipse导入项目的问题,无法next
  16. php工具箱mysql启动不_解决php工具箱(phpStudy)Apache启动成功,MySql无法启动的问题...
  17. 双目测距算法matlab模拟,基于BM算法的双目测距.pdf
  18. 护照、身份证识别阅读器
  19. java导出excel 序号_java web将数据导出为Excel格式文件代码片段
  20. java 随机手机验证码_Java实现随机生成手机短信验证码的简单代码

热门文章

  1. darda oracle tfa_OSW - feiyun8616 - 博客园
  2. 3D hand pose:BMC
  3. PDF怎么加页码?PDF添加页码的方法
  4. Linux中国 QQ 交流群 大全
  5. PHP字符串函数strtolower(将字符串转化为小写)
  6. php函数大小写转化,php大小写转换函数(strtolower、strtoupper)用法介绍
  7. 图灵超算工作站UltraLAB GT400M上市
  8. LAMBDA表达式常用写法
  9. Base64解密转图片
  10. 5类网线,超5类网线,6类网线,超6类网线的区别