Swoole http server + yaf, swoole socket server + protobuf 等小结
拥抱swoole, 拥抱更好的php
接触swoole已经4年多了,一直没有好好静下心来学习。一直在做web端的应用,对网络协议和常驻内存型服务器一窍不通。一不留神swoole已经从小众扩展变成了流行框架,再不学习就完了
swoole + yaf
swoole server 的角色
还是先用swoole来做一个http server。
常见的php web应用,通常是apache+fast-cgi
或者 nginx + php-fpm
。这里以php-fpm
为例,我们配置nginx.conf
的时候都要配置一个
location ~*\.php$ {
root /usr/share/nginx/html;
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
...
}
主要是这句 fastcgi_pass 127.0.0.1:9000;
。就是说nginx 匹配到请求的uri是php后缀的时候,就把http request 转交给127.0.0.1:9000
处理了。如果你查看或者修改过php-fpm的配置文件,就知道9000是php-fpm的默认端口。那么到这里我们就清楚了,nginx把php文件交给php-fpm处理,php-fpm执行php脚本后返回http response给nginx。
接下来就好理解swoole http server 的作用以及应该扮演的角色。swoole http server 自己接受http请求,处理静态文件和php脚本,然后返回给客户端。swoole server 的配置项中有一个 document_root
用来告诉swoole 从哪里读取静态文件。当然,我们仍然可以用nginx来处理静态文件,只把php脚本交给swoole处理,这里需要修改nginx.conf,用nginx的代理功能 proxy_pass
location ~ .(gif|jpg|jpeg|png|bmp|swf|css|js)$ {
root /data/www/swoole-server/public;
}
location / {
proxy_http_version 1.1;
proxy_set_header Connection "keep-alive";
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:9501;
}
以上说了这么多,作为一个php web开发人员,应该可以大概理解平常写的逻辑代码,就是在swoole server 的 onRequest
中。包括平常的PHP全局变量 _SERVER, _COOKIE _GET _POST 等等,都在swoole server 的回调函数的参数 Request 中。那么我们接下来在onRequest回调中,自然要解析 uri,然后做路由解析进入到具体的业务逻辑。最简单的就是直接require uri的这个php脚本,也就是第一次接触php的script模式。路由解析,加载控制器MVC渲染这些都是框架最擅长的事情,因此在onRequest中我们引入框架,返回结果给swoole response对象。
接入Yaf
Swoole 的worker子进程是实际的工作进程,在收到客户端request的时候,swoole把request发送给worker,调用onRequest回调处理。如果我们在onRequest中引入Yaf 创建yaf app对象,由于onRequest是一个轮询事件回调,worker会重复创建yaf app,yaf app实际上处于相同的上下文,因此会提示已经存在yaf application对象。而且,我们并不需要在这里重复读取我们的配置文件。我们把yaf application 放在 onWorkerStart 中,一个worker 只产生一个yaf app对象,这个yaf对象轮询处理request uri 。
Swoole Http Server onWorkerStart & onRequest
public function onWorkerStart($serv, $work_id) {
// var_dump(get_included_files()); // 打印worker启动前已经加载的php文件
cli_set_process_title('swoole_worker_'.$work_id); // 设置worker子进程名称
Yaf\Registry::set('swoole_serv', $serv);
$this->app = new Yaf\Application( APPLICATION_PATH . "conf/application.ini");
$this->app->bootstrap();
}
public function onRequest($request, $response) {
// print_r($request->server);
$uri = $request->server['request_uri'];
printf("[%s]get %s\n", date('Y-m-d H:i:s'), $uri);
if ($uri == '/favicon.ico') {
$response->status(404);
$response->end();
} else {
Yaf\Registry::set('swoole_req', $request);
Yaf\Registry::set('swoole_res', $response);
// yaf 会自动输出脚本内容,因此这里使用缓存区接受交给swoole response 对象返回
ob_start();
$this->app->getDispatcher()->dispatch(new Yaf\Request\Http($this->rewrite($uri))); // rewrite 中可以应用自己的规则
$data = ob_get_clean();
$response->end(data);
}
}
如果你用过yaf,接下来只需要写一个标准的yaf框架应用就可以了。yaf 框架的public文件夹不再需要入口文件 index.php,nginx 中也不再需要重写uri规则,想想为啥
Swoole WebSocket
理解了http server 之后,我们再来创建一个websocket 服务器。websocket是web开发人员相对更熟悉的服务器,浏览器用javascript可以写一个现成的客户端。swoole websocket服务器与http 服务器大同小异,只不过onRequest()
方法变成了onMessage()
,$response->end()
变成了$server->push()
;
websocket是有状态的长连接,http是无状态的。无状态意思是说http你只需要知道request是什么,然后给他response,不管是谁,请求几次request,都是一样的response。而有状态的意思是,对于每一个请求,你需要分辨它是谁。因此对于相同的请求,可能会有不同的处理。websocket的每个客户端链接有唯一标识fd,有点类似于会话session id 的意思。
与onRequest()方法类似,在onMessage()方法中,我们需要对客户端发送的数据进行路由解析,然后想客户端返回结果。不过这里不再是http协议的url请求格式了,是我们自己组装的协议数据包,比如一个JSON结构,包括action
,controller
,module
等等。我们仍然可以引入yaf框架,利用他的类库自动加载Loader和路由Dispatcher机制,来处理客户端请求,这里不再赘述。
public function onMessage(\Swoole\Websocket\Server $serv, \Swoole\Websocket\Frame $frame) {
$route = json_decode($frame->data);
if ($route->module) {
try {
ob_start();
$this->app->getDispatcher()->dispatch(new Yaf\Request\Simple('cli', $route->module, $route->controller, $route->action, $route->params));
$response = ob_get_clean();
} catch (Exception $e) {
// handle exception
}
$serv->push($frame->fd, $response);
} else {
printf("[%s] unknow message: %s\n", date('Y-m-d H:i:s'), $frame->data);
}
}
PHP 使用 Protobuf 消息
上面我们使用了一个 JSON 协议传输websocket的例子,而 Protobuf 是 与JSON 类似的一种消息协议,除此之外,大家熟知的xml也是一种消息协议。ProtoBuf 是google开源的一种通信协议,既然是google的,那么别问,学就对了。
相比JSON与XML,ProtoBuf的好处体现在
- 解析快。为什么比XML,JSON的字符串解析快呢,google大神们说快那就是快,别问。
- 节省包体大小。它把我们的消息结构体转为二进制流进行传输,到了另一端再通过相同的结构体定义解析还原。
- 天然的消息加密。传输过程中是二进制,xml或者json还需要进一步加解密才能保密。与之同时带来的缺点,就是可读性差。你看着一堆二进制串,在消息解析出来之前完全不知道发的是啥(个人认为并不是什么缺点)。
php 处理protobuf
用php处理protobuf我们需要用到两个东西
- protoc https://repo1.maven.org/maven2/com/google/protobuf/protoc/
protoc 是将proto结构体文件转换成对应的php文件,每个文件就是一个消息体类/path/protoc --php_dir=/php-lib/ xxx.proto
- proto-php 扩展(类库)https://github.com/protocolbuffers/protobuf/tree/master/php
protoc 只是负责将proto文件转成php类,这些类的父类定义及使用需要php安装protobuf扩展,或者在项目中直接引入php类库(扩展和类库的概念应该知道的吧。。)
我们在解析protobuf二进制流之前,是需要先指定对应的消息结构体的,因此我们不能只发送一个protobuf,至少应该再附带一个消息ID。通过这个消息ID对应的结构体,我们才能解析具体的protobuf消息。
php处理二进制数据需要用到pack()
和unpack()
。如果像我一样没接触过的同学,可以临时补补课,学习一下字节序什么的
假设我们有一个int32位无符号消息ID,那么每个包体的结构就是 消息ID
+protobuf
。发送消息之前,我们进行数据打包
public function pack($msg_id, $msg_body) {
$proto_class = Proto::GetResponseMessageProto($msg_id); // 由消息ID获取对应的proto结构体类名
if (!$proto_class ) {
$this->err = 'No msg id matched.';
return FALSE;
}
try {
$msg_obj = new $proto_class ();
// 定义消息
$msg_obj->mergeFromArray($msg_body);
// 打包protobuf
$buf_str= $msg_obj->serializeToString();
// 拼接消息体
$this->bufString = pack('N', $msg_id). $buf_str;;
return TRUE;
} catch (\Exception $e){
$this->err = $e->getMessage();
return FALSE;
}
}
数据打包相对简单些,数据解包会有一点曲折。也就是在这里我感觉PHP在处理二进制数据上有点局限,也可能是我没有掌握更高效的方法。如果有的话,还望各位读者不吝赐教。
public function unpack($msg) {
$data = unpack('Nmsg_id/a*msg_body', $msg);
$msg_id = $data['msg_id'];
// 暂时把protobuf解析成字符串
$buf_str = $data['msg_body'];
$proto_class = Proto::GetRequestMessageProto($msg_id);
if (!$proto_class) {
$this->err = 'No msg id matched.';
return FALSE;
// handle error.
}
try {
$msg_obj = new $proto_class();
// 上面已经把probuf解析成了字符串,因此这里需要再转化为二进制
$msg_obj->mergeFromString(pack('a*', $buf_str));
print_r($msg_obj->serializeToJsonString()); // protobuf 类的读取接口比较少,建议去看看源码
} catch (\Exception $e) {
$this->err = $e->getMessage();
return FALSE;
// handle invalid msg
// throw new MessageParseException('Invalid message');
}
$this->msg_obj = $msg_obj->serializeToJsonString(); // 消息体
$this->msg_id = $msg_id; // 消息ID
return TRUE;
}
接收消息的处理
// onMessage
public function onMessage(swoole_websocket_server $serv, swoole_websocket_frame $frame) {
$msg = new \Message\Message();
if ($msg->unpack($frame->data)) {
printf("[%s] receive data: %d %s\n", date('Y-m-d H:i:s'), $msg->msg_id, $msg->msg_obj);
// dispatcher
list($module, $controller, $action) = $this->dispatch($msg->msg_id); // 自己的消息路由,就是某一个消息ID交给哪个控制器进行处理
try {
ob_start();
$this->app->getDispatcher()->dispatch(new Yaf\Request\Simple('cli', $module, $controller, $action, json_decode($msg->msg_obj, TRUE)));
$response = ob_get_clean();
$code = 0;
} catch (Exception $e) {
$response = json_encode(['err' => $e->getMessage()]);
$code = -1;
}
print_r($response);
if (!$msg->pack($msg->msg_id, $response)) {
print_r('msg pack err:'. $msg->err);
} else {
$serv->push($frame->fd, $msg->bufString, WEBSOCKET_OPCODE_BINARY); // websocket 发送二进制
}
} else {
printf("[%s] unpack err: %s\n", date('Y-m-d H:i:s'), $frame->data);
print_r('msg unpack err:'. $msg->err);
}
}
附前端javascript的示例
javascript处理相对来说还更简单,用到的是 ArrayBuffer
var protoRoot = null;
protobuf.load('/data/game.proto', function(err, root) {
if (err)
throw err;
protoRoot = root;
});
function writeBuf(msgid, buf) {
// buf 是protobuf消息的二进制结果
var length = buf.length;
var buffer = new ArrayBuffer(buf.length + 4); // 消息ID占4位
var dv = new DataView(buffer);
dv.setUint32(0, msgid, false); // 大端字节序
for (let i=0;i<buf.length;i++) {
dv.setInt8(4+i, buf[i]); // 逐字节写入buffer
}
console.log(buffer);
return buffer;
}
function readBuf(buf) {
var dv = new DataView(buf);
var msgid = dv.getUint32(0, false);
var buf = new Uint8Array(buf, 4); // 截取消息ID后面的字节,交给protobuf解析
return [msgid, buf];
}
function Request_Message(msg, req, callback) {
// 将客户端请求的消息msg转成protobuf
var RequestMessage = protoRoot.lookupType("dapianzi."+req); // 这里需要加上命名空间
var errMsg = RequestMessage.verify(msg);
if (errMsg)
throw Error(errMsg);
var message = RequestMessage.fromObject(msg);
var buffer = RequestMessage.encode(message).finish();
callback(buffer); // 下一步调用writeBuf 产生消息包,发送给服务器
}
function Response_Message(buf, res, callback) {
// buf 是readBuf()中返回的二进制串,这里交给protobuf解析成消息体
var ResponseMessage = protoRoot.lookupType("dapianzi."+res);
var message = ResponseMessage.decode(buf);
var object = ResponseMessage.toObject(message, {
longs: String,
enums: String,
bytes: String,
});
callback(object); // 进行客户端逻辑
}
后记
在websocket服务器中使用yaf还是觉得比较牵强,毕竟yaf是一个web框架,使用它仅仅是可以比较方便的使用lib自动加载,以及路由映射。因此,还是得自己想办法写一个简单的框架,实现消息路由,类库加载,事件注册,和全局对象的容器管理。
Swoole http server + yaf, swoole socket server + protobuf 等小结的更多相关文章
- python socket server源码学习
原文请见:http://www.cnblogs.com/wupeiqi/articles/5040823.html 这里就是自己简单整理一下: #!/usr/bin/env python # -*- ...
- Java Socket Server的演进 (一)
最近在看一些网络服务器的设计, 本文就从起源的角度介绍一下现代网络服务器处理并发连接的思路, 例子就用java提供的API. 1.单线程同步阻塞式服务器及操作系统API 此种是最简单的socket服务 ...
- Python之路-python(面向对象进阶(模块的动态导入、断言、Socket Server))
模块的动态导入 断言 Socket Server 一.模块的动态导入 class C(object): def __init__(self): self.name = "zhangsan&q ...
- 使用 nc (Netcat) 建立傳送資料的 socket server
原文:http://blog.longwin.com.tw/2012/02/nc-data-send-socket-server-2012/ 於 Debian / Ubuntu Linux 想要透過 ...
- 面向连接的Socket Server的简单实现(简明易懂)
一.基本原理 有时候我们需要实现一个公共的模块,需要对多个其他的模块提供服务,最常用的方式就是实现一个Socket Server,接受客户的请求,并返回给客户结果. 这经常涉及到如果管理多个连接及如何 ...
- C# Socket Server 收不到数据
#/usr/bin/env python # -*- coding: utf- -*- # C# Socket Server 收不到数据 # 说明: # 最近在调Python通过Socket Clie ...
- c++ 创建 socket server
下面一段代码是创建socket server的代码片段: 需要引用的库包括: #include <sys/types.h> #include <sys/socket.h> #i ...
- Python Socket,How to Create Socket Server? - 网络编程实例
文章出自:Python socket – network programming tutorial by Silver Moon 原创译文,如有版权问题请联系删除. Network programin ...
- 启动redis出现Creating Server TCP listening socket *:6379: bind: No such file or directory
E:\redis>redis-server.exe redis.windows.conf [8564] 10 Oct 20:00:36.745 # Creating Server TCP lis ...
随机推荐
- Informatica PowerCenter下载地址
https://edelivery.oracle.com/EPD/Download/get_form?egroup_aru_number=12854075
- oracle数据库中函数的递归调用
如有下面的表结构AAAA,用一个字段prev_id表示记录的先后顺序,要对其排序,需要用的递归函数 ID PREV_ID CONT 99 a 23 54 d 21 23 e 54 33 c 33 ...
- 04.CSS的继承性和层叠性
CSS有两大特性: 继承性和层叠性 继承性 面向对象语言都会存在继承的概念 , 在面向对象语言中, 继承的特点: 继承了父类的属性和方法. 那么 css 就是在设置属性的 , 不会牵扯到方法 ...
- 一卡通大冒险(hdu 2512)
因为长期钻研算法, 无暇顾及个人问题,BUAA ACM/ICPC 训练小组的帅哥们大部分都是单身.某天,他们在机房商量一个绝妙的计划"一卡通大冒险".这个计划是由wf最先提出来的, ...
- 【284】◀▶ arcpy.da & arcpy 数据访问模块
使用游标访问数据 数据访问模块 (arcpy.da) 参考: ArcGIS Python编程案例(9)-ArcPy数据访问模块 读取几何 写入几何 使用 Python 指定查询 01 da.Sea ...
- FPGA和CPLD的比较
1 FPGA的集成度比CPLD高,具有更复杂的布线结构和逻辑实现. 2 CPLD更适合触发器有限而乘积丰富的结构,更适合完成复杂的组合逻辑:FPGA更适合于触发器丰富的结构,适合完成时序逻辑. 3 c ...
- Python模块及其导入
一.模块 1.模块的定义: 为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少, 很多编程语言都采用这种组织代码的方式.在Python中,一个.py文件 ...
- window - BOM对象
Window 对象 Window 对象表示浏览器中打开的窗口. 如果文档包含框架(frame 或 iframe 标签),浏览器会为 HTML 文档创建一个 window 对象,并为每个框架创建一个额外 ...
- ORA-01145: 除非启用了介质恢复 否则不允许立即脱机
Microsoft Windows [版本 6.1.7601]版权所有 (c) 2009 Microsoft Corporation.保留所有权利. C:\Users\Administrator> ...
- Log4Net 在ASP.NET WebForm 和 MVC的全局配置
使用log4net可以很方便地为应用添加日志功能.应用Log4net,开发者可以很精确地控制日志信息的输出,减少了多余信息,提高了日志记录性能.同时,通过外部配置文件,用户可以不用重新编译程序就能改变 ...