万字长文轻松彻底入门 Flutter,秒变大前端 - 知乎

mikel阅读(867)

来源: 万字长文轻松彻底入门 Flutter,秒变大前端 – 知乎

本文真对 Flutter 的技术特性,做了一些略全面的入门级的介绍,如果你听说过Flutter,想去了解它,但是又不想去翻厚厚的API,那么本文就是为你准备的。

随着纯客户端到Hybrid技术,到RN&Weex,再到如今的Flutter技术,客户端实现技术不断前进。 在之前的一个APP项目中,因为历史原因当时选择了weex,随着使用的不断深入,我们逐渐发现了weex的渲染性能问题已经成为一个隐患和瓶颈。 而Flutter技术的不断成熟和流行,Flutter的良好的跨平台性和高性能优点,不断吸引着我们。

(本文包含以下内容,阅读完需要约18分钟)

  • 1.Flutter是啥玩意儿?
  • 2.移动端跨平台技术对比
    2.1 H5+原生APP
    2.2 RN&Weex
    2.3 Flutter
  • 3.Dart语言
  • 4.环境配置
  • 5.Hello World
    5.1 创建项目
    5.2 项目结构
    5.3 启动模拟器
    5.4 启动项目APP
    5.5 简化版的Hello World
    5.6 给页面加上状态
    5.7 小结一下
  • 6.路由
    6.1 单个页面的跳转
    6.2 更多页面跳转使用路由表
    6.3 路由传参
  • 7.widget
    7.1 Text
    7.2 Button
    7.3 Container
    7.4 Image
  • 8.布局
    8.1 Row & Column & Center 行列轴布局
    8.2 Align 角定位布局
    8.3 Stack & Positioned 绝对定位
    8.4 Flex & Expanded 流式布局
  • 9.动画
    9.1 简单动画:淡入淡出
    9.2 复杂一些的动画:放大缩小
  • 10.http请求
    10.1 HttpClient
    10.2 http
    10.3 Dio
  • 11.吐吐槽
    11.1 墙
    11.2 组件过度设计
    11.3 嵌套太多不适应
    11.4 布局修改会导致嵌套关系修改
    11.5 Dart语言升级
    11.6 不能热更新
  • 12.结语

1.Flutter是啥玩意儿?

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。

  • 具有跨平台开发特性,支持IOS、Android、Web三端。
  • 热重载特性大大提高了开发效率
  • 自绘UI引擎和编译成原生代码的方式,使得系统的运行时的高性能成为了可能
  • 使用Dart语言,目前已经支持同时编译成Web端代码,

到底值不值得跟进Flutter技术呢? 还是看下Flutter,Weex,ReactNative的搜索指数对比,大概就知道这个行业趋势了。

蓝色是Flutter,可以看出上升势头非常强劲。苦逼的前端就是这样,你不跟潮流,潮流就会把你抛弃。

2.移动端跨平台技术对比

为啥会有Flutter这种东西? 他的原理是什么? 他是怎么做到高性能的? 要明白这些问题,我们不得不从几种移动端跨平台技术的对比讲起。

2.1 H5+原生APP

技术门槛最低,接入速度最快,热更新最方便的,自然就是H5方式。APP中提供一个Webview使用H5页面的Http直连。APP和H5可以相互独立开发,JS使用Bridge与原生进行数据通信,显示界面依赖Webview的浏览器渲染。 但是带来的问题也很明显,因为是需要远程直连,那么初次打开H5页面,会有瞬间的白屏,并且Webview本身会有至少几十M的内存消耗。

当然,作为前端开发人员,在H5方式可以使用SPA单页面、懒加载、离线H5等各种前端优化手段进行性能优化,以使得H5的表现更接近原生。但是首次的瞬间白屏和内存,Bridge的通信效率低下,始终是被技术框架给局限住了。

2.2 RN&Weex

由于H5的那些弊端,爱折腾的前端工程师,祭出了RN、Weex两个大杀器, 使用原生去解析RN、Weex的显示配置,显示层、逻辑层都直接与原生数据通信。 因为抛弃了浏览器,自然渲染性能、执行性能都提升了一大截。

