workerman开发文档

walkor

注意,这个是workerman老版本文档,已经不适用目前版本

workerman文档最新地址 https://www.workerman.net/doc/workerman

一、workerman是什么

  workerman是一个高性能的PHP Socket服务器框架,它类似PHP-FPM,提供进程控制及socket通讯功能,区别是PHP-FPM是以FAST-CGI的协议对外提供服务的,而workerman却可以支持各种协议(包括自定义协议),并且支持长链接,支持进程内全局对象资源等永久保持等特性。

二、workerman能做什么

  虽然workerman可以作为Webserver的替代Nginx PHP-FPM等架构,并且性能也比Nginx PHP-FPM高,但是我们不推荐这样做,因为PHP的WebServer市场上已经很成熟了,workerman不会再去做重复的事情。反而workerman把精力花在传统WebServer无法胜任的角色上,例如非HTTP协议的应用、长链接应用、UDP应用、IM、游戏后端等。

三、workerman目录结构

../
├── applications
└── workerman

  1、workerman目录是workerman框架的核心代码,包含了启动脚本及workerman配置等。为了方便理解你可以把它看作PHP-FPM。
  2、applications目录存放的是应用程序,例如应用框架、应用入口文件(类似index.php),业务逻辑等等。

workerman与applications是如何交互的
  workerman可以看作是PHP-FPM,所以它需要一个入口文件(类似index.php),这个入口文件可以放在applications中,这个入口文件一般满足以下要求:
  1、继承Man\Core\SocketWorker类
  2、实现Man\Core\SocketWorker::dealInput方法
  3、实现Man\Core\SocketWorker::dealProcess方法
workerman就是运行入口文件中的dealInput dealProcess方法来与业务衔接的。

四、workerman请求周期

  workerman请求周期包括了请求解析+业务处理两部分,正好对应Man\Core\SocketWorker::dealInput/dealProcess两个方法。也就是说客户端每次请求一般都会调用Man\Core\SocketWorker::dealInput/dealProcess,这两个方法也正是暴露给开发者的接口,实现这两个接口使得开发者可以在请求到来时和请求接收完毕时加入自己的业务逻辑。下面详细说明两个方法。

Man\Core\SocketWorker::dealInput($recv_buffer)方法
  这个方法是用来根据应用层协议判断从客户端接收到的$recv_buffer数据是否合法、是否完整。这个方法的返回值有三种情况,X>0说明还有X字节没有收到,X==0说明数据全部收到,进入业务处理阶段dealProcess,X==false说明数据包格式错误

Man\Core\SocketWorker::dealProcess($recv_buffer)方法
  当Man\Core\SocketWorker::dealInput($recv_buffer)返回0时,即说明客户端接收到的$recv_buffer是合法并完整的,这时根据应用层协议解析出实际的内容做相应的业务逻辑。

五、如何使用workerman

1、使用workerman第一步就是要确认你用哪种应用层协议。
什么是应用层协议?
  应用层协议是用来识别网络传来数据是否完整及传输的数据是什么,协议一般包括两部分:
  1、数据完整性标记(Man\Core\SocketWorker::dealInput)。
  2、数据格式(Man\Core\SocketWorker::dealProcess)。
  不要把协议想象成是很复杂的东西,举个例子应用层协议类似于有人给你邮寄过来一堆物品(网络数据),你要判断东西(数据)是否都送来了,那么你就需要看下快递清单(类似协议头,里面记录了数据长度),清单上写着3个箱子,你看到确实有3个箱子,确认物品全部收到了(以上是Man\Core\SocketWorker::dealInput函数要做的事)。然后打开箱子(双方协商的解包方法、json_decode、parse_xml等)便可以看到具体的物品(数据)了,有了物品(数据)你便可以做你应该做的事情了(这部分是Man\Core\SocketWorker::dealProcess做的事情)。

让我们来定义一个应用层协议
1、数据完整性标识
  第一种:固定位置的一个字段标记整个包的长度。例如数据首部四个字节(int)是数据包长度+数据,即$packed_int . $string
  第二种:在每一段数据结束时加上一个特粟的标记位,一般是一个字节。例如在数据末尾加上\0表示一个数据包结束,即$string.‘\0’
  当然还有其它方法
