JavaCV的摄像头实战之一:基础 - 程序员欣宸 - 博客园

mikel阅读(56)

来源: JavaCV的摄像头实战之一:基础 – 程序员欣宸 – 博客园

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

关于《JavaCV的摄像头实战》系列

  • 《JavaCV的摄像头实战》顾名思义,是使用JavaCV框架对摄像头进行各种处理的实战集合,这是欣宸作为一名Java程序员,在计算机视觉(computer vision)领域的一个原创系列,通过连续的编码实战,与您一同学习掌握视频、音频、图片等资源的各种操作
  • 另外要说明的是,整个系列使用的摄像头是USB摄像图或者笔记本的内置摄像头,并非基于网络访问的智能摄像头

本篇概览

  • 作为整个系列的开篇,本文非常重要,从环境到代码的方方面面,都会为后续文章打好基础,简单来说本篇由以下内容构成:
  1. 环境和版本信息
  2. 基本套路分析
  3. 基本框架编码
  4. 部署媒体服务器
  • 接下来就从环境和版本信息开始吧

环境和版本信息

  • 现在就把实战涉及的软硬件环境交代清楚,您可以用来参考:
  1. 操作系统:win10
  2. JDK:1.8.0_291
  3. maven:3.8.1
  4. IDEA:2021.2.2(Ultimate Edition)
  5. JavaCV:1.5.6
  6. 媒体服务器:基于dockek部署的nginx-rtmp,镜像是:alfg/nginx-rtmp:v1.3.1

源码下载

名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在javacv-tutorials文件夹下,如下图红框所示:

在这里插入图片描述

  • javacv-tutorials里面有多个子工程,《JavaCV的摄像头实战》系列的代码在simple-grab-push工程下:

在这里插入图片描述

基本套路分析

  • 全系列有多个基于摄像头的实战,例如窗口预览、把视频保存为文件、把视频推送到媒体服务器等,其基本套路是大致相同的,用最简单的流程图表示如下:

在这里插入图片描述

  • 从上图可见,整个流程就是不停的从摄像头取帧,然后处理和输出

基本框架编码

  • 看过了上面基本套路,聪明的您可能会有这样的想法:既然套路是固定的,那代码也可以按套路固定下来吧
  • 没错,接下来就考虑如何把代码按照套路固定下来,我的思路是开发名为AbstractCameraApplication的抽象类,作为《JavaCV的摄像头实战》系列每个应用的父类,它负责搭建整个初始化、取帧、处理、输出的流程,它的子类则专注帧数据的具体处理和输出,整个体系的UML图如下所示:

在这里插入图片描述

  • 接下来就该开发抽象类AbstractCameraApplication.java了,编码前先设计,下图是AbstractCameraApplication的主要方法和执行流程,粗体全部是方法名,红色块代表留给子类实现的抽象方法:

在这里插入图片描述

  • 接下来是创建工程,我这里创建的是maven工程,pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>javacv-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.bolingcavalry</groupId>
    <version>1.0-SNAPSHOT</version>
    <artifactId>simple-grab-push</artifactId>
    <packaging>jar</packaging>

    <properties>
        <!-- javacpp当前版本 -->
        <javacpp.version>1.5.6</javacpp.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.13.3</version>
        </dependency>

        <!-- javacv相关依赖,一个就够了 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacpp.version}</version>
        </dependency>
    </dependencies>
</project>
  • 接下来就是AbstractCameraApplication.java的完整代码,这些代码的流程和方法命名都与上图保持一致,并且添加了详细的注释,有几处要注意的地方稍后会提到:
package com.bolingcavalry.grabpush.camera;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.*;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Scalar;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author will
 * @email zq2599@gmail.com
 * @date 2021/11/19 8:07 上午
 * @description 摄像头应用的基础类,这里面定义了拉流和推流的基本流程,子类只需实现具体的业务方法即可
 */
@Slf4j
public abstract class AbstractCameraApplication {

    /**
     * 摄像头序号,如果只有一个摄像头,那就是0
     */
    protected static final int CAMERA_INDEX = 0;

    /**
     * 帧抓取器
     */
    protected FrameGrabber grabber;

    /**
     * 输出帧率
     */
    @Getter
    private final double frameRate = 30;

    /**
     * 摄像头视频的宽
     */
    @Getter
    private final int cameraImageWidth = 1280;

    /**
     * 摄像头视频的高
     */
    @Getter
    private final int cameraImageHeight = 720;

    /**
     * 转换器
     */
    private final OpenCVFrameConverter.ToIplImage openCVConverter = new OpenCVFrameConverter.ToIplImage();

    /**
     * 实例化、初始化输出操作相关的资源
     */
    protected abstract void initOutput() throws Exception;

    /**
     * 输出
     */
    protected abstract void output(Frame frame) throws Exception;

    /**
     * 释放输出操作相关的资源
     */
    protected abstract void releaseOutputResource() throws Exception;

    /**
     * 两帧之间的间隔时间
     * @return
     */
    protected int getInterval() {
        // 假设一秒钟15帧,那么两帧间隔就是(1000/15)毫秒
        return (int)(1000/ frameRate);
    }

    /**
     * 实例化帧抓取器,默认OpenCVFrameGrabber对象,
     * 子类可按需要自行覆盖
     * @throws FFmpegFrameGrabber.Exception
     */
    protected void instanceGrabber() throws FrameGrabber.Exception {
        grabber = new OpenCVFrameGrabber(CAMERA_INDEX);
    }

    /**
     * 用帧抓取器抓取一帧,默认调用grab()方法,
     * 子类可以按需求自行覆盖
     * @return
     */
    protected Frame grabFrame() throws FrameGrabber.Exception {
        return grabber.grab();
    }

    /**
     * 初始化帧抓取器
     * @throws Exception
     */
    protected void initGrabber() throws Exception {
        // 实例化帧抓取器
        instanceGrabber();

        // 摄像头有可能有多个分辨率,这里指定
        // 可以指定宽高,也可以不指定反而调用grabber.getImageWidth去获取,
        grabber.setImageWidth(cameraImageWidth);
        grabber.setImageHeight(cameraImageHeight);

        // 开启抓取器
        grabber.start();
    }

    /**
     * 预览和输出
     * @param grabSeconds 持续时长
     * @throws Exception
     */
    private void grabAndOutput(int grabSeconds) throws Exception {
        // 添加水印时用到的时间工具
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        long endTime = System.currentTimeMillis() + 1000L *grabSeconds;

        // 两帧输出之间的间隔时间,默认是1000除以帧率,子类可酌情修改
        int interVal = getInterval();

        // 水印在图片上的位置
        org.bytedeco.opencv.opencv_core.Point point = new org.bytedeco.opencv.opencv_core.Point(15, 35);

        Frame captureFrame;
        Mat mat;

        // 超过指定时间就结束循环
        while (System.currentTimeMillis()<endTime) {
            // 取一帧
            captureFrame = grabFrame();

            if (null==captureFrame) {
                log.error("帧对象为空");
                break;
            }

            // 将帧对象转为mat对象
            mat = openCVConverter.convertToMat(captureFrame);

            // 在图片上添加水印,水印内容是当前时间,位置是左上角
            opencv_imgproc.putText(mat,
                    simpleDateFormat.format(new Date()),
                    point,
                    opencv_imgproc.CV_FONT_VECTOR0,
                    0.8,
                    new Scalar(0, 200, 255, 0),
                    1,
                    0,
                    false);

            // 子类输出
            output(openCVConverter.convert(mat));

            // 适当间隔,让肉感感受不到闪屏即可
            if(interVal>0) {
                Thread.sleep(interVal);
            }
        }

        log.info("输出结束");
    }

    /**
     * 释放所有资源
     */
    private void safeRelease() {
        try {
            // 子类需要释放的资源
            releaseOutputResource();
        } catch (Exception exception) {
            log.error("do releaseOutputResource error", exception);
        }

        if (null!=grabber) {
            try {
                grabber.close();
            } catch (Exception exception) {
                log.error("close grabber error", exception);
            }
        }
    }

    /**
     * 整合了所有初始化操作
     * @throws Exception
     */
    private void init() throws Exception {
        long startTime = System.currentTimeMillis();

        // 设置ffmepg日志级别
        avutil.av_log_set_level(avutil.AV_LOG_INFO);
        FFmpegLogCallback.set();

        // 实例化、初始化帧抓取器
        initGrabber();

        // 实例化、初始化输出操作相关的资源,
        // 具体怎么输出由子类决定,例如窗口预览、存视频文件等
        initOutput();

        log.info("初始化完成,耗时[{}]毫秒,帧率[{}],图像宽度[{}],图像高度[{}]",
                System.currentTimeMillis()-startTime,
                frameRate,
                cameraImageWidth,
                cameraImageHeight);
    }