但是,每次遇到显示的变更,JS都还会通过Bridge和原生转一道再做渲染的调整,所以Bridge就最后成为了性能的瓶颈。在实际项目中,特别是做一些大量复杂动画处理的时候,由于渲染部分需要频繁通信,性能问题变得尤为突出。 有兴趣的同学可以去看看BindingX,里面有关于动画中数据通信效率低下导致动画帧率低下的详细说明。

2.3 Flutter

不得不佩服Google开发人员的想象力,为了达到极致性能,Flutter更前进了一步,Flutter代码编译完成以后,直接就是原生代码,并且使用自绘UI引擎原生方式做渲染。 Flutter依赖一个Skia 2D图形化引擎。Skia也是Android平台和Chrome的底层渲染引擎,所以性能方面完全不用担心。因为使用Dart做AOT编译成原生,自然也比使用解释性的JS在V8引擎中执行性能更快,并且因为去掉Bridge,没有了繁琐的数据通信和交互,性能就更前进了一步。

3.Dart语言

学习Flutter,得先了解Dart。Dart语言曾经雄心勃勃的要替换JavaScript, 但是发布的时机正好遇到JS的飞速发展,于是就逐渐沉寂,直到配合Flutter的发布,才又重新焕发了生机。

在最近2019年9月的一次Google开发者大会中,伴随着Flutter1.9的发布,目前的Dart也同时更新到了2.5版本, 提供了机器学习和对C跨平台调用的能力。总体来说,Dart语法,对于前端同学,上手还是很容易的,风格很像。

关于Dart语法,请移步传送门:dart.dev/samples

4.环境配置

无论学什么新语言,首先都是环境配置。由于Flutter出自Google,所以有一定门槛,如果在公司内安装,你还需要一个方便的代理切换工具, 比如:Proxifier 。

安装教程,参照官网:flutter.dev/docs/get-st

Flutter支持多种编辑器如:Android Studio , XCode。 但是既然作为支持跨双端的开发,个人还是推荐使用 VSCode

VSCode安装完成后,需要安装Flutter插件,和Dart插件. 在扩展窗口里,搜索Flutter,和Dart,点击“Install”即可,非常方便。

如果安装不上去,记得开启下代理。

5.Hello World

作为一个伟大的程序员,第一行代码总是从Hello World开始。^_^

5.1 创建项目:

方法1:直接使用命令创建:

flutter create projectname

方法2:使用VSCode创建:

View -> Command Palette -> Flutter:New Project 即可

注意请先打开代理,否则你的创建进度,会一直被卡住。

5.2 项目结构

将项目先拖入VSCode,看下目录结构。自动创建完成的项目中,我们看到已经自带了Android,IOS相关的运行环境。

入口主文件是main.dart. 可以打开来先熟悉下,暂时不了解没关系,后面再讲。

还有一个重要的文件是pubspec.yaml ,是项目的配置文件,这个后续也会做修改。

5.3 启动模拟器

点击VSCode右下角的模拟器,启动模拟器。(VSCode会自动找到Android环境、IOS环境下的模拟器,以及真机环境)

5.4 启动项目APP

选中Main.dart, 点击Debug-> Start Debugging , 项目就会启动调试,并在模拟器里运行。

5.5 简化版的Hello World

讲道理,Flutter一上来就用StatefulWidget做一个自增的Demo,其实是对新手不太友好。 我还是喜欢循序渐进,先删掉那些复杂的自增逻辑,我们基于StatelessWidget 只做一个最简单的静态页面显示。(什么是StatefulWidget 和StatelessWidget?后面会说)

main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget{
   @override
  Widget build(BuildContext context) {
       return Scaffold(
            appBar: AppBar(
              title: Text("我是Title"),
            ),
            body: Center(
                    child: Text(
                        'Hello World',
                    )
            )
      );
  }  
}

在上面的代码中,可以清楚看到,最简单的页面的层级关系:

MaterialApp -> MyHomePage -> Scaffold -> body -> Center -> Text

Scaffold是啥?他是Flutter的页面脚手架,你可以当HTML页面一样去理解,不同的是,他除了Body以外,还提供appBar顶部TitleBar、bottomNavigationBar底部导航栏等属性。

显示效果:

这是最简单的页面,没有交互,只有显示,但是实际业务场景中,是不太可能都是这种页面的,页面上的数据一般都是来自接口返回,然后再在页面上进行动态的渲染。 此时,就需要使用使用带状态的StatefulWidget了