2、数据格式
  数据格式有很多选择,如文本、json、xml、二进制流等等

一个协议的实现 4bytes+json, 即pack('N', $len).$json

创建文件 applications/JsonDemo/JsonProto.php

<?php
class JsonProto
{
    // 根据首部四个字节(int)判断数据是否接收完毕
    public static function check($buffer)
    {
        // 读取首部四个字节
        $buffer_data = unpack('Ntotal_length', $buffer);
        // 得到这次数据的整体长度(字节)
        $total_length = $buffer_data['total_length'];
        // 已经收到的长度(字节)
        $recv_length = strlen($buffer);
        if($total_length>$recv_length)
        {
            // 还有这么多字节要接收
            return $total_length - $recv_length;
        }
        // 接收完毕
        return 0;
    }

    // 打包
    public static function encode($data)
    {
        // 选用json格式化数据
        $buffer = json_encode($data);
        // 包的整体长度为json长度加首部四个字节(首部数据包长度存储占用空间)
        $total_length = 4 + strlen($buffer);
        return pack('N', $total_length) . $buffer;
    }

    // 解包
    public static function decode($buffer)
    {
        $buffer_data = unpack('Ntotal_length', $buffer);
        // 得到这次数据的整体长度(字节)
        $total_length = $buffer_data['total_length'];
        // json的数据
        $json_string = substr($buffer, 4);
        return json_decode($json_string, true);
    }
} 

2、创建入口文件并实现 Man\Core\SocketWorker::dealInput dealProcess方法
创建入口文件 applications/JsonDemo/JsonDemo.php

<?php
require_once __DIR__.'/JsonProto.php';
class JsonDemo extends Man\Core\SocketWorker
{
    // workeman 请求周期第一步,根据应用层协议判断数据是否接收完毕
    public function dealInput($recv_buffer)
    {
        // 判断数据是否接收完毕
        return JsonProto::check($recv_buffer); 
    }

    // workerman请求周期第二步,请求接收完毕后根据接收到的数据运行对应的业务逻辑
    public function dealProcess($recv_buffer)
    {
        // 得到json数据
        $json_data = JsonProto::decode($recv_buffer);

        /**
          *  这里根据你的json_data内容出处理不同的业务逻辑
         **/

        // 如果有需要,可以向客户端发送结果
        $this->sendToClient(JsonProto::encode(array('code'=>0, 'msg'=>'ok')));
    }
}

3、配置workerman
创建配置 workerman/conf/conf.d/JsonDemo.conf,配置名与类名相同。

;进程入口文件,类似index.php
worker_file = ../applications/JsonDemo/JsonDemo.php
;监听的ip 端口,udp协议则是 listen=udp://0.0.0.0:2020
listen = tcp://0.0.0.0:2020
;你的网络服务是长连接还是短连接。如果需要和客户端一直保持链接(例如聊天类应用)则设置为1,否则设置为0
persistent_connection = 0
;启动多少服务进程
start_workers=5
;以哪个用户运行该进程,为了安全考录,应该使用权限较低的用户,例如www-data nobody等
user=root
;socket有数据可读的时候预读长度,一般设置为应用层协议包头的长度,因为JsonProto首部四个字节标识了长度,所以这里设置为4
preread_length=4

六、启动workerman

./workerman/bin/workermand start

至此你的服务框架已经开发完了

七、写一个简单的PHP客户端

<?php
require_once __DIR__.'/JsonProto.php';
$socket = stream_socket_client('tcp://127.0.0.1:2020');
// JsonDemo::dealInput 会收到 $recv_buffer 类似 xxxx{'mod':'abc','act':'cde'} 这样的数据,其中xxxx是四个字节的int数字,表现为乱码,后面是json串
fwrite($socket, JsonProto::encode(array('mod'=>'abc', 'act'=>'cde')));
var_export(fread($socket, 65535));

八、关于进程模型

