热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Swoolehttpserver+yaf,swoolesocketserver+protobuf等小结

拥抱swoole,拥抱更好的phpSwoole是什么?Yaf是什么?接触swoole已经4年多了,一直没有好好静下心来学习。一直在做web端的应用,对网络协议和常

拥抱swoole, 拥抱更好的php

Swoole 是什么?

Yaf 是什么?

接触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的,那么别问,学就对了。

什么是ProtoBuf

相比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()。如果像我一样没接触过的同学,可以临时补补课,学习一下字节序什么的

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

后记

在websocket服务器中使用yaf还是觉得比较牵强,毕竟yaf是一个web框架,使用它仅仅是可以比较方便的使用lib自动加载,以及路由映射。因此,还是得自己想办法写一个简单的框架,实现消息路由,类库加载,事件注册,和全局对象的容器管理。


推荐阅读
  • 本文介绍了django中视图函数的使用方法,包括如何接收Web请求并返回Web响应,以及如何处理GET请求和POST请求。同时还介绍了urls.py和views.py文件的配置方式。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • HDU 2372 El Dorado(DP)的最长上升子序列长度求解方法
    本文介绍了解决HDU 2372 El Dorado问题的一种动态规划方法,通过循环k的方式求解最长上升子序列的长度。具体实现过程包括初始化dp数组、读取数列、计算最长上升子序列长度等步骤。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Windows下配置PHP5.6的方法及注意事项
    本文介绍了在Windows系统下配置PHP5.6的步骤及注意事项,包括下载PHP5.6、解压并配置IIS、添加模块映射、测试等。同时提供了一些常见问题的解决方法,如下载缺失的msvcr110.dll文件等。通过本文的指导,读者可以轻松地在Windows系统下配置PHP5.6,并解决一些常见的配置问题。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 本文介绍了在mac环境下使用nginx配置nodejs代理服务器的步骤,包括安装nginx、创建目录和文件、配置代理的域名和日志记录等。 ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • CentOS 6.5安装VMware Tools及共享文件夹显示问题解决方法
    本文介绍了在CentOS 6.5上安装VMware Tools及解决共享文件夹显示问题的方法。包括清空CD/DVD使用的ISO镜像文件、创建挂载目录、改变光驱设备的读写权限等步骤。最后给出了拷贝解压VMware Tools的操作。 ... [详细]
author-avatar
XC一米_623
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有