5.6 给页面加上状态

给自己一个需求,按钮点击时,修改页面上显示的文字“Hello World” 变成“You Click Me”

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget{
  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage>{
   var msg="Hello World"; //msg默认文字
   @override
   Widget build(BuildContext context) {
       return Scaffold(
            appBar: AppBar(
              title: Text("我是Title"),
            ),
            body: Center(
                      child:Column(
                              children:<Widget>[
                                  Text(msg), //根据变量值,显示文字
                                  FlatButton(
                                      color: Colors.blue,
                                      textColor: Colors.white,
                                      //点击按钮,修改msg的文字
                                      onPressed: () {
                                        setState(() {
                                          this.msg="You Click ME";
                                        });
                                      },
                                      child: Text(
                                        "Click ME",
                                        style: TextStyle(fontSize: 20.0),
                                      ),
                                  )
                              ]
                      )
                  )
      );
  }  

}

执行效果:

上面最关键的一段代码就是这个:

 onPressed: () {
         setState(() {
                this.msg="You Click ME";
          });
 },

相信写过小程序的同学,对这个 setState 还是很眼熟的 ^_^

5.7 小结一下

StatelessWidget:无状态变更,UI静态固化的Widget, 页面渲染性能更高。
StatefulWidget:因状态变更可以导致UI变更的的Widget,涉及到数据渲染场景,都使用StatefulWidget。

为啥要分两个? StatelessWidget拥有的功能,StatefulWidget都有了啊?

答案只有一个:性能、性能、性能

在StatefulWidget里,因为要维护状态,他的生命周期比StatelessWidget更复杂,每次执行setState,都会触发
window.scheduleFrame() 导致整个页面的widget被刷新,性能就会降低。

使用过小程序的同学在这点上应该有体会,在小程序的官方文档中,会强烈建议减少setData的使用频率,以避免性能的下降。 只不过flutter更是激进,推出了StatelessWidget,并直接在该Widget里砍掉了setState的使用。

页面结构关系如下:

6.路由

实际的项目,是有多个不同的页面的,页面之间的跳转,就要用到路由了。 我们增加一个list页面,点击Home页的“Click Me”按钮,跳转到列表页list。

6.1 单个页面的跳转

增加list.dart

import 'package:flutter/material.dart';

class ListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //定义列表widget的list
    List<Widget> list=<Widget>[];

    //Demo数据定义
    var data=[
      {"id":1,"title":"测试数据AAA","subtitle":"ASDFASDFASDF"},
      {"id":2,"title":"测试数据bbb","subtitle":"ASDFASDFASDF"},
      {"id":3,"title":"测试数据ccc","subtitle":"ASDFASDFASDF"},
      {"id":4,"title":"测试数据eee","subtitle":"ASDFASDFASDF"},
    ];

    //根据Demo数据,构造列表ListTile组件list
    for (var item in data) {
      print(item["title"]);

      list.add( ListTile( 
          title: Text(item["title"],style: TextStyle(fontSize: 18.0) ),
          subtitle: Text(item["subtitle"]),
          leading:  Icon( Icons.fastfood, color:Colors.orange ),
          trailing: Icon(Icons.keyboard_arrow_right)
      ));
    }

    //返回整个页面
    return Scaffold(
      appBar: AppBar(
        title: Text("List Page"),
      ),
      body: Center(
        child: ListView(
          children: list,
        )
      ),
    );
  }
}

在main.dart增加list页面的引入

import 'list.dart';

修改Home页的按钮事件,增加Navigator.push跳转

FlatButton(
          color: Colors.blue,textColor: Colors.white,
          onPressed: () {    
                       Navigator.push(context, MaterialPageRoute(builder:(context) {
                                return  ListPage();
                       }));
              },
           child: Text("Click ME",style: TextStyle(fontSize: 20.0) ),
    )

核心方法就是:Navigator.push(context,MaterialPageRoute)

跳转示例:

6.2 更多页面跳转使用路由表

在MaterialApp中,有一个属性是routes,我们可以对路由进行命名,这样跳转的时候,只需要使用对应的路由名字即可,如:Navigator.pushNamed(context, RouterName)。点击两个不同的按钮,分别跳转到ListPage,和Page2去。