workerman整体的进程模型是master slave 模型:
  1、master进程负责监控slave,slave进程退出时master进程会重新创建相同的slave进程
  2、slave进程是实际干活儿的进程,负责接收请求(Man\Core\SocketWorker::dealInput)以及处理请求(Man\Core\SocketWorker::dealProcess)

进程模型的变种
  workerman[b]整体[/b]的进程模型是master slave 模型,但是根据slave的工作不同,有一些区别,下面通过例子讲解。
http://www.workerman.net/workerman 中有两个例子,一个是FileRecevierDemo(二进制文件传输),一个是ChatDemo(聊天)。

FileRecevierDemo文件传输
  测试方法是启动workerman后,命令行运行 cd applications/FileReceiverDemo/ ,命令行运行 php ClientDemo.php workerman.png 2 会把当前目录的workerman.png 文件传输给workerman,然后workerman会把文件存储到本地(逻辑在applications/FileReceiverDemo/FileReceiverDemo.php中),并且返回存储位置,然后服务端和客户端断开链接。
FileRecevierDemo分析
  1、dealInput:所用协议 头部5个字节,前4个字节如上面JsonDemo一样标识数据包长度,另外一个字节表示文件类型(1:jpg,2:png..),再后面紧接着是二进制图片数据
  2、dealProcess:因为头部固定5个字节,则从第6个字节开始到包长的所有数据就是图片数据,截取完毕后根据头部1个字节的文件类型(1:jpg、2:png..),保存文件到本地,并发给客户端结果($this->sendToClient())

FileRecevierDemo进程模型
  FileRecevierDemo是典型的master slave进程模型,slave进程具有以下特点:
  1、slave既负责网络传输,又负责业务处理
  2、短链接(业务处理完毕后就断开链接,下次传文件时会再次链接)

ChatDemo聊天
  测试方法:启动workerman后,命令行运行 cd applications/ChatDemo/Tests,命令行运行 php Chat.php。打字回车发给所有人,uid:XXXXX发消息给某个人。

ChatDemo进程模型分析
  1、ChatDemo的slave进程分为两种:gateway进程和worker进程
  2、gateway进程:applications/ChatDemo/Bootstrap/Gateway.php,这个进程只负责网路IO,不处理业务逻辑。具体工作内容为:维持服务端与客户端的长链接,将客户端的数据转发给worker进程(目前是udp协议,有需要可以自行改成tcp),worker进程做实际的业务处理,有需要时将结果数据发给gateway进程(目前是udp协议,有需要可以自行改成tcp),gateway进程将结果数据转发给对应的用户。
  3、worker进程:applications/ChatDemo/Bootstrap/Worker.php,这个进程主要负责业务逻辑。具体工作内容为:负责接收gateway转发来的客户端请求,并解析处理,如果有需要,则同样通过gateway向某个/些客户端发送数据。

ChatDemo进程模型特点
  1、有两个slave进程,一个负责网络IO并维持客户端链接,一个负责业务处理
  2、长链接,客户端与gateway之间是TCP长链接,通过这个链接实现客户端与服务端的双向通信