    /**
     * 执行抓取和输出的操作
     */
    public void action(int grabSeconds) {
        try {
            // 初始化操作
            init();
            // 持续拉取和推送
            grabAndOutput(grabSeconds);
        } catch (Exception exception) {
            log.error("execute action error", exception);
        } finally {
            // 无论如何都要释放资源
            safeRelease();
        }
    }
}
  • 上述代码有以下几处要注意:
  1. 负责从摄像头取数据的是OpenCVFrameGrabber对象,即帧抓取器
  2. initGrabber方法中,通过setImageWidth和setImageHeight方法为帧抓取器设置图像的宽和高,其实也可以不用设置宽高,由帧抓取器自动适配,但是考虑到有些摄像头支持多种分辨率,所以还是按照自己的实际情况来主动设置
  3. grabAndOutput方法中,使用了while循环来不断地取帧、处理、输出,这个while循环的结束条件是指定时长,这样的结束条件可能满足不了您的需要,请按照您的实际情况自行调整(例如检测某个按键是否按下)
  4. grabAndOutput方法中,将取到的帧转为Mat对象,然后在Mat对象上添加文字,内容是当前时间,再将Mat对象转为帧对象,将此帧对象传给子类的output方法,如此一来,子类做处理和输出的时候,拿到的帧都有了时间水印
  • 至此,父类已经完成,接下来的实战,咱们只要专注用子类处理和输出帧数据即可

部署媒体服务器

  • 《JavaCV的摄像头实战》系列的一些实战涉及到推流和远程播放,这就要用到流媒体服务器了,流媒体服务器的作用如下图,咱们也在这一篇提前部署好:

在这里插入图片描述

  • 关于媒体服务器的类型,我选的是常用的nginx-rtmp,简单起见,找了一台linux电脑,在上面用docker来部署,也就是一行命令的事儿:
docker run -d --name nginx_rtmp -p 1935:1935 -p 18080:80 alfg/nginx-rtmp:v1.3.1
  • 另外还有个特殊情况,就是我这边有个闲置的树莓派3B,也可以用来做媒体服务器,也是用docker部署的,这里要注意镜像要选用shamelesscookie/nginx-rtmp-ffmpeg:latest,这个镜像有ARM64版本,适合在树莓派上使用:
docker run -d --name nginx_rtmp -p 1935:1935 -p 18080:80 shamelesscookie/nginx-rtmp-ffmpeg:latest
  • 至此,《JavaCV的摄像头实战》系列的准备工作已经完成,接下来的文章,开始精彩的体验之旅吧,欣宸原创,必不让您失望~

你不孤单,欣宸原创一路相伴

https://github.com/zq2599/blog_demos

2种方法教你,如何将exe注册为windows服务,直接从后台运行 - 知乎

mikel阅读(132)

来源: 2种方法教你,如何将exe注册为windows服务,直接从后台运行 – 知乎

方法一:使用windows自带的命令sc

首先我们要打开cmd,下面的命令在cmd中运行,最好使用管理员运行cmd

注册服务:

sc create ceshi binpath= D:\ceshi\ceshi.exe type= own start= auto displayname= ceshi

binpath:你的应用程序所在的路径。

displayname:服务显示的名称

如何判断服务是否注册成功:

在cmd中输入services.msc打开系统服务,查看是否出现ceshi名称的服务(即displayname=后面的参数,我这里是ceshi

or

按下面的方式尝试启动服务

启动服务

net start ceshi

停止服务

net stop ceshi

删除服务

sc delete "ceshi"

方法二:使用instsrv+srvany

使用方法一,如果你的exe不符合服务的规范,启动有可能会失败

这种情况下,我们使用instsrv+srvany

什么是instsrv+srvany

instsrv.exe.exe和srvany.exe是Microsoft Windows Resource Kits工具集中 的两个实用工具,这两个工具配合使用可以将任何的exe应用程序作为window服务运行。

srany.exe是注册程序的服务外壳,可以通过它让应用程序以system账号启动,可以使应用程序作为windows的服务随机器启动而自动启动,从而隐藏不必要的窗口

下载:

链接:pan.baidu.com/s/1gKu_Ww 提取码:s1vm

window64位系统

安装

  1. 将instsrv.exe和srvany.exe拷贝到C:\WINDOWS\SysWOW64目录下
  2. 打开cmd
  3. 运行命令:instsrv MyService C:\WINDOWS\SysWOW64\srvany.exe

注意:Myservice是自定义的服务的名称,可以根据应用程序名称任意更改

运行成功!

配置

  1. 打开注册表:(cmd中输入:regedit
  2. ctrl+F,搜索Myservice(之前自定义的服务名称)
  3. 右击Myservice新建项,名称为Parameters
  4. 之后在Parameters中新建几个字符串值
  • 名称 Application 值:你要作为服务运行的程序地址。
  • 名称 AppDirectory 值:你要作为服务运行的程序所在文件夹路径。
  • 名称 AppParameters 值:你要作为服务运行的程序启动所需要的参数。

之后启动服务Myservice即可后台运行exe!

window32位系统

安装

  1. 将instsrv.exe和srvany.exe拷贝到C:\WINDOWS\system32目录下
  2. 打开cmd
  3. 运行命令:instsrv MyService C:\WINDOWS\system32\srvany.exe

注意:Myservice是自定义的服务的名称,可以根据应用程序名称任意更改

运行成功!

我这里是64位。

配置

  1. 打开注册表:(cmd中输入:regedit)
  2. ctrl+F,搜索Myservice(之前自定义的服务名称)
  3. 右击Myservice新建项,名称为Parameters
  4. 之后在Parameters中新建几个字符串值
  • 名称 Application 值:你要作为服务运行的程序地址。
  • 名称 AppDirectory 值:你要作为服务运行的程序所在文件夹路径。
  • 名称 AppParameters 值:你要作为服务运行的程序启动所需要的参数。

之后启动服务Myservice即可后台运行exe!

gatewayworker在events的onWorkerStart回调中如何获取到worker进程,避免多进程多个定时器并发启动的问题

mikel阅读(143)

问题代码

class Events
{
public static function onWorkerStart()
{

        Timer::add(1, function(){
            //echo "timer:".time()."\n";
            list($msec, $sec) = explode(' ', microtime());
            $msectime = (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
            $time=array();
            $time['time']=$msectime;
            $message = array();
            $message["code"]=201;//定时器
            $message["message"]='time '.$msectime;
            $message["data"]=$time;
            Gateway::sendToAll(json_encode($message));
        });
}
这个定时器代码每秒群发消息给客户端进行倒计时,当多进程的时候每个进程都会启动一个定时器来群发消息,在客户端访问不频繁的情况下,不会出现阻塞的情况,当客户端和服务器间频繁发送消息的时候,客户端一次接受所有进程的消息,导致处理不过来卡顿的问题会很严重,其实只需要一个进程启动定时器,群发消息即可于是对进程id进行了判断,修改过的代码如下:
class Events
{
    public static function onWorkerStart($worker)
    {
        
        //Gateway::sendToAll('workerid'.$businessWorker->id);
        if($worker->id === 0){
            Timer::add(1, function(){
                //echo "timer:".time()."\n";
    			list($msec, $sec) = explode(' ', microtime());
    			$msectime = (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
    			$time=array();
    			$time['time']=$msectime;
    			$message = array();
    			$message["code"]=201;//定时器
    			$message["message"]='time '.$msectime;
    			$message["data"]=$time;
    			Gateway::sendToAll(json_encode($message));
            });
        }
    }

参考文章:https://www.workerman.net/q/4568

深入理解GatewayWorker框架 - 简书

mikel阅读(82)

来源: 深入理解GatewayWorker框架 – 简书

序言

本文只是结合GatewayWorker和Workerman的官方文档和源码,深入了解执行过程。以便更深入的了解并使用

GatewayWorker基于Workerman开发的一个项目框架。Register进程负责保存Gateway进程和BusinessWorker进程的地址,建立两者的连接。Gateway进程负责维持客户端连接,并转发客户端的数据给BusinessWorker进程处理,BusinessWorker进程负责处理实际的业务逻辑(默认调用Events.php处理业务),并将结果推送给对应的客户端。Register、Gateway、BusinessWorker进程都是继承Worker类实现各自的功能,所以了解GatewayWorker框架的内部执行过程,需要优先理解Worker的内容

GatewayWorker目录结构

├── Applications // 这里是所有开发者应用项目
│   └── YourApp  // 其中一个项目目录,目录名可以自定义
│       ├── Events.php // 开发者只需要关注这个文件
│       ├── start_gateway.php // gateway进程启动脚本,包括端口        号等设置
│       ├── start_businessworker.php // businessWorker进程启动  脚本
│       └── start_register.php // 注册服务启动脚本
│
├── start.php // 全局启动脚本,此脚本会依次加载Applications/项目/start_*.php启动脚本
│
└── vendor    // GatewayWorker框架和Workerman框架源码目  录,此目录开发者不用关心

start.php 为启动脚本,在该脚本中,统一加载start_gateway.php start_businessworker.php start_register.php进程脚本,最后通过Worker::runAll();运行所有服务。

工作原理

1、Register、Gateway、BusinessWorker进程启动
2、Gateway、BusinessWorker进程启动后向Register服务进程发起长连接注册自己
3、Register服务收到Gateway的注册后,把所有Gateway的通讯地址保存在内存中
4、Register服务收到BusinessWorker的注册后,把内存中所有的Gateway的通讯地址发给BusinessWorker
5、BusinessWorker进程得到所有的Gateway内部通讯地址后尝试连接Gateway
6、如果运行过程中有新的Gateway服务注册到Register(一般是分布式部署加机器),则将新的Gateway内部通讯地址列表将广播给所有BusinessWorker,BusinessWorker收到后建立连接
7 、如果有Gateway下线,则Register服务会收到通知,会将对应的内部通讯地址删除,然后广播新的内部通讯地址列表给所有BusinessWorker,BusinessWorker不再连接下线的Gateway
8、至此Gateway与BusinessWorker通过Register已经建立起长连接
9、客户端的事件及数据全部由Gateway转发给BusinessWorker处理,BusinessWorker默认调用Events.php中的onConnect onMessage onClose处理业务逻辑。
10、BusinessWorker的业务逻辑入口全部在Events.php中,包括onWorkerStart进程启动事件(进程事件)、onConnect连接事件(客户端事件)、onMessage消息事件(客户端事件)、onClose连接关闭事件(客户端事件)、onWorkerStop进程退出事件(进程事件)

1 Register、Gateway、BusinessWorker进程启动

项目根目录下的start.php 为启动脚本,在该脚本中,加载start_gateway.php start_businessworker.php start_register.php进程脚本,完成各个服务的Worker初始化:

// 加载所有Applications/*/start.php,以便启动所有服务
foreach(glob(__DIR__.'/Applications/*/start*.php') as $start_file)
{
    require_once $start_file;
}

最后通过Worker::runAll();运行所有服务。

// 运行所有服务
Worker::runAll();
运行所有服务,先看一遍runAll()方法的执行内容

public static function runAll()
{
    // 检查运行环境
    self::checkSapiEnv();
    //初始化环境变量
    self::init();
    // 解析命令
    self::parseCommand();
    // 尝试以守护进程模式运行
    self::daemonize();
    // 初始化所有worker实例,主要是监听端口
    self::initWorkers();
    // 初始化所有信号处理函数
    self::installSignal();
    // 保存主进程pid
    self::saveMasterPid();
    // 展示启动界面
    self::displayUI();
    // 创建子进程(worker进程),然后给每个子进程绑定loop循环监听事件tcp
    self::forkWorkers();
    // 尝试重定向标准输入输出
    self::resetStd();
    // 监控所有子进程(worker进程)
    self::monitorWorkers();
}

self::init()初始化环境变量中,有以下部分代码,保存$_idMap从PID映射到工作进程ID

// Init data for worker id.
   self::initId();
protected static function initId()
 {
   foreach (self::$_workers as $worker_id => $worker) {
       $new_id_map = array();
       for($key = 0; $key < $worker->count; $key++) {
          $new_id_map[$key] = isset(self::$_idMap[$worker_id]      [$key]) ? self::$_idMap[$worker_id][$key] : 0;
       }
       self::$_idMap[$worker_id] = $new_id_map;
   }
 }

self::forkWorkers()方法通过循环self::$_workers数组,fork各自worker的count数量的进程。然后通过调用

$worker->run();

运行当前worker实例,在run方法中通过

  if (!self::$globalEvent) {
       $event_loop_class = self::getEventLoopName();
      self::$globalEvent = new $event_loop_class;
      // Register a listener to be notified when server socket is ready to read.
       if ($this->_socketName) {
           if ($this->transport !== 'udp') {
               self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ,
                   array($this, 'acceptConnection'));
           } else {
               self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ,
                   array($this, 'acceptUdpConnection'));
           }
       }

获取一个当前可用的事件轮询方式,然后根据当前的协议类型添加一个监听到事件轮询中
然后,尝试出发当前进程模型的onWorkerStart回调,此回调会在Gateway类以及BusinessWorker类中都会定义,代码

 if ($this->onWorkerStart) {
       try {
           call_user_func($this->onWorkerStart, $this);
       } catch (\Exception $e) {
           self::log($e);
           // Avoid rapid infinite loop exit.
           sleep(1);
           exit(250);
       } catch (\Error $e) {
           self::log($e);
           // Avoid rapid infinite loop exit.
           sleep(1);
           exit(250);
       }
   }

最后,执行事件的循环等待socket事件,处理读写等操作,代码

 // Main loop.
   self::$globalEvent->loop();

以上是runAll()方法的部分内容,会在了解GatewayWorker的工作原理的时候用到

2.1 Gateway进程向Register服务进程发起长连接注册自己

初始化Gateway

$gateway = new Gateway("text://0.0.0.0:8383");

在Gateway类中重写run方法,当调用runAll()方法启动进程时,fork进程之后,运行worker实例的时候,会调用到此重写的run方法

public function run()
{
    // 保存用户的回调,当对应的事件发生时触发
    $this->_onWorkerStart = $this->onWorkerStart;
    $this->onWorkerStart  = array($this, 'onWorkerStart');
    // 保存用户的回调,当对应的事件发生时触发
    $this->_onConnect = $this->onConnect;
    $this->onConnect  = array($this, 'onClientConnect');
    // onMessage禁止用户设置回调
    $this->onMessage = array($this, 'onClientMessage');
    // 保存用户的回调,当对应的事件发生时触发
    $this->_onClose = $this->onClose;
    $this->onClose  = array($this, 'onClientClose');
    // 保存用户的回调,当对应的事件发生时触发
    $this->_onWorkerStop = $this->onWorkerStop;
    $this->onWorkerStop  = array($this, 'onWorkerStop');
    $this->_startTime = time();
    // 运行父方法
    parent::run();
}

定义了$this->onWorkerStart回调,

$this->onWorkerStart  = array($this, 'onWorkerStart');

 

 

执行到Worker类中的run()方法时,被触发。即,上边提到的

Worker脚本中的run方法

调用Gateway类中的onWorkerStart方法,代码

public function onWorkerStart()
{
    $this->lanPort = $this->startPort + $this->id;
    if ($this->pingInterval > 0) {
        $timer_interval = $this->pingNotResponseLimit > 0 ? $this->pingInterval / 2 : $this->pingInterval;
        Timer::add($timer_interval, array($this, 'ping'));
    }
    if ($this->lanIp !== '127.0.0.1') {
        Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingBusinessWorker'));
    }
    if (strpos($this->registerAddress, '127.0.0.1') !== 0) {
        Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingRegister'));
    }
    if (!class_exists('\Protocols\GatewayProtocol')) {
        class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
    }
    // 初始化 gateway 内部的监听,用于监听 worker 的连接已经连接上发来的数据
    $this->_innerTcpWorker = new Worker("GatewayProtocol://{$this->lanIp}:{$this->lanPort}");
    $this->_innerTcpWorker->listen();
    // 重新设置自动加载根目录
    Autoloader::setRootPath($this->_autoloadRootPath);
    // 设置内部监听的相关回调
    $this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage');
    $this->_innerTcpWorker->onConnect = array($this, 'onWorkerConnect');
    $this->_innerTcpWorker->onClose   = array($this, 'onWorkerClose');
    // 注册 gateway 的内部通讯地址,worker 去连这个地址,以便 gateway 与 worker 之间建立起 TCP 长连接
    $this->registerAddress();
    if ($this->_onWorkerStart) {
        call_user_func($this->_onWorkerStart, $this);
    }
}

$this->startPort : 内部通讯起始端口,假如$gateway->count=4,起始端口为4000,可在gateway启动脚本中自定义
$this->id : 基于worker实例分配的进程编号,当前从0开始,根据count自增。在fork进程的时候生成

Worker.php

$this->_innerTcpWorker:用于监听 worker 的连接已经连接上发来的数据。在工作原理5中,BusinessWorker进程得到所有的Gateway内部通讯地址后尝试连接Gateway以及其他两者之间的通信(连接,消息,关闭)会被调用
$this->registerAddress(): 代码中$this->registerAddress是在start_gateway.php初始化Gateway类之后定义的。该端口是Register进程所监听。此处异步的向Register进程发送数据,存储当前 Gateway 的内部通信地址 

public function registerAddress()
{
    $address                   = $this->lanIp . ':' . $this->lanPort;
    $this->_registerConnection = new AsyncTcpConnection("text://{$this->registerAddress}");
    $this->_registerConnection->send('{"event":"gateway_connect", "address":"' . $address . '", "secret_key":"' . $this->secretKey . '"}');
    $this->_registerConnection->onClose = array($this, 'onRegisterConnectionClose');
    $this->_registerConnection->connect();
}

$this->lanIp: Gateway所在服务器的内网IP

2.2 BusinessWorker进程向Register服务进程发起长连接注册自己

BusinessWorker类中同样重写run方法,定义了$this->onWorkerStart

 public function run()
 {
    $this->_onWorkerStart  = $this->onWorkerStart;
    $this->_onWorkerReload = $this->onWorkerReload;
    $this->_onWorkerStop = $this->onWorkerStop;
    $this->onWorkerStop   = array($this, 'onWorkerStop');
    $this->onWorkerStart   = array($this, 'onWorkerStart');
    $this->onWorkerReload  = array($this, 'onWorkerReload');
    parent::run();
 }

执行Worker类中的run方法,触发BusinessWorker中的onWorkerStart

protected function onWorkerStart()
{
    if (!class_exists('\Protocols\GatewayProtocol')) {
        class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol');
    }
    $this->connectToRegister();
    \GatewayWorker\Lib\Gateway::setBusinessWorker($this);
    \GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey;
    if ($this->_onWorkerStart) {
        call_user_func($this->_onWorkerStart, $this);
    }
    
    if (is_callable($this->eventHandler . '::onWorkerStart')) {
        call_user_func($this->eventHandler . '::onWorkerStart', $this);
    }

    if (function_exists('pcntl_signal')) {
        // 业务超时信号处理
        pcntl_signal(SIGALRM, array($this, 'timeoutHandler'), false);
    } else {
        $this->processTimeout = 0;
    }

    // 设置回调
    if (is_callable($this->eventHandler . '::onConnect')) {
        $this->_eventOnConnect = $this->eventHandler . '::onConnect';
    }

    if (is_callable($this->eventHandler . '::onMessage')) {
        $this->_eventOnMessage = $this->eventHandler . '::onMessage';
    } else {
        echo "Waring: {$this->eventHandler}::onMessage is not callable\n";
    }

    if (is_callable($this->eventHandler . '::onClose')) {
        $this->_eventOnClose = $this->eventHandler . '::onClose';
    }

    // 如果Register服务器不在本地服务器,则需要保持心跳
    if (strpos($this->registerAddress, '127.0.0.1') !== 0) {
        Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingRegister'));
    }
}

通过connectToRegister方法,发送数据到Register进程,连接服务注册中心

public function connectToRegister()
{
    $this->_registerConnection = new AsyncTcpConnection("text://{$this->registerAddress}");
    $this->_registerConnection->send('{"event":"worker_connect","secret_key":"' . $this->secretKey . '"}');
    $this->_registerConnection->onClose   = array($this, 'onRegisterConnectionClose');
    $this->_registerConnection->onMessage = array($this, 'onRegisterConnectionMessage');
    $this->_registerConnection->connect();
}

3 Register服务收到Gateway的注册后,把所有的Gateway的通讯地址保存在内存中

在Register类中,重写了run方法,定义了当前的

     $this->onConnect = array($this, 'onConnect');
    // 设置 onMessage 回调
    $this->onMessage = array($this, 'onMessage');

    // 设置 onClose 回调
    $this->onClose = array($this, 'onClose');

三个属性,当Register启动的进程收到消息时,会触发onMessage方法

 public function onMessage($connection, $buffer)
{
    // 删除定时器
    Timer::del($connection->timeout_timerid);
    $data       = @json_decode($buffer, true);
    if (empty($data['event'])) {
        $error = "Bad request for Register service. Request info(IP:".$connection->getRemoteIp().", Request Buffer:$buffer). See http://wiki.workerman.net/Error4 for detail";
        Worker::log($error);
        return $connection->close($error);
    }
    $event      = $data['event'];
    $secret_key = isset($data['secret_key']) ? $data['secret_key'] : '';
    // 开始验证
    switch ($event) {
        // 是 gateway 连接
        case 'gateway_connect':
            if (empty($data['address'])) {
                echo "address not found\n";
                return $connection->close();
            }
            if ($secret_key !== $this->secretKey) {
                Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
                return $connection->close();
            }
            $this->_gatewayConnections[$connection->id] = $data['address'];
            $this->broadcastAddresses();
            break;
        // 是 worker 连接
        case 'worker_connect':
            if ($secret_key !== $this->secretKey) {
                Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true));
                return $connection->close();
            }
            $this->_workerConnections[$connection->id] = $connection;
            $this->broadcastAddresses($connection);
            break;
        case 'ping':
            break;
        default:
            Worker::log("Register unknown event:$event IP: ".$connection->getRemoteIp()." Buffer:$buffer. See http://wiki.workerman.net/Error4 for detail");
            $connection->close();
    }
}

当$event = ‘gateway_connect’时,是Gateway发来的注册消息,保存到$this->_gatewayConnections数组中,在通过broadcastAddresses方法将当前$this->_gatewayConnections中所有的Gatewat通讯地址转发给所有BusinessWorker进程

4 Register服务收到BusinessWorker的注册后,把内存中所有的Gateway的通讯地址发给BusinessWorker

 

 

同第3步中,Register类收到BusinessWorker的注册时,会触发onMessage方法中的worker_connect,case选项。

image.png

同时,将当前worker连接加入到$_workerConnections数组中,在通过broadcastAddresses方法将当前$this->_gatewayConnections中所有的Gatewat通讯地址转发给所有BusinessWorker进程。

5 BusinessWorker进程得到所有的Gateway内部通讯地址后尝试连接Gateway

在BusinessWoker类的启动中,通过重写run方法,定义的启动onWorkerStart方法中,通过connectToRegister方法注册服务中心的同时,也定义了onMessage匿名函数,用于接收消息回调。

$this->_registerConnection->onMessage = array($this, 'onRegisterConnectionMessage');

即,当注册中心发来消息时候,回调到此处

 public function onRegisterConnectionMessage($register_connection, $data)
{
    $data = json_decode($data, true);
    if (!isset($data['event'])) {
        echo "Received bad data from Register\n";
        return;
    }
    $event = $data['event'];
    switch ($event) {
        case 'broadcast_addresses':
            if (!is_array($data['addresses'])) {
                echo "Received bad data from Register. Addresses empty\n";
                return;
            }
            $addresses               = $data['addresses'];
            $this->_gatewayAddresses = array();
            foreach ($addresses as $addr) {
                $this->_gatewayAddresses[$addr] = $addr;
            }
            $this->checkGatewayConnections($addresses);
            break;
        default:
            echo "Receive bad event:$event from Register.\n";
    }
}

其中Register类发来的数据是

$data   = array(
        'event'     => 'broadcast_addresses',
        'addresses' => array_unique(array_values($this->_gatewayConnections)),
    );

这个时候,就会通过checkGatewayConnections方法检查gateway的这些通信端口是否都已经连接,在通过tryToConnectGateway方法尝试连接gateway的这些内部通信地址

6 Gateway进程收到BusinessWorker进程的连接消息

同样,在Gateway进程启动的时候,触发的onWorkerStart方法中,也定义了一个内部通讯的onWorkerMessage

$this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage');

由此来接收BusinessWorker进程发来的连接消息,部分代码

public function onWorkerMessage($connection, $data)
{
    $cmd = $data['cmd'];
    if (empty($connection->authorized) && $cmd !== GatewayProtocol::CMD_WORKER_CONNECT && $cmd !== GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT) {
        self::log("Unauthorized request from " . $connection->getRemoteIp() . ":" . $connection->getRemotePort());
        return $connection->close();
    }
    switch ($cmd) {
        // BusinessWorker连接Gateway
        case GatewayProtocol::CMD_WORKER_CONNECT:
            $worker_info = json_decode($data['body'], true);
            if ($worker_info['secret_key'] !== $this->secretKey) {
                self::log("Gateway: Worker key does not match ".var_export($this->secretKey, true)." !== ". var_export($this->secretKey));
                return $connection->close();
            }
            $key = $connection->getRemoteIp() . ':' . $worker_info['worker_key'];
            // 在一台服务器上businessWorker->name不能相同
            if (isset($this->_workerConnections[$key])) {
                self::log("Gateway: Worker->name conflict. Key:{$key}");
        $connection->close();
                return;
            }
    $connection->key = $key;
            $this->_workerConnections[$key] = $connection;
            $connection->authorized = true;
            return;
        // GatewayClient连接Gateway

将worker的进程连接保存到$this->_workerConnections[$key] = $connection;

7 Gateway进程收到客户端的连接,消息时,会通过Gateway转发给worker处理

 // Gateway类的run方法中定义此属性
 $this->onMessage = array($this, 'onClientMessage');
 
 // 收到客户端消息的时候出发此函数
 public function onClientMessage($connection, $data)
 {
    $connection->pingNotResponseCount = -1;
    $this->sendToWorker(GatewayProtocol::CMD_ON_MESSAGE, $connection, $data);
 }

在sendToWorker方法中,将数据发给worker进程处理

作者:AntFoot
链接:https://www.jianshu.com/p/47a9f2f0c18b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

深入理解GatewayWorker框架_houzhyan-博客-CSDN博客_gatewayworker

mikel阅读(79)

$this->id

来源: 深入理解GatewayWorker框架_houzhyan-博客-CSDN博客_gatewayworker

原文地址:http://www.php-master.com/post/342621.html

序言

本文只是结合GatewayWorker和Workerman的官方文档和源码,深入了解执行过程。以便更深入的了解并使用

GatewayWorker基于Workerman开发的一个项目框架。Register进程负责保存Gateway进程和BusinessWorker进程的地址,建立两者的连接。Gateway进程负责维持客户端连接,并转发客户端的数据给BusinessWorker进程处理,BusinessWorker进程负责处理实际的业务逻辑(默认调用Events.php处理业务),并将结果推送给对应的客户端。Register、Gateway、BusinessWorker进程都是继承Worker类实现各自的功能,所以了解GatewayWorker框架的内部执行过程,需要优先理解Worker的内容

GatewayWorker目录结构

  1. ├── Applications // 这里是所有开发者应用项目
  2. │ └── YourApp // 其中一个项目目录,目录名可以自定义
  3. │ ├── Events.php // 开发者只需要关注这个文件
  4. │ ├── start_gateway.php // gateway进程启动脚本,包括端口 号等设置
  5. │ ├── start_businessworker.php // businessWorker进程启动 脚本
  6. │ └── start_register.php // 注册服务启动脚本
  7. ├── start.php // 全局启动脚本,此脚本会依次加载Applications/项目/start_*.php启动脚本
  8. └── vendor // GatewayWorker框架和Workerman框架源码目 录,此目录开发者不用关心

start.php 为启动脚本,在该脚本中,统一加载start_gateway.php start_businessworker.php start_register.php进程脚本,最后通过Worker::runAll();运行所有服务。

工作原理

  1. 1、Register、Gateway、BusinessWorker进程启动
  2. 2、Gateway、BusinessWorker进程启动后向Register服务进程发起长连接注册自己
  3. 3、Register服务收到Gateway的注册后,把所有Gateway的通讯地址保存在内存中
  4. 4、Register服务收到BusinessWorker的注册后,把内存中所有的Gateway的通讯地址发给BusinessWorker
  5. 5、BusinessWorker进程得到所有的Gateway内部通讯地址后尝试连接Gateway
  6. 6、如果运行过程中有新的Gateway服务注册到Register(一般是分布式部署加机器),则将新的Gateway内部通讯地址列表将广播给所有BusinessWorker,BusinessWorker收到后建立连接
  7. 7 、如果有Gateway下线,则Register服务会收到通知,会将对应的内部通讯地址删除,然后广播新的内部通讯地址列表给所有BusinessWorker,BusinessWorker不再连接下线的Gateway
  8. 8、至此Gateway与BusinessWorker通过Register已经建立起长连接
  9. 9、客户端的事件及数据全部由Gateway转发给BusinessWorker处理,BusinessWorker默认调用Events.php中的onConnect onMessage onClose处理业务逻辑。
  10. 10、BusinessWorker的业务逻辑入口全部在Events.php中,包括onWorkerStart进程启动事件(进程事件)、onConnect连接事件(客户端事件)、onMessage消息事件(客户端事件)、onClose连接关闭事件(客户端事件)、onWorkerStop进程退出事件(进程事件)

1 Register、Gateway、BusinessWorker进程启动

项目根目录下的start.php 为启动脚本,在该脚本中,加载start_gateway.php start_businessworker.php start_register.php进程脚本,完成各个服务的Worker初始化:

  1. // 加载所有Applications/*/start.php,以便启动所有服务
  2. foreach(glob(__DIR__.‘/Applications/*/start*.php’) as $start_file)
  3. {
  4. require_once $start_file;
  5. }

最后通过Worker::runAll();运行所有服务。

  1. // 运行所有服务
  2. Worker::runAll();

运行所有服务,先看一遍runAll()方法的执行内容

  1. public static function runAll()
  2. {
  3. // 检查运行环境
  4. self::checkSapiEnv();
  5. //初始化环境变量
  6. self::init();
  7. // 解析命令
  8. self::parseCommand();
  9. // 尝试以守护进程模式运行
  10. self::daemonize();
  11. // 初始化所有worker实例,主要是监听端口
  12. self::initWorkers();
  13. // 初始化所有信号处理函数
  14. self::installSignal();
  15. // 保存主进程pid
  16. self::saveMasterPid();
  17. // 展示启动界面
  18. self::displayUI();
  19. // 创建子进程(worker进程),然后给每个子进程绑定loop循环监听事件tcp
  20. self::forkWorkers();
  21. // 尝试重定向标准输入输出
  22. self::resetStd();
  23. // 监控所有子进程(worker进程)
  24. self::monitorWorkers();
  25. }

self::init()初始化环境变量中,有以下部分代码,保存$_idMap从PID映射到工作进程ID

  1. // Init data for worker id.
  2. self::initId();
  3. protected static function initId()
  4. {
  5. foreach (self::$_workers as $worker_id => $worker) {
  6. $new_id_map = array();
  7. for($key = 0; $key < $worker->count; $key++) {
  8. $new_id_map[$key] = isset(self::$_idMap[$worker_id] [$key]) ? self::$_idMap[$worker_id][$key] : 0;
  9. }
  10. self::$_idMap[$worker_id] = $new_id_map;
  11. }
  12. }

self::forkWorkers()方法通过循环self::$_workers数组,fork各自worker的count数量的进程。然后通过调用

$worker->run();

运行当前worker实例,在run方法中通过

  1. if (!self::$globalEvent) {
  2. $event_loop_class = self::getEventLoopName();
  3. self::$globalEvent = new $event_loop_class;
  4. // Register a listener to be notified when server socket is ready to read.
  5. if ($this->_socketName) {
  6. if ($this->transport !== ‘udp’) {
  7. self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ,
  8. array($this, ‘acceptConnection’));
  9. } else {
  10. self::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ,
  11. array($this, ‘acceptUdpConnection’));
  12. }
  13. }

获取一个当前可用的事件轮询方式,然后根据当前的协议类型添加一个监听到事件轮询中
然后,尝试出发当前进程模型的onWorkerStart回调,此回调会在Gateway类以及BusinessWorker类中都会定义,代码

  1. if ($this->onWorkerStart) {
  2. try {
  3. call_user_func($this->onWorkerStart, $this);
  4. } catch (\Exception $e) {
  5. self::log($e);
  6. // Avoid rapid infinite loop exit.
  7. sleep(1);
  8. exit(250);
  9. } catch (\Error $e) {
  10. self::log($e);
  11. // Avoid rapid infinite loop exit.
  12. sleep(1);
  13. exit(250);
  14. }
  15. }

最后,执行事件的循环等待socket事件,处理读写等操作,代码

  1. // Main loop.
  2. self::$globalEvent->loop();

以上是runAll()方法的部分内容,会在了解GatewayWorker的工作原理的时候用到

2.1 Gateway进程向Register服务进程发起长连接注册自己

初始化Gateway

$gateway = new Gateway("text://0.0.0.0:8383");

在Gateway类中重写run方法,当调用runAll()方法启动进程时,fork进程之后,运行worker实例的时候,会调用到此重写的run方法

  1. public function run()
  2. {
  3. // 保存用户的回调,当对应的事件发生时触发
  4. $this->_onWorkerStart = $this->onWorkerStart;
  5. $this->onWorkerStart = array($this, ‘onWorkerStart’);
  6. // 保存用户的回调,当对应的事件发生时触发
  7. $this->_onConnect = $this->onConnect;
  8. $this->onConnect = array($this, ‘onClientConnect’);
  9. // onMessage禁止用户设置回调
  10. $this->onMessage = array($this, ‘onClientMessage’);
  11. // 保存用户的回调,当对应的事件发生时触发
  12. $this->_onClose = $this->onClose;
  13. $this->onClose = array($this, ‘onClientClose’);
  14. // 保存用户的回调,当对应的事件发生时触发
  15. $this->_onWorkerStop = $this->onWorkerStop;
  16. $this->onWorkerStop = array($this, ‘onWorkerStop’);
  17. $this->_startTime = time();
  18. // 运行父方法
  19. parent::run();
  20. }

定义了$this->onWorkerStart回调,

$this->onWorkerStart  = array($this, 'onWorkerStart');

 

 

执行到Worker类中的run()方法时,被触发。即,上边提到的

Worker脚本中的run方法

调用Gateway类中的onWorkerStart方法,代码

  1. public function onWorkerStart()
  2. {
  3. $this->lanPort = $this->startPort + $this->id;
  4. if ($this->pingInterval > 0) {
  5. $timer_interval = $this->pingNotResponseLimit > 0 ? $this->pingInterval / 2 : $this->pingInterval;
  6. Timer::add($timer_interval, array($this, ‘ping’));
  7. }
  8. if ($this->lanIp !== ‘127.0.0.1’) {
  9. Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, ‘pingBusinessWorker’));
  10. }
  11. if (strpos($this->registerAddress, ‘127.0.0.1’) !== 0) {
  12. Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, ‘pingRegister’));
  13. }
  14. if (!class_exists(‘\Protocols\GatewayProtocol’)) {
  15. class_alias(‘GatewayWorker\Protocols\GatewayProtocol’, ‘Protocols\GatewayProtocol’);
  16. }
  17. // 初始化 gateway 内部的监听,用于监听 worker 的连接已经连接上发来的数据
  18. $this->_innerTcpWorker = new Worker(“GatewayProtocol://{$this->lanIp}:{$this->lanPort});
  19. $this->_innerTcpWorker->listen();
  20. // 重新设置自动加载根目录
  21. Autoloader::setRootPath($this->_autoloadRootPath);
  22. // 设置内部监听的相关回调
  23. $this->_innerTcpWorker->onMessage = array($this, ‘onWorkerMessage’);
  24. $this->_innerTcpWorker->onConnect = array($this, ‘onWorkerConnect’);
  25. $this->_innerTcpWorker->onClose = array($this, ‘onWorkerClose’);
  26. // 注册 gateway 的内部通讯地址,worker 去连这个地址,以便 gateway 与 worker 之间建立起 TCP 长连接
  27. $this->registerAddress();
  28. if ($this->_onWorkerStart) {
  29. call_user_func($this->_onWorkerStart, $this);
  30. }
  31. }

$this->startPort : 内部通讯起始端口,假如$gateway->count=4,起始端口为4000,可在gateway启动脚本中自定义
$this->id : 基于worker实例分配的进程编号,当前从0开始,根据count自增。在fork进程的时候生成

Worker.php

$this->_innerTcpWorker:用于监听 worker 的连接已经连接上发来的数据。在工作原理5中,BusinessWorker进程得到所有的Gateway内部通讯地址后尝试连接Gateway以及其他两者之间的通信(连接,消息,关闭)会被调用
$this->registerAddress(): 代码中$this->registerAddress是在start_gateway.php初始化Gateway类之后定义的。该端口是Register进程所监听。此处异步的向Register进程发送数据,存储当前 Gateway 的内部通信地址

 

  1. public function registerAddress()
  2. {
  3. $address = $this->lanIp . ‘:’ . $this->lanPort;
  4. $this->_registerConnection = new AsyncTcpConnection(“text://{$this->registerAddress});
  5. $this->_registerConnection->send(‘{“event”:”gateway_connect”, “address”:”‘ . $address . ‘”, “secret_key”:”‘ . $this->secretKey . ‘”}’);
  6. $this->_registerConnection->onClose = array($this, ‘onRegisterConnectionClose’);
  7. $this->_registerConnection->connect();
  8. }

$this->lanIp: Gateway所在服务器的内网IP

2.2 BusinessWorker进程向Register服务进程发起长连接注册自己

BusinessWorker类中同样重写run方法,定义了$this->onWorkerStart

  1. public function run()
  2. {
  3. $this->_onWorkerStart = $this->onWorkerStart;
  4. $this->_onWorkerReload = $this->onWorkerReload;
  5. $this->_onWorkerStop = $this->onWorkerStop;
  6. $this->onWorkerStop = array($this, ‘onWorkerStop’);
  7. $this->onWorkerStart = array($this, ‘onWorkerStart’);
  8. $this->onWorkerReload = array($this, ‘onWorkerReload’);
  9. parent::run();
  10. }

执行Worker类中的run方法,触发BusinessWorker中的onWorkerStart

  1. protected function onWorkerStart()
  2. {
  3. if (!class_exists(‘\Protocols\GatewayProtocol’)) {
  4. class_alias(‘GatewayWorker\Protocols\GatewayProtocol’, ‘Protocols\GatewayProtocol’);
  5. }
  6. $this->connectToRegister();
  7. \GatewayWorker\Lib\Gateway::setBusinessWorker($this);
  8. \GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey;
  9. if ($this->_onWorkerStart) {
  10. call_user_func($this->_onWorkerStart, $this);
  11. }
  12. if (is_callable($this->eventHandler . ‘::onWorkerStart’)) {
  13. call_user_func($this->eventHandler . ‘::onWorkerStart’, $this);
  14. }
  15. if (function_exists(‘pcntl_signal’)) {
  16. // 业务超时信号处理
  17. pcntl_signal(SIGALRM, array($this, ‘timeoutHandler’), false);
  18. } else {
  19. $this->processTimeout = 0;
  20. }
  21. // 设置回调
  22. if (is_callable($this->eventHandler . ‘::onConnect’)) {
  23. $this->_eventOnConnect = $this->eventHandler . ‘::onConnect’;
  24. }
  25. if (is_callable($this->eventHandler . ‘::onMessage’)) {
  26. $this->_eventOnMessage = $this->eventHandler . ‘::onMessage’;
  27. } else {
  28. echo “Waring: {$this->eventHandler}::onMessage is not callable\n”;
  29. }
  30. if (is_callable($this->eventHandler . ‘::onClose’)) {
  31. $this->_eventOnClose = $this->eventHandler . ‘::onClose’;
  32. }
  33. // 如果Register服务器不在本地服务器,则需要保持心跳
  34. if (strpos($this->registerAddress, ‘127.0.0.1’) !== 0) {
  35. Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, ‘pingRegister’));
  36. }
  37. }

通过connectToRegister方法,发送数据到Register进程,连接服务注册中心

  1. public function connectToRegister()
  2. {
  3. $this->_registerConnection = new AsyncTcpConnection(“text://{$this->registerAddress});
  4. $this->_registerConnection->send(‘{“event”:”worker_connect”,”secret_key”:”‘ . $this->secretKey . ‘”}’);
  5. $this->_registerConnection->onClose = array($this, ‘onRegisterConnectionClose’);
  6. $this->_registerConnection->onMessage = array($this, ‘onRegisterConnectionMessage’);
  7. $this->_registerConnection->connect();
  8. }

3 Register服务收到Gateway的注册后,把所有的Gateway的通讯地址保存在内存中

在Register类中,重写了run方法,定义了当前的

  1. $this->onConnect = array($this, ‘onConnect’);
  2. // 设置 onMessage 回调
  3. $this->onMessage = array($this, ‘onMessage’);
  4. // 设置 onClose 回调
  5. $this->onClose = array($this, ‘onClose’);

三个属性,当Register启动的进程收到消息时,会触发onMessage方法

  1. public function onMessage($connection, $buffer)
  2. {
  3. // 删除定时器
  4. Timer::del($connection->timeout_timerid);
  5. $data = @json_decode($buffer, true);
  6. if (empty($data[‘event’])) {
  7. $error = “Bad request for Register service. Request info(IP:”.$connection->getRemoteIp().“, Request Buffer:$buffer). See http://wiki.workerman.net/Error4 for detail”;
  8. Worker::log($error);
  9. return $connection->close($error);
  10. }
  11. $event = $data[‘event’];
  12. $secret_key = isset($data[‘secret_key’]) ? $data[‘secret_key’] : ;
  13. // 开始验证
  14. switch ($event) {
  15. // 是 gateway 连接
  16. case ‘gateway_connect’:
  17. if (empty($data[‘address’])) {
  18. echo “address not found\n”;
  19. return $connection->close();
  20. }
  21. if ($secret_key !== $this->secretKey) {
  22. Worker::log(“Register: Key does not match “.var_export($secret_key, true).” !== “.var_export($this->secretKey, true));
  23. return $connection->close();
  24. }
  25. $this->_gatewayConnections[$connection->id] = $data[‘address’];
  26. $this->broadcastAddresses();
  27. break;
  28. // 是 worker 连接
  29. case ‘worker_connect’:
  30. if ($secret_key !== $this->secretKey) {
  31. Worker::log(“Register: Key does not match “.var_export($secret_key, true).” !== “.var_export($this->secretKey, true));
  32. return $connection->close();
  33. }
  34. $this->_workerConnections[$connection->id] = $connection;
  35. $this->broadcastAddresses($connection);
  36. break;
  37. case ‘ping’:
  38. break;
  39. default:
  40. Worker::log(“Register unknown event:$event IP: “.$connection->getRemoteIp().” Buffer:$buffer. See http://wiki.workerman.net/Error4 for detail”);
  41. $connection->close();
  42. }
  43. }

当$event = ‘gateway_connect’时,是Gateway发来的注册消息,保存到$this->_gatewayConnections数组中,在通过broadcastAddresses方法将当前$this->_gatewayConnections中所有的Gatewat通讯地址转发给所有BusinessWorker进程

4 Register服务收到BusinessWorker的注册后,把内存中所有的Gateway的通讯地址发给BusinessWorker

 

 

同第3步中,Register类收到BusinessWorker的注册时,会触发onMessage方法中的worker_connect,case选项。

image.png

 

同时,将当前worker连接加入到$_workerConnections数组中,在通过broadcastAddresses方法将当前$this->_gatewayConnections中所有的Gatewat通讯地址转发给所有BusinessWorker进程。

5 BusinessWorker进程得到所有的Gateway内部通讯地址后尝试连接Gateway

在BusinessWoker类的启动中,通过重写run方法,定义的启动onWorkerStart方法中,通过connectToRegister方法注册服务中心的同时,也定义了onMessage匿名函数,用于接收消息回调。

$this->_registerConnection->onMessage = array($this, 'onRegisterConnectionMessage');

即,当注册中心发来消息时候,回调到此处

  1. public function onRegisterConnectionMessage($register_connection, $data)
  2. {
  3. $data = json_decode($data, true);
  4. if (!isset($data[‘event’])) {
  5. echo “Received bad data from Register\n”;
  6. return;
  7. }
  8. $event = $data[‘event’];
  9. switch ($event) {
  10. case ‘broadcast_addresses’:
  11. if (!is_array($data[‘addresses’])) {
  12. echo “Received bad data from Register. Addresses empty\n”;
  13. return;
  14. }
  15. $addresses = $data[‘addresses’];
  16. $this->_gatewayAddresses = array();
  17. foreach ($addresses as $addr) {
  18. $this->_gatewayAddresses[$addr] = $addr;
  19. }
  20. $this->checkGatewayConnections($addresses);
  21. break;
  22. default:
  23. echo “Receive bad event:$event from Register.\n”;
  24. }
  25. }

其中Register类发来的数据是

  1. $data = array(
  2. ‘event’ => ‘broadcast_addresses’,
  3. ‘addresses’ => array_unique(array_values($this->_gatewayConnections)),
  4. );

这个时候,就会通过checkGatewayConnections方法检查gateway的这些通信端口是否都已经连接,在通过tryToConnectGateway方法尝试连接gateway的这些内部通信地址

6 Gateway进程收到BusinessWorker进程的连接消息

同样,在Gateway进程启动的时候,触发的onWorkerStart方法中,也定义了一个内部通讯的onWorkerMessage

$this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage');

由此来接收BusinessWorker进程发来的连接消息,部分代码

  1. public function onWorkerMessage($connection, $data)
  2. {
  3. $cmd = $data[‘cmd’];
  4. if (empty($connection->authorized) && $cmd !== GatewayProtocol::CMD_WORKER_CONNECT && $cmd !== GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT) {
  5. self::log(“Unauthorized request from “ . $connection->getRemoteIp() . “:” . $connection->getRemotePort());
  6. return $connection->close();
  7. }
  8. switch ($cmd) {
  9. // BusinessWorker连接Gateway
  10. case GatewayProtocol::CMD_WORKER_CONNECT:
  11. $worker_info = json_decode($data[‘body’], true);
  12. if ($worker_info[‘secret_key’] !== $this->secretKey) {
  13. self::log(“Gateway: Worker key does not match “.var_export($this->secretKey, true).” !== “. var_export($this->secretKey));
  14. return $connection->close();
  15. }
  16. $key = $connection->getRemoteIp() . ‘:’ . $worker_info[‘worker_key’];
  17. // 在一台服务器上businessWorker->name不能相同
  18. if (isset($this->_workerConnections[$key])) {
  19. self::log(“Gateway: Worker->name conflict. Key:{$key});
  20. $connection->close();
  21. return;
  22. }
  23. $connection->key = $key;
  24. $this->_workerConnections[$key] = $connection;
  25. $connection->authorized = true;
  26. return;
  27. // GatewayClient连接Gateway

将worker的进程连接保存到$this->_workerConnections[$key] = $connection;

7 Gateway进程收到客户端的连接,消息时,会通过Gateway转发给worker处理

  1. // Gateway类的run方法中定义此属性
  2. $this->onMessage = array($this, ‘onClientMessage’);
  3. // 收到客户端消息的时候出发此函数
  4. public function onClientMessage($connection, $data)
  5. {
  6. $connection->pingNotResponseCount = –1;
  7. $this->sendToWorker(GatewayProtocol::CMD_ON_MESSAGE, $connection, $data);
  8. }

在sendToWorker方法中,将数据发给worker进程处理

PR教程: PR一键去水印教程, 这样去水印即快又干净! - 知乎

mikel阅读(231)

来源: PR教程: PR一键去水印教程, 这样去水印即快又干净! – 知乎

在后期制作,视频剪辑工作中,去水印是经常遇到的活(ps:虽然这活在工作中应用的比较频繁,不建议大家常用,尊重人家的知识产权,即使使用,也要取得原作者的同意哦),作为从事影视后期制作的我们,掌握有效去水印的方法,是吃这口饭的必备技能哦!

去水印的方法有很多,使用比较多的是“裁切画面”“模糊马赛克等”,但这些最终的效果都不尽如人意哦!今天大叔带来这个特效去水印的方法,效果 nice!

本期大叔就把AE实景运动文字合成教程分享给小伙伴,干货哦~PS:主要解析PR特效插件:视频去水印的使用方法!

PR去水印文图解析

1、(如下图)特效—中间值—拖入视频镜头素材

2、(如下图)点选矩形蒙版—调整蒙版形状,使其覆盖到水印所在的区域!

3、(如下图)调整参数半价和蒙版羽化各位35即可!

视频网站获取M3U8链接,来下载完整视频_Crayonxin2000的博客-CSDN博客_m3u8链接

mikel阅读(326)

来源: 视频网站获取M3U8链接,来下载完整视频_Crayonxin2000的博客-CSDN博客_m3u8链接

前言
网络上的视频大多都经过切片处理,用idm下载视频,有些能下载完整的视频,但是很多都是一些视频小片段,如下图展示的鹅厂的ts片段(爱奇艺是.f4v)

 

后期可以通过ffmpeg或者一些其他程序来合并,但是有点麻烦。而且idm也不是万能的,有些网站idm是不能下载视频的。
这里我来介绍一种通过获取m3u8来下载完整视频(无需idm),以鹅厂视频为例。

获取m3u8链接
m3u8链接长这样:
https://apd-ef3f9709dae705ed2f13a9e885e1a1bf.v.smtcdns.com/varietyts.tc.qq.com/Axf21aHSim4PktKAuXtWvQsBkEOptU6pommPaiNFWIMg/uwMROfz2r5zCIaQXGdGnC2df644Q3LWUuLvyGY4RMhgE_3T2/yL3J5gp1e6f93V9mlCZVgk26P50wKzEcNG9T4e3lRTKvJ5GER05sFlYdcDuVg2_Scuo4C7t3zFBFnUyyxoAFlTMO-wnLMytwjxnfUSQ4Bu8mNCW5aq8JlI1orZ5YgStoc9X3sMAdVnTJd3d7o9RT0UDVWwpENQgU-3eCKCMXFLo/h0035uysemv.321002.ts.m3u8?ver=4

有没有发现链接带有“m3u8”字样?咱们就是要找这样的链接

补充:
其实可以借助浏览器插件或者油猴脚本来直接获取m3u8链接。我觉得获取链接直接用谷歌浏览器自带的开发者工具就可以完成。而且视频网站的代码更新的很快,使用第三方的工具可能出现不能用的情况。下面介绍使用谷歌浏览器自带的开发者工具来获取。如果您已经知道怎么获取m3u8链接了,可以跳过本节,直接去看下一标题内容

打开你想要看的视频,再按f12打开开发者工具。再刷新一下。播放视频再立即关闭。在开发者工具中选中Network,再在过滤器filter中输入 “ m3u8 ” ,会出现几条http请求。

普通的视频网站的m3u8链接应该会直接出现,直接copy 链接就行了。如下图

 

但是鹅厂比较鬼,他把m3u8链接藏在了参数里,需要点开请求,找到vurl参数

复制这个参数就可以得m3u8链接了。
获取到链接接下来就容易了

使用m3u8工具来下载文件
m3u8工具有很多。这里我使用的是网上一位大神写的下载工具,虽然比较老了,还是挺好用的。你们也可以使用其他的软件。

这里是下载链接:M3U8 Downloader

把链接粘贴到地址栏。
文件格式,下载路径选择好,就可以开始下载了。

还有一款批量下载m3u8链接的软件:
m3u8批量
这个软件主要是使用的是aria2和ffmpeg配合下载拼装

总结
其实寻找m3u8链接是最难的。不同家的视频网站,链接的放的位置也不一样,根据实际情况灵活运用

补充
这个方法只适合哪些使用m3u8形式来获取视频流目录的视频网站。本人亲测鹅厂的是完全没问题,还有大多数普通视频网站都是可以的
爱奇艺不可以。
可以关注我,后期发一篇关于爱奇艺下载的
————————————————
版权声明:本文为CSDN博主「Crayon鑫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Crayonxin2000/article/details/113796821

 GatewayWorker客户端向服务端发送心跳请求及心跳检测的实现_sinat_34469308的博客-CSDN博客

mikel阅读(207)

来源: (1条消息) GatewayWorker客户端向服务端发送心跳请求及心跳检测的实现_sinat_34469308的博客-CSDN博客

现在在做一个功能,就是服务端要知道客户端何时掉线的功能,那么要实现这个功能,首先需要实现客户端向服务端发送心跳请求,以及服务端为客户端进行心跳检测的功能。

为了实现这个功能,我选择使用GatewayWorker框架,刚刚接触这个框架,实现了一个简单的客户端心跳检测。

首先,在服务端设置心跳检测:

服务端的心跳检测的设置,需要在start_gateway.php文件中进行设置,start_gateway.php文件是 gateway进程启动脚本,包括端口号等设置。

// 心跳间隔
$gateway->pingInterval = 15;
$gateway->pingNotResponseLimit = 1;
// 心跳数据
$gateway->pingData = ”;
1
2
3
4
5
代码含义:
(1)$gateway->pingInterval = 15; 心跳检测的时间间隔为15秒

(2)$gateway->pingNotResponseLimit = 1; 心跳检测的时间间隔数

(3)$gateway->pingData = ‘’;服务端定时向客户端发送的数据(暂不考虑)

(4)客户端定时向服务端发送心跳,那么g a t e w a y − &gt; p i n g N o t R e s p o n s e L i m i t 必 须 要 大 于 0 , 如 果 gateway-&gt;pingNotResponseLimit 必须要大于0,如果gateway−>pingNotResponseLimit必须要大于0,如果gateway->pingNotResponseLimit =0,就表示客户端不向服务端发送心跳,服务端即使没有收到客户端的心跳,也不会断开连接,更不会触发onClose回调函数。

(5)$gateway->pingInterval x $gateway->pingNotResponseLimit 的值,就是心跳时间期限。

(6)上面代码的含义就是在 15(15×1) 秒的时间里,如果服务端没有检测到客户端发送的心跳请求,那么服务端就认为客户端已经掉线了,服务端自动触发onClose回调函数,进行客户端掉线的善后工作。

需要说明的是,在start_gateway.php文件中进行设置的心跳检测,只是相当于一个config的设置操作,并没有实现任何客户端心跳数据的发送,以及服务端心跳数据的接收和心跳停止后所触发的后续功能。只是起到一个心跳检测的设置作用。

在客户端设置心跳请求:

在客户端设置心跳请求,就是在前端页面(浏览器)里编写js代码,通过在html页面中的js代码,实现前端(客户端)发送心跳请求,

先实现了一个简单的心跳请求测试:

<script>
var ws = new WebSocket(“ws://127.0.0.1:8282”);
ws.onopen = function(){
console.info(“与服务端连接成功”);
ws.send(‘test msg\n’);//相当于发送一个初始化信息
console.info(“向服务端发送心跳包字符串”);
setInterval(show,3000);
}

function show(){
ws.send(‘heart beat\n’);
}

ws.onConnect = function(e){

}
ws.onmessage = function(e){
console.log(e.data);
}
//心跳处理
//获取会员id
ws.onclose = function(e){
console.log(e);
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这个代码很简单,代码解释:
(1) var ws = new WebSocket(“ws://127.0.0.1:8282”); 实例化gatewayworker,
(2)通过onopen属性,与服务端建立连接,

ws.onopen = function(){
console.info(“与服务端连接成功”);
ws.send(‘test msg\n’);//相当于发送一个初始化信息
console.info(“向服务端发送心跳包字符串”);
setInterval(show,3000);
}
1
2
3
4
5
6
这里要说的是,客户端与服务端建立连接,我没有在gatewayworker和workerman里找到相应的方法,所以只能用websocket的onopen属性来实现。
(3)在客户端的定时器,采用了setInterval(show,3000);来实现,每3秒执行一次show()函数。
(4)通过show()函数来向服务端发送心跳数据。

function show(){
ws.send(‘heart beat\n’);
}
1
2
3
服务端心跳请求的处理:
服务端处理心跳,都是在gatewayworker的业务文件——events.php进行的,
这里只写一下onclose函数,就是当gatewayworker检测到客户端心跳停止(比如断电,关闭页面,网线被女朋友拔掉,死机等),就自动触发onclose函数,那么客户端心跳停止后的善后功能,都可以在onclose函数中加以实现,在这个测试案例中,onclose函数实现了发送心跳停止的提示语句。

/**
* 当用户断开连接时触发
* @param int $client_id 连接id
*/
public static function onClose($client_id)
{
// 向所有人发送
echo “the heart beats stoped.\n”;
echo “the user logouted.\n”;

}
1
2
3
4
5
6
7
8
9
10
11
运行结果:

打开浏览器,打开客户端页面,客户端通过setInterval(show,3000)函数,自动发送心跳请求(向服务端发送heart beat字样的信息),因为客户端是每3秒发送一次心跳,符合服务端心跳检测的15秒内需有心跳的心跳检测规则,所以一切正常,当客户端页面关闭,心跳停止,服务端发现15秒内没有一次心跳,就认为客户端已经下线,自动触发服务端的onclose函数,

以上就是使用gatewayworker,进行客户端向服务端发送心跳请求,以及服务端进行客户端的心跳检测的简单实现,

这里有几点想要说的是:
(1)gatewayworker为客户端提供的接口或者方法并不多,比如客户端的定时器,客户端向服务端发送信息,以及客户端与服务端进行连接这些功能的实现,都需要使用websocket的属性和方法实现,gatewayworker并没有提供客户端方法的支持。
(2)正因为(1),对于不支持websocket的浏览器,就会产生功能性的问题,所以在后面还需要使用socket.io等库进行浏览器兼容性的处理。
(3)关于gatewayworker的定时器:

Timer::add(10, function(){
echo “timer\n”;
});
1
2
3
gatewayworker或者workerman的定时器,都是针对服务端的,这个Timer定时器类,是服务器端定时实现某些功能操作,而跟客户端的定时器无关,所以客户端要实现心跳功能,用不上Timer定时器类啊,还是需要客户端自行编写定时器功能。
————————————————
版权声明:本文为CSDN博主「sinat_34469308」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sinat_34469308/article/details/83757183

格式化时间戳,时间加一天_张广森的博客-CSDN博客_时间戳加一天

mikel阅读(177)

来源: (1条消息) 格式化时间戳,时间加一天_张广森的博客-CSDN博客_时间戳加一天

$ceshi=”1417247764″; //时间戳格式

第一种:

$ceshi+86400;//在时间戳的基础上加一天(即60*60*24)
第二种:

$firstdaystr=date(“Y-m-d H:i:s”,$ceshi); //格式化时间戳,转为正常格式 2014-12-18
//$end_time=strtotime($firstdaystr.” +24 hours”); //把时间加24小时,就是加一天。并且是时间戳的格式

$end_time=date(“Y-m-d H:i”,strtotime($firstdaystr.” +24 hours”)); //时间加一天,时间转为正常格式

一些例子:

date(‘Y-m-d H:i:s’,strtotime(‘+1 day’));
date(‘Y-m-d H:i:s’,strtotime(“+1 day +1 hour +1 minute”);
可以修改参数1为任何想需要的数 day也可以改成year(年),month(月),hour(小时),minute(分),second(秒)
$tomorrow = date(“Y-m-d”,mktime (0,0,0,date(“m”) ,date(“d”)+1,date(“Y”)));
<?php
echo(strtotime(“now”));
echo(strtotime(“3 October 2005”));
echo(strtotime(“+5 hours”));
echo(strtotime(“+1 week”));
echo(strtotime(“+1 week 3 days 7 hours 5 seconds”));
echo(strtotime(“next Monday”));
echo(strtotime(“last Sunday”));
?>
一,PHP时间戳函数获取指定日期的unix时间戳 strtotime(”2009-1-22″) 示例如下:

echo strtotime(”2009-1-22″) 结果:1232553600

说明:返回2009年1月22日0点0分0秒时间戳

二,PHP时间戳函数获取英文文本日期时间 示例如下:

便于比较,使用date将当时间戳与指定时间戳转换成系统时间

(1)打印明天此时的时间戳strtotime(”+1 day”)

当前时间:echo date(”Y-m-d H:i:s”,time()) 结果:2009-01-22 09:40:25

指定时间:echo date(”Y-m-d H:i:s”,strtotime(”+1 day”)) 结果:2009-01-23 09:40:25

(2)打印昨天此时的时间戳strtotime(”-1 day”)

当前时间:echo date(”Y-m-d H:i:s”,time()) 结果:2009-01-22 09:40:25

指定时间:echo date(”Y-m-d H:i:s”,strtotime(”-1 day”)) 结果:2009-01-21 09:40:25

(3)打印下个星期此时的时间戳strtotime(”+1 week”)

当前时间:echo date(”Y-m-d H:i:s”,time()) 结果:2009-01-22 09:40:25

指定时间:echo date(”Y-m-d H:i:s”,strtotime(”+1 week”)) 结果:2009-01-29 09:40:25

(4)打印上个星期此时的时间戳strtotime(”-1 week”)

当前时间:echo date(”Y-m-d H:i:s”,time()) 结果:2009-01-22 09:40:25

指定时间:echo date(”Y-m-d H:i:s”,strtotime(”-1 week”)) 结果:2009-01-15 09:40:25

(5)打印指定下星期几的时间戳strtotime(”next Thursday”)

当前时间:echo date(”Y-m-d H:i:s”,time()) 结果:2009-01-22 09:40:25

指定时间:echo date(”Y-m-d H:i:s”,strtotime(”next Thursday”)) 结果:2009-01-29 00:00:00

(6)打印指定上星期几的时间戳strtotime(”last Thursday”)

当前时间:echo date(”Y-m-d H:i:s”,time()) 结果:2009-01-22 09:40:25

指定时间:echo date(”Y-m-d H:i:s”,strtotime(”last Thursday”)) 结果:2009-01-15 00:00:00

以上PHP时间戳函数示例可知,strtotime能将任何英文文本的日期时间描述解析为Unix时间戳,我们结合mktime()或date()格式化日期时间获取指定的时间戳,实现所需要的日期时间。