声明:本站文章均为作者个人原创,图片均为实际截图。如有需要请收藏网站,禁止转载,谢谢配合!!!

Workerman 是一款开源高性能异步PHP socket即时通讯框架。支持高并发,超高稳定性,被广泛的用于手机app、移动通讯,微信小程序,手游服务端、网络游戏、PHP聊天室、硬件通讯、智能家居、车联网、物联网等领域的开发。 支持 TCP 长连接,支持Websocket、HTTP等协议,支持自定义协议。拥有异步Mysql、异步Redis、异步Http、MQTT物联网客户端、异步消息队列等众多高性能组件。

Workerman 的目标是让PHP开发者更容易的开发出基于socket的高性能的应用服务,而不用去了解 PHP socket 以及PHP 多进程细节。 Workerman 本身是一个PHP多进程服务器框架,具有 PHP 进程管理以及 socket 通信的模块,所以不依赖 php-fpm、nginx 或者 apache 等这些容器便可以独立运行。

特性

workerman 诸多的不同寻常的特性,使得PHP开发者可以开发出不同寻常的应用程序。

  • 纯 PHP 开发
    workerman完全使用PHP开发,使用workerman开发的应用程序不依赖php-fpm、apache、nginx这些容器就可以独立运行。 这使得PHP开发者开发、部署、调试应用程序非常方便。

  • 支持 PHP 多进程
    为了充分发挥服务器多CPU的性能,workerman默认支持多进程多任务。workerman开启一个主进程和多个子进程对外提供服务, 主进程负责监控子进程退出信号,并负责生成新的子进程去处理服务,这样做不仅提高了应用程序的性能,而且使得workerman更加稳定。

  • 支持 TCP、UDP
    workerman支持TCP和UDP两种传输层协议,只需要更改配置的一个字段,便可以更换传输层协议,业务代码无需任何改动。

  • 支持长连接
    很多时候需要PHP应用程序要与客户端保持长连接,比如聊天室、游戏等,但是传统的PHP容器(apache、nginx、php-fpm)很难做到这一点。 使用workerman可以轻松使用PHP长连接。php单个进程可以支持几千甚至几万的并发连接,多进程则可支持数十万甚至上百万的并发连接。

  • 支持各种应用层协议
    接口上支持各种应用层协议,包括自定义协议。Workerman默认支持的协议有HTTP、WebSocket、以及简单的Text文本协议。 同时Workerman提供了通用的协议接口,开发者基于此接口便可以方便的开发出自己的协议。

  • 支持高并发
    workerman支持Libevent事件轮询库(需要安装 Libevent扩展), 使用Libevent在高并发时性能非常卓越,如果没有安装Libevent则使用PHP内置的Select相关系统调用。

  • 支持服务平滑重启
    当需要重启服务时(例如发布版本),我们不希望正在处理用户请求的进程被立刻终止,更不希望重启的那一刻没有足够的进程对外提供服务, 为了保证任意时刻都有足够的进程对外提供服务,则可以使用平滑重启命令,平滑重启过程中workerman会让子进程处理完请求后才退出, 并且能够保证在任意时刻都有足够的进程对外服务。

  • 支持HHVM
    支持HHVM,对于php性能有大幅度(一般为50%左右甚至更高)的提升,尤其是在cpu密集运算中。实际经过压力测试确实有明显性能提升效果。

  • 支持以指定用户运行子进程
    因为子进程是实际处理用户请求的进程,为了安全考虑,子进程不能有太高的权限,所以workerman支持设置子运行进程运行的用户。

  • 自带监控
    workerman内部带有监控统计模块,能够统计workerman自身的一些数据,如进程退出次数及退出状态,每个进程占用内存大小及监听的ip端口、每个进程启动时间、 进程运行的服务名、每个进程处理请求数、连接数、数据包发送失败量等等。这些信息可以本地运行php start.php status本地查看。

  • 支持毫秒级别定时器
    支持毫秒级别定时器,可以做定时任务或者定时计算,如游戏中地图上AI相关计算。

    • 支持异步IO
      Workerman自带的网络IO接口是异步的,开发者可实现基于事件的异步编程
  • 支持对象或者资源永久保持
    在一个进程生命周期内静态成员或者全局变量在不主动销毁的情况下是永久保持的,也就是只要初始化一次静态成员或者全局变量在当前进程的整个生命周期内的所有请求都可以复用这个 静态成员或者全局变量。例如只要单个进程内初始化一次数据库连接,则以后这个进程的所有请求都可以复用这个数据库连接,不用每个用户请求都去重连数据库,避免了频繁连接数据库过程中TCP三次握手、 数据库权限验证、断开连接时TCP四次握手的过程,极大的提高了应用程序效率。memcache、redis等初始化也是同样的道理。

  • 高性能
    由于php文件从磁盘读取解析一次后便会常驻内存,下次使用时直接使用内存中的opcode, 极大的减少了磁盘IO及PHP中请求初始化、创建执行环境、词法解析、语法解析、编译opcode、请求关闭等诸多耗时过程, 并且不依赖nginx、apache等容器,少了nginx等容器与PHP通信的网络开销,最主要的是资源可以永久保持,不必每次初始化数据库连接等等, 所以使用workerman开发应用程序,性能非常高。