九、为什么使用gateway worker模型

  gateway worker模型非常适合长链接应用,例如聊天、游戏后台等。如果是短链接,则建议使用上面FileRecevierDemo基础的master slave模型。
  1、gateway只负责网络IO,worker主要负责业务逻辑。各司其职,非常高效。
  打个比方,一个餐馆有4工人(进程),他们即负责招呼客人(网络IO),又负责在厨房做菜(业务逻辑)。当客人一下子来很多的时候(很多链接或很多数据),大家有可能都去招待客人了(都处理网络IO),厨房没人做菜(做业务)。当大家都做菜的时候(做业务),又没人招呼客人(接收链接),导致客人(用户)都在等待。但是当我们把工人(进程)分工一下,2个人专门招呼客人(geteway进程),两个人专门做菜(worker进程),这样每个时刻都有有人(进程)招待客人(接收数据),都有人(进程)做菜(处理业务)。当gateway不够用的时候(一般都是够用的)增加gateway,worker忙不过来的时候增加worker进程。这样效率会提升很多。
  2、提高稳定性
  gateway进程因为要维持用户链接,这要求gateway进程一定要非常稳定,不然如果gateway进程出问题,则这个进程上的所有用户都会断开链接。让gateway只负责网络IO,不负责业务,就是因为业务频繁变化,可能会有致命的错误(例如调用了一个不存在的函数)导致进程退出,进而导致用户链接断开。而让gateway只负责网络IO,就是要避免这种风险。而worker进程是无状态的(没有保存用户链接等状态信息),即使偶尔出现FatalErr,也只会影响当前的这次请求,而不会对整个服务造成大的影响。
  3、热更新
  由于gateway进程没有业务逻辑,所以geteway进程极少有代码更新。而worker进程由于负责业务逻辑,会有经常性的代码更新。这样看来我们每次代码更新,只要重启worker进程就可以实现运行新的业务代码。实际上也是这样,当更新程序逻辑时,我们只需要重启worker进程就可以了,这样就不会导致更新代码的时候用户链接会断开,达到不影响用户的情况下热更新后台程序。
  4、扩展容易
  当worker进程不够用的时候,我们可以水平扩展它,可增加worker的进程数量,甚至可以增加服务器专门运行worker进程,达到水平扩展的目的,以支持更大的用户量。gateway进程也是同样的道理。

十、gateway worker进程模型预留的接口

  正如上面所说,gateway worker模型非常适合长链接应用,为此ChatDemo专门抽象出来相应的接口以简化长链接应用的开发。
使用ChatDemo中的gateway worker进程模型,业务开发者只需要实现worker进程中的业务逻辑即可,开发者只需要关注applications/ChatDemo/Event.php。这个文件开放了三个方法给开发者去实现。
  1、onConnect
  当用户链接服务器后发的第一个数据包时会触发这个方法,一般在这里做用户登录验证工作,例如通过用户传来的$message($message是客户端传递过来未作任何加工的原始数据,里面一般包含了用户名和密码)获得用户名密码,然后从数据库中获得uid(uid必须为大于0小于42亿的数字),并存储这个uid的内部gateway通信地址( GateWay::storeUid($uid)),还要向gateway进程注册(GateWay::notifyConnectionSuccess($uid))这个uid,还可以在这里向数据库中标记该用户在线状态。如果用户不合法则可以调用GateWay::kickCurrentUser(‘’)踢掉当前登录用户。
  2、onMessage
  当合法用户(onConnection时用户合法并注册)发来数据时会触发这个方法。这个方法里面主要是处理业务逻辑。$message是客户端发过来的未作任何加工的原始数据,通过商定的协议去解析$message,然后根据数据内容做路由分发,运行同的业务函数等。如果这期间需要给某uid或者所有人的客户端发消息,则使用Gateway::sendToUid/SendToAll方法即可。
  3、onClose
  当用户客户端主动断开时,触发这个方法。
在这个方法中开发者可以清理用户的数据,例如在数据库中加上下线标记。

发送数据接口
  chatDemo中提供了两个给用户客户端发送数据的接口
  1、Gateway::sendToAll($message)
  这个是向所有在线的(当前链接服务器的)所有用户发送消息。$message是要发送的数据,客户端会收到未经过任何处理的原始$message数据。客户端收到这个$message数据后根据协议解析数据内容,做相应的展现。
  2、Gateway::sendToUid($uid, $message);
  向某个在线用户的客户端发送消息,$uid是接收者的uid。$message是要发送的数据,目标用户的客户端会收到未经过任何处理的原始$message数据。客户端收到这个$message数据后根据协议解析数据内容,做相应的展现。
  3、Gateway::kickUid/kickCurrentUid
  踢掉某uid用户/当前用户
  4、如何给某批(群组)的用户发消息
  需要循环调用Gateway::sendToUid($uid, $message);方法

待续.....

15176 1 1
1个评论

smith

nice!

  • 暂无评论
年代过于久远,无法发表评论

walkor

139591
积分
0
获赞数
0
粉丝数
2014-05-04 加入
🔝