Main.dart修改一下如下

import 'package:flutter/material.dart';
import 'list.dart';
import 'page2.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      //路由表定义
      routes:{
        "ListPage":(context)=> ListPage(),
        "Page2":(context)=> Page2(),
      },
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget{
  @override
  MyHomePageState createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage>{
   @override
   Widget build(BuildContext context) {
       return Scaffold(
            appBar: AppBar(
              title: Text("我是Title"),
            ),
            body: Center(
                      child:Column(
                              children:<Widget>[
                                  RaisedButton(
                                      child: Text("Clikc to ListPage" ),
                                      onPressed: () {
                                        //根据命名路由做跳转
                                         Navigator.pushNamed(context, "ListPage");
                                      },
                                  ),
                                   RaisedButton(
                                      child: Text("Click to Page2" ),
                                      onPressed: () {
                                          //根据命名路由做跳转
                                         Navigator.pushNamed(context, "Page2");
                                      },
                                  )

                              ]
                      )
                  )
      );
  }  

}

示例:

当我们有了路由以后,就可以开始在一个项目里用不同的页面,去学习不同的功能了。

6.3 路由传参

列表页跳转到详情页,需要路由传参,这个在flutter体系里,又是怎么做的呢?

首先,在main.dart里,增加详情页DedailPage的路由配置

//路由表定义
      routes:{
        "ListPage":(context)=> ListPage(),
        "Page2":(context)=> Page2(),
        "DetailPage":(context)=> DetailPage(), //增加详情页的路由配置
      },

并修改ListPage里ListTile的点击事件,增加路由跳转传参,这里是将整个item数据对象传递

ListTile( 
          title: Text(item["title"],style: TextStyle(fontSize: 18.0) ),
          subtitle: Text(item["subtitle"]),
          leading:  Icon( Icons.fastfood, color:Colors.orange ),
          trailing: Icon(Icons.keyboard_arrow_right),
          onTap:(){
            //点击的时候,进行路由跳转传参
             Navigator.pushNamed(context, "DetailPage", arguments:item);
          },
      )

详情页DetailPage里,获取传参并显示

import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
     //获取路由传参
     final Map args = ModalRoute.of(context).settings.arguments;

    return Scaffold(
      appBar: AppBar(
        title: Text("Detail Page"),
      ),
      body: 
        new Column(
          children: <Widget>[
             Text("我是Detail页面"),
             Text("id:${args['id']}" ),
             Text("id:${args['title']}"),
             Text("id:${args['subtitle']}")
          ],
        )
      );
  }
}

Demo效果:

7.widget

Flutter提供了很多默认的组件,而每个组件的都继承自widget 。 在Flutter眼里:一切都是widget。 这句看起来是不是很熟悉? 还记得在webpack里,一切都是module吗? 类似的还有java的一切都是对象。貌似任何一个技术,最后都是用哲学作为指导思想。

widget,作为可视化的UI组件,包含了显示UI、功能交互两部分。大的widget,也可以由多个小的widget组合而成。

常用的widget组件:

7.1 Text

Demo:

Text(
         "Hello world",
         style: TextStyle(
                      fontSize: 50,
                      fontWeight: FontWeight.bold,
                      color:Color(0xFF0000ff)
                  )
    ),

Text的样式,来自另一个widget:TextStyle。 而TextStyle里的color,又是另一个widget Color的实例。

如果用flutter的缩进的方法,看起来确实有点丑陋,习惯写CSS的前端同学,可以看看下面的风格:

Text( "Hello world", style: TextStyle( fontSize: 50,fontWeight: FontWeight.bold,color:Color(0xFF0000ff) ) )

写成一行,是不是就顺眼多了?这算前端恶习吗?^_^

7.2 Button

对于flutter来说,Button就提供了很多种,我们来看看他们的区别:

RaisedButton: 凸起的按钮
FlatButton:扁平化按钮
OutlineButton:带边框按钮
IconButton:带图标按钮

按钮测试页dart:

import 'package:flutter/material.dart';

class ButtonPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Button Page"),
      ),
      body: Column(
        children: <Widget>[
             RaisedButton(
                  child: Text("我是 RaiseButton" ),
                  onPressed: () {},
              ),
               FlatButton(
                  child: Text("我是 FlatButton" ),
                  color: Colors.blue,
                  onPressed: () {},
              ),
              OutlineButton(
                  child: Text("我是 OutlineButton" ),
                  textColor: Colors.blue,
                  onPressed: () {},
              ),
              IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {},
              )  
        ]
      )
    );
  }
}

Demo:

项目中要用哪个,就各取所需吧~

7.3 Container

Container是非常常用的一个widget,他一般是用作一个容器。我们先来看看他的基础属性,顺便可以想想他像HTML里的啥?

基础属性:width,height,color,child

body: Center(
        child: Container(
           color: Colors.blue,
           width: 200,
           height: 200,
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),
        )
      )

Padding

我们也可以不设置宽高,用padding在内部撑开增加留白:

Container(
           color: Colors.blue,
           padding: EdgeInsets.all(30),
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),

        )

Margin

我们还可以使用margin,在容器的外部撑开增加偏移量,

Container(
           color: Colors.blue,
           padding: EdgeInsets.all(30),
           margin: EdgeInsets.only(left: 150,top: 0,right: 0,bottom: 0),
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),
        )

Transform

我们还可以给这个矩形,使用tansform做一些变化,比如,旋转一个角度

Container(
           color: Colors.blue,
           padding: EdgeInsets.all(30),
           child: Text("Hello Container ",style:TextStyle(fontSize: 20,color: Colors.white)),
           transform: Matrix4.rotationZ(0.5)
        )

看到这里,好多前端同学要说了,好熟悉啊。 对,他就是很像Html里的一个东西:DIV,你确实可以对应的去加强理解。

7.4 Image

网络图片加载

使用NetworkImage,可以做网络图片的加载:

child:Image(
          image: NetworkImage("https://mat1.gtimg.com/pingjs/ext2020/qqindex2018/dist/img/qq_logo_2x.png"),
           width: 200.0,
        )  

本地图片加载

加载本地图片,就稍微复杂一些,首先要把图片的路径配置,加入到之前说过的pubspec.yaml配置文件里去:

加载本地图片时使用AssetImage:

child:Image(
               image: AssetImage("assets/images/logo.png"),
                width: 200.0,
            )      

也可以使用简写:

 Image.asset("assets/images/logo.png",width:200.0)

flutter提供的组件很多,这里就不一一举例说明,有兴趣的还是建议大家去看API:api.flutter.dev/

8.布局

我们已经了解了这么多组件,那么怎么绘制一个完整的页面呢? 这就到了页面布局的部分了。

8.1 Row & Column & Center 行列轴布局

字面意义也很好理解,行布局、列布局、居中布局,这些布局对于Flutter来说,也都是一个个的widget。

区别在于,row、column 是有多个children的widget, 而Center是只有 1个child的 widget。

 Row(
     children:<Widget>[]
 ) 

 Column(
     children:<Widget>[]
 )    

 Center(
      child:Text("Hello")
 )

8.2 Align 角定位布局

我们常常在Container里,需要显示的内容在左上角,左下角,右上角,右下角。 在html时代,使用CSS可以很容易的实现,但是flutter里,必须依赖Align 这个定位的Widget

右下角定位示例:

 child: Container(
           color: Colors.blue,
           width: 300,
           height: 200,
           child: Align(
                      alignment: Alignment.bottomRight,
                      child:Text("Hello Align ",style:TextStyle(fontSize: 20,color: Colors.white)),
                  )
        )

显示效果:

Alignment提供了多种定位供选择,还算是很贴心的。

8.3 Stack & Positioned 绝对定位

当然还有绝对定位的需求,这在css里,使用position:absolute就搞定了,但是在flutter里,需要借助stack+ positioned两个widget一起组合使用。

Stack: 支持元素堆叠
Positioned:支持绝对定位

child:Stack(
              children: <Widget>[
                  Image.network("https://ossweb-img.qq.com/upload/adw/image/20191022/627bdf586e0de8a29d5a48b86700a790.jpeg"),
                  Positioned(
                    top: 20,
                    right: 10,
                    child:Image.asset("assets/images/logo.png",width:200.0)
                  )
              ],
            )

8.4 Flex & Expanded 流式布局