诸多应用
workerman 拥有诸多的应用,如 Thrift-Rpc、Json-Rpc、 聊天室、统计监控服务 以及本站Web程序等. 目前workerman已经被多家公司使用,其中不乏日营业额过亿的电子商务公司用户订单系统的开发,以及大型游戏公司用于游戏后台的开发。

  • 支持分布式部署
    WorkerMan支持分布式部署,可以平滑的动态添加减少服务器而不影响服务质量。从而使得WorkerMan集群能够支持相当大的吞吐量或者并发TCP连接。

  • 支持心跳检测
    Gateway/Worker开发模型支持服务端的心跳检测,可以定时向客户端发送应用层的心跳,能够及时检测到客户端极端掉线情况(掉电、突发网络故障等)。

为什么workerman拥有卓越的性能

  • 避免读取磁盘和反复编译
    workerman运行过程中,单个进程生命周期内只要PHP文件被载入编译过一次,便会常驻内存,不会再去从磁盘读取或者再去编译。 workerman省去了重复的请求初始化、创建执行环境、词法解析、语法解析、编译生成opcode以及请求关闭等诸多耗时的步骤。 实际上workerman运行起来后便几乎没有磁盘IO及PHP文件编译开销,剩下的只是简单的计算过程,这使得workerman运行飞快。

  • 数据或者资源可以全局共享
    workerman中多个请求是可以共享数据或者资源的,当前请求产生的全局变量或者类的静态成员在下一次请求中仍然有效。 这对于减少开销,提高运行效率非常有用。例如业务只要初始化一次数据库连接,那么全局都可以共享这个连接,这实现了真正意义上的数据库长连接。 从而不必每次用户请求都去创建新的连接,避免了连接时三次握手、连接后权限验证以及断开连接时四次挥手等耗时的交互过程。不仅数据库,像redis、 memcache等同样有效。少了这些开销和网络交互,使得workerman运行更快。

  • 没有多余的网络开销
    传统PHP应用程序需要借助apache、nginx等容器才能对外提供网络服务,这就导致多了一层apache、nginx等容器到PHP进程的数据传输开销。 并且由于运行了apache或者nginx容器,这将进一步消耗服务器的资源。 workerman便没有这部分开销,这是由于workerman自身便是一个服务器容器具有PHP进程管理以及网络通讯功能, 完全不依赖于apache、nginx、php-fpm等这些容器便可以独立运行,所以性能更高。

  • 进程模型简单
    workerman是多进程(也有多线程版本)的,可以充分利用服务器多核资源。并且workerman具有简单的进程模型,主进程只负责监控子进程,而每个子进程独自接受维护客户端的连接,独自读取连接上发来的数据,独自处理。 子进程间默认没有任何数据通讯,主进程和子进程之间只有一次信号通讯。简单的进程通讯模型使得workerman相比其它复杂的进程模型的软件更高效。

1、安装workerman

composer require workerman/workerman

2、项目根目录新建一个文件,如 socket.php

将以下内写入此文件

#!/usr/bin/env php
<?php
define('APP_PATH', __DIR__ . '/application/');
define('BIND_MODULE','push/Socket');
// 加载框架引导文件
require __DIR__ . '/thinkphp/start.php';

3、新建socket类

application/push/controller/Socket.php

<?php
namespace app\push\controller;

use app\push\lib\SocketLib;
use Workerman\Lib\Timer;
use Workerman\Worker;

class Socket
{
    public function __construct()
    {
        $config = config('tcp');
        $worker = new Worker('tcp://' . $config['host'] . ':' . $config['port']);

        $worker->onWorkerStart = function(){

        };

        $worker->onMessage = function($connection, $data){
            //接收消息,记录日志
            SocketLib::set_file_log('message is : '. $data, 'message_' . date('Y-m-d'));
            $result = SocketLib::handle_reply($connection, $data);
            //发送消息,记录日志
            SocketLib::set_file_log('send is : '. $result, 'send_' . date('Y-m-d'));
            $connection->send($result);
        };

        //连接时,写日志
        $worker->onConnect = function($connection){
            SocketLib::set_file_log("new connection from ip " . $connection->getRemoteIp() . "\n");
        };

        //发生错误,写入日志
        $worker->onError = function(\Exception $exception){
            SocketLib::set_file_log('error is : '.  $exception->getMessage(), 'error_' . date('Y-m-d'));
        };

        Worker::runAll();
    }
}

4、新建SocketLib.php

application/push/lib/SocketLib.php

<?php
namespace app\push\lib;

use think\Exception;
use Workerman\Lib\Timer;

class SocketLib
{
    //订阅事件类型
    static $_sub_event_list = ['A', 'B', 'C', 'D', 'E', 'F'];
    /**
     * 处理订阅回复事件
     * @param object $con
     * @param string $content
     */
    public static function handle_reply($con, $content)
    {
        try {
            //解析上报内容,统一解析下
            $content = json_decode($content, true);
            $event = isset($content['event']) ? $content['event'] : '';
            //判断当前订阅事件是否合法
            if (in_array($event, self::$_sub_event_list)) {
                return self::$event($content); //根据不同的事件类型,执行相对应的方法
            } else {
                return self::fail($event, '订阅事件不合法');
            }
        } catch (Exception $e) {
            return self::fail($event, $e->getMessage());
        }
    }


    //订阅事件A
    public function A($content){
        //写业务逻辑
    }

    //订阅事件B
    public function A($content){
        //写业务逻辑
    }

    public static function timer($connection)
    {

        Timer::add(5, function() use($connection) {
            $device_id = $redis->lpop('topic_cabinet_IDQUE_list');
            if (! empty($device_id)) {
                //执行下发
                $result = TcpEvents::handle_reply($connection, '{"ORD":"IDQUE","SN":"'.$device_id.'"}');
                $connection->send($result);
            }
            $device_id = $redis->lpop('topic_cabinet_BUCKID_list');
            if (! empty($device_id)) {
                //执行下发
                $result = TcpEvents::handle_reply($connection, '{"ORD":"BUCKID","SN":"'.$device_id.'"}');
                $connection->send($result);
            }
        });
    }

    /**
     * 成功的返回信息
     */
    public static function success($event, $msg = 'success', $extra = [])
    {
        return json_encode(
            array_merge(
                [
                    "event"=> $event,
                    'msg' => $msg
                ],
                $extra
            ),
            JSON_UNESCAPED_UNICODE
        );
    }

    /**
     * 失败的返回信息
     */
    public static function fail($event, $msg = 'fail', $extra = [])
    {
        return json_encode(
            array_merge(
                [
                    "event"=> $event,
                    'msg' => $msg
                ],
                $extra
            ),
            JSON_UNESCAPED_UNICODE
        );
    }

    public static function set_file_log($message, $file = '')
    {
        try {
            $log_file = ROOT_PATH . 'runtime/log/socket/tcp_' . ($file ? $file : date('Y-m-d')) . '.log';
            $file_exists = file_exists($log_file) ? true : false;
            file_put_contents($log_file, 'time is : ' . date('Y-m-d H:i:s') . ' ' .$message. "\r\n", FILE_APPEND);
            if (! $file_exists) @chmod("/somedir/somefile", 0777);
        } catch (Exception $e) {
            $log_file = ROOT_PATH . 'runtime/log/socket/tcp_catch.log';
            $file_exists = file_exists($log_file) ? true : false;
            $message = 'catch message is : ' . $e->getMessage();
            file_put_contents($log_file, 'time is : ' . date('Y-m-d H:i:s') . ' ' .$message. "\r\n", FILE_APPEND);
            if (! $file_exists) @chmod("/somedir/somefile", 0777);
        }

        if (self::DEBUG) echo $message;
    }
}

附注:项目结构

application
    --push
        --controller/Socket.pph     
        --lib/SOcketLib.php
    socket.php