Flex流式布局作为前端同学都熟悉,之前讲过的Row,Column,其实都是继承自Flex,也属于流式布局。

如果轴向不确定,使用Flex,通过修改direction的值设定轴向
如果轴向已确定,使用Row,Column,布局更简洁,更有语义化

Flex测试页:

class FlexPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Flex Page"),
      ),
      body:  Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Container(
              width: 30,
              height: 100,
              color: Colors.blue,
            ),
            Expanded(
              flex: 1,
              child: Container(
                height: 100.0,
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 1,
              child: Container(
                height: 100.0,
                color: Colors.green,
              ),
            ),
          ],
        ),
    );
  }
}

示例中,轴向横向排列,最左边一个固定宽度的Container,右边两个Expanded,各自占剩下的宽度的一半。

9.动画

Flutter既然说了,一切都是Widget,包括动画实现,也是一个Widget。 我们还是看一个示例

9.1 简单动画:淡入淡出:

使用flutter提供的现成的Widget:

import 'package:flutter/material.dart';

class AnimatePage extends StatefulWidget {
  _AnimatePage  createState()=> _AnimatePage();
} 

class _AnimatePage extends State<AnimatePage> {
  bool _visible=true;
  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Animate Page"),
      ),
      body: 
          Center(

            child: Column(
                  children: <Widget>[
                      AnimatedOpacity(
                        opacity: _visible ? 1.0:0.0,
                        duration: Duration(milliseconds: 1000),
                        child: Image.asset("assets/images/logo.png"),
                      ),

                      RaisedButton(
                        child: Text("显示隐藏"),
                        onPressed: (){
                          setState(() {
                            _visible=!_visible;
                          });
                         },
                      ),

                  ],
                ),
          )    
      );

  }
}

其中的AnimatedOpacity就是动画透明度变化的的Widget,而被透明度控制变化的Image则是AnimatedOpacity的子元素。这个和以往前端写动画的方式,就完全不一样了,需要改变一下思维方式。

Demo效果

9.2 复杂一些的动画:放大缩小

当写复杂一些动画的时候,没有对应的widget组件,就需要自己使用Animation,和AnimationController,以及Tween来组合。

Animation: 保存动画的值和状态
AnimationController: 控制动画,包含:启动forward()、停止stop()、反向播放reverse()等方法
Tween: 提供begin,end作为动画变化的取值范围
Curve:设置动画使用曲线变化,如非匀速动画,先加速,后减速等的设定。

动画示例:

class AnimatePage2 extends StatefulWidget {
  _AnimatePage  createState()=> _AnimatePage();
} 

class _AnimatePage extends State<AnimatePage2>  with SingleTickerProviderStateMixin {

  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller =  AnimationController(duration:  Duration(seconds: 3), vsync: this);

     //使用弹性曲线,数据变化从0到300
     animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
     animation = Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
        setState(() {
        });
      });


    //启动动画(正向执行)
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text("Animate Page"),
      ),
      body: 
          Center(
              child: Image.asset(
                  "assets/images/logo.png",
                  width: animation.value, 
                  height: animation.value
              ),
            )  
      );   
  }

  dispose() {
    //路由销毁时需要释放动画资源
    controller.dispose();
    super.dispose();
  }

}

很重要的一点,在路由销毁的时候,需要释放动画资源,否则容易导致内存泄漏

显示Demo:

10.http请求

做业务逻辑,总离不开http请求,接下来,就来看下flutter的http请求是如何做的。

10.1 HttpClient

httpClient在 dart:io库中,不需要引入第三方库就可以使用,示例代码如下:

使用示例

import 'dart:convert';
import 'dart:io';

Future _getByHttpClient() async{
    //接口地址
    const url="https://www.demo.com/api";

    //定义httpClient
    HttpClient client = new HttpClient();
    //定义request
    HttpClientRequest request = await client.getUrl(Uri.parse(url));
    //定义reponse
    HttpClientResponse response = await request.close();
    //respinse返回的数据,是字符串
    String responseBody = await response.transform(utf8.decoder).join();
    //关闭httpClient
    client.close();
    //字符串需要转化为JSON
    var json= jsonDecode(responseBody);
    return json;

} 

总的看起来,代码还是挺繁琐的,使用起来并不方便。

10.2 http

这是Dart.dev提供的第三方类库,地址:pub.dev/packages/http

需要先在pubspec.yaml里添加类库应用

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0
  http: ^0.12.0+2

使用示例:

Future _getByDartHttp() async {
  // 接口地址
 const url="https://www.demo.com/api";//获取接口的返回值
 final response = await http.get(url);
 //接口的返回值转化为JSON
 var json = jsonDecode(response.body); 
 return json;
}

这种写法,比上面的httpClient简洁了许多。

Dio

国内使用最广泛的,还是flutterchina在github上提供的Dio第三方库,目前Star达到了5800多个。

官网地址:github.com/flutterchina

使用Dio,因为是第三方库,所以同样要先在 pubspec.yaml 添加第三方库引用。

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^2.0.0
  dio: 2.1.16

使用示例:

import 'package:dio/dio.dart';

Future _getByDio() async{

      // 接口地址
      const url="https://www.demo.com/api";

      //定义 Dio实例
      Dio dio = new Dio();
      //获取dio返回的Response
      Response response = await dio.get(url);
      //返回值转化为JSON
      var json=jsonDecode(response.data);
      return json;
}

接口调用也是比httpclient简单很多,可能由于fluterchina在他的官方教程里,极力推荐这个dio库,所以目前这个第三方库的使用情况最为广泛。和Dart.dev的http不同的是,他需要new一个Dio的实例,在创建实例的时候,还可以传入更多的扩展配置参数。

BaseOptions options = new BaseOptions(
    baseUrl: "https://www.xx.com/api",
    connectTimeout: 5000,
    receiveTimeout: 3000,
);
Dio dio = new Dio(options);

11.吐吐槽

学习Flutter的过程中,其实还是有很多坎坷和需要吐槽的地方。

11.1 墙

因为有墙在,所以在配置flutter,或者下载flutter插件和第三方库的时候,需要墙内外来回切换。

11.2 组件过度设计

提供的各种widget组件很多,但是真正核心的组件、常用的组件,也就哪些。 比如Flex 和column、row的关系,比如,Tween 与IntTween,ColorTween,SizeTween等20多个Tween子类之间的关系,你需要花很大的精力,去看每个具体子类的实现差别。

11.3 嵌套太多不适应

因为嵌套层级很多,而且布局、动画、功能都在一起,第一次上手Flutter和Dart,这种嵌套关系让人很晕菜,这个只能去慢慢克服。 另外,多开发自定义的组件,可以让嵌套关系看起来清晰一些。

11.4 布局修改会导致嵌套关系修改

前端的html+css分离世界里,不改变嵌套关系,修改CSS就可以调整布局。 但是在Flutter里因为布局也是嵌套关系,这就导致必须去改变嵌套关系。 要让嵌套更简单变动影响更小,页面拆分成子组件变得尤为重要。

11.5 Dart语言升级

没错,语言升级也会导致学习的困扰,外面的资料新旧都有,比如有些是 new Text() ,有些直接是Text() ,新手上路会很晕菜。 其实这都是Dart语言升级导致的,记住Dart升级2.X以后,都不使用new了。感兴趣的可以自己去看下Dart的升级变更说明。

11.6 不能热更新

年中的时候,Google官方宣布flutter暂不官方支持热更新,但是闲鱼团队已经有了自己的热更新方案。 关于热更新,只能静观其变了。 性能、开发效率、热更新,总是要有取舍的。即使是闲鱼团队,热更新也是付出了一点点性能下降的代价的,这是你选择flutter的初衷吗?还是那句话:权衡得失。

12.结语

随着 9 月谷歌发布 Flutter1.9 以及flutter for web,Flutter的组件化思路,使得一份代码跨三端变成可能,相信Flutter的未来会更加广阔。

这不是一篇教程,只是在学习Flutter过程中的一点体验和经历,也因为时间关系,研究并不深入,如有疏漏,还请不吝赐教。

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

mikel阅读(1015)

来源: 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阅读(1044)

来源: 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阅读(892)

问题代码

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阅读(1188)

来源: 深入理解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阅读(738)

$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阅读(1332)

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

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

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

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

PR去水印文图解析

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

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

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

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

mikel阅读(2488)

来源: 视频网站获取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阅读(2322)

来源: (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