后端 – web百事通 https://www.askme-121.space web互联网之家 Tue, 26 Nov 2024 12:14:12 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.5.13 https://www.askme-121.space/wp-content/uploads/2023/12/cropped-05ee702f-4b38-40f3-915f-c8fc68b10a91-32x32.png 后端 – web百事通 https://www.askme-121.space 32 32 Rust语言入门第二篇-Cargo教程 https://www.askme-121.space/rust-cargo/ https://www.askme-121.space/rust-cargo/#respond Tue, 26 Nov 2024 12:13:23 +0000 https://www.askme-121.pw/?p=553 一.Cargo 是什么

Cargo 是 Rust 的官方构建系统和包管理器。它负责管理 Rust 项目的依赖项、构建项目以及管理项目的配置。

二.Cargo教程

要使用 Cargo 创建一个新的 Rust 项目,请使用cargo new

cargo new hello_cargo

这一行命令新建了名为 hello_cargo 的目录和项目。我们将项目命名为 hello_cargo,同时 Cargo 在一个同名目录中创建项目文件。

进入目录,将会看到 Cargo 生成了两个文件和一个目录:一个 Cargo.toml 文件,一个 src 目录,以及位于 src 目录中的 main.rs 文件。

Cargo.toml文件

这个 Cargo 文件描述了一个 Rust 项目的元数据和依赖项。下面是对文件内容的解析:

1.[package] 是一个片段(section)标题,表示接下来的内容是关于项目包的配置。

name = "hello_cargo":指定了项目的名称为 “hello_cargo”
version = "0.1.0":指定了项目的版本号为 “0.1.0”。
edition = "2021":指定了 Rust 编译器所使用的 Edition(版本),这里是 2021 Edition。

2.[dependencies] 部分:

这个部分通常用于列出项目所依赖的外部库。Cargo 文件还可以包含其他部分,比如 [workspace]、[profile] 等,用于配置工作空间和编译器参数等。这些部分不是必需的,取决于项目的需要。

src/main.rs 文件

fn main() {
    println!("Hello, world!");
}

这段 Rust 代码定义了一个程序的入口点(main 函数),并在控制台打印 “Hello, world!” 消息。

构建并运行Cargo项目

cargo build

具体而言,cargo build 命令的含义是:

1.编译项目代码:Cargo 会根据项目根目录下的 Cargo.toml 文件中的配置信息,编译整个 Rust 项目的源代码。

2.生成可执行文件或库:根据项目类型(可执行程序或库),Cargo 会生成对应的可执行文件(通常在 target/debug 目录下)或者库文件(通常在 target/debug 目录下),这取决于项目的配置和 Cargo.toml 文件中的指定。

3.处理依赖关系:如果项目依赖了其他的 Rust crate(包),Cargo 会自动下载并编译这些依赖,以确保项目的所有依赖都被正确处理和链接。

4.检查代码:Cargo 会在编译过程中执行各种检查,包括语法检查、类型检查、错误检查等,以确保代码的质量和正确性。

我们也可以使用编译然后运行它,所有这些都在一个步骤中完成:cargo run

]]>
https://www.askme-121.space/rust-cargo/feed/ 0
Rust语言入门第一篇-环境搭建 https://www.askme-121.space/rust-makeup/ https://www.askme-121.space/rust-makeup/#respond Fri, 22 Nov 2024 14:35:19 +0000 https://www.askme-121.pw/?p=552 1、C++开发环境配置

Rust 语言的底层是依赖于 C/C++ 编译器的。在安装 Rust 编译器时,通常会自动安装所需的 C/C++ 编译环境,以便 Rust 能够生成可执行文件或库。因此,在安装 Rust 之前,建议确保系统已经安装了 C/C++ 编译器。

下载地址:https://github.com/niXman/mingw-builds-binaries/releases

验证C++是否环境配置成功,按下 “Win + R” 键,打开 “运行” 对话框。 输入gcc -v

2.Rust 环境配置

如果您曾经安装过 rustup,可以执行 rustup update 来升级 Rust。

配置 PATH 环境变量

在 Rust 开发环境中,所有工具都安装在 ~/.cargo/bin 目录中,您可以在这里找到包括 rustc、cargo 和 rustup 在内的 Rust 工具链。Rust 开发者通常会将该目录加入 PATH环境变量中。安装后在终端尝试执行 rustc –version

3.Rust程序

启动的过程中报错了,按下 “Win + R” 键,打开 “运行” 对话框。 输入rustup default stable-x86_64-pc-windows-gnu

代码讲解

  1. fn main() {
    这行代码定义了一个名为main的函数。在Rust程序中,main函数是程序的入口点,也是程序开始执行的地方。Rust程序从main函数开始执行,因此这个函数是必须的,没有main函数的Rust程序将无法正常运行。
  2. println!(“Hello, world!”);
    这是main函数中的一行代码,它调用了一个宏(macro)println! 来向控制台输出一段文本。在Rust中,println! 是一个宏,用于在标准输出流(通常是控制台)打印一段格式化的文本。在这里,println!(“Hello, world!”); 的作用是将字符串”Hello, world!” 输出到控制台。

println! 是一个宏,而不是普通的函数。宏在Rust中使用!来调用,例如println!、panic!等。"Hello, world!" 是一个字符串字面量(string literal),它是一个固定的文本内容,会被直接输出到控制台。

执行过程:

当你运行这段Rust程序时,操作系统会首先调用main函数。在main函数中,println!("Hello, world!");会被执行,导致字符串"Hello, world!"被输出到控制台。这是一个简单但经典的示例,用于展示如何编写并执行最基本的Rust程序。

注意事项:

  • Rust的语法相对严格和安全,因此每个语句末尾需要分号;来结束。
  • 字符串字面量(如"Hello, world!")是不可变的,它们存储在程序的只读内存区域中。
  • Rust中的宏(macro)和普通函数有所区别,宏在编译时进行代码替换,因此可以实现更灵活的代码生成和转换。
]]>
https://www.askme-121.space/rust-makeup/feed/ 0
Rust语言 https://www.askme-121.space/rust/ https://www.askme-121.space/rust/#respond Sat, 16 Nov 2024 13:55:10 +0000 https://www.askme-121.pw/?p=548

一,Rust语言是什么

Rust 是一种系统级编程语言,由 Mozilla 开发。它的设计注重安全性、并发性和高性能。Rust 最初发布于 2010年,其目标是成为一种能够替代 C 和 C++ 的编程语言,同时提供更好的内存安全性和并发支持。

以下是 Rust 目标成为一种能够替代 C 和 C++ 的编程语言的一些优势:

1. 内存安全性:Rust 通过所有权系统和借用检查器来保证内存安全。这意味着在编写 Rust 代码时,不会出现诸如空指针、野指针或者数据竞争等内存安全问题,这些问题在 C 和 C++ 中经常导致程序崩溃或安全漏洞。

2.并发性:Rust 内置了对并发编程的支持,包括线程、消息传递和共享状态。通过使用 Rust 的并发编程模型,开发者可以编写出更加安全和高效的并发代码,避免了在 C 和 C++ 中常见的并发问题,如死锁、数据竞争等。

3.性能:Rust 的设计注重性能,其编译器能够生成高效的机器码,并且在不牺牲安全性的前提下尽可能地进行优化。因此,Rust 程序通常具有与 C 和 C++ 程序相媲美的性能。

4.可移植性:Rust 支持跨平台开发,可以在多种操作系统和硬件架构上运行。这使得 Rust 成为一个适用于各种项目和环境的通用编程语言。

访问 Rust 官网给出的答案涵盖了选择 Rust 的三个主要原因:高性能、可靠性和生产力。让我对这些原因进行更详细的解释:

1.高性能:Rust 的设计旨在提供接近于 C 和 C++ 的执行效率。它通过一系列的编译器优化和内存安全机制,使得 Rust 代码能够在不损失性能的情况下实现内存安全。这意味着你可以编写高性能的应用程序,同时又能够避免常见的内存错误,如空指针、野指针、数据竞争等。

2.可靠性:Rust 引入了独特的所有权系统和借用规则,以确保在编译时捕获内存安全问题。这种静态检查机制能够在代码编译阶段就发现潜在的错误,而不是在运行时产生意外行为或崩溃。通过在编译时防止内存错误和并发问题,Rust 可以提供更可靠、更稳定的软件。

3.生产力:尽管 Rust 强调了性能和可靠性,但它也注重开发者的生产力。Rust 提供了现代化的语法和丰富的功能,使得编写和维护代码更加轻松。它支持泛型编程、模式匹配、闭包等高级特性,提供了强大的标准库和第三方库,使得开发者可以快速构建复杂的应用程序。此外,Rust 的文档和工具链也都非常完善,有助于开发者更快地上手和解决问题。

二,Rust语言能做什么?

Rust语言的设计使其适用于许多不同的领域,包括但不限于以下几个方面:

  1. 传统命令行程序:
    Rust提供了强大的系统编程功能,使其非常适合编写各种命令行工具和实用程序。
  2. Web 应用:
    借助诸如Actix、Rocket等现代Web框架,Rust可以用于构建高性能、安全的Web应用程序。
  3. 网络服务器:
    Rust的并发和异步编程特性使其成为构建高性能网络服务器的理想选择,例如使用Tokio等异步运行时。
  4. 嵌入式设备:
    Rust的低级别控制和内存安全性使其成为嵌入式系统开发的良好选择,可以用于编写嵌入式设备的驱动程序、控制程序等。
  5. 操作系统:
    Rust的内存安全性和并发性使其成为操作系统开发的有力工具。例如,Redox OS就是一个完全用Rust编写的操作系统。
  6. 游戏引擎:
    虽然Rust在游戏开发领域的应用还不如其他语言广泛,但由于其性能和安全性,一些游戏引擎开始考虑使用Rust编写关键组件。
  7. 数据库:
    Rust社区中有一些正在积极开发的数据库项目,例如RocksDB等,可以用于构建高性能的数据库系统。
  8. 并行计算:
    Rust提供了并发和并行编程的支持,使其适用于开发并行计算应用程序,例如科学计算、数据处理等。

Rust的优势:

1.卓越的性能和安全性:Rust以性能和安全性为设计目标,通过强大的编译时检查和所有权系统,可以避免常见的内存安全问题,同时保持着接近于原生编译语言的性能。
2.灵活的语法和类型系统: Rust具有灵活的语法和严格的类型系统,支持函数式编程和面向对象编程,使得开发者能够编写出简洁、高效的代码。
3.包管理和构建工具: Rust拥有Cargo作为其默认的包管理和构建工具,它提供了便捷的依赖管理和项目构建功能,帮助开发者更快速地进行开发和部署。
4.快速的编译和执行速度: Rust的编译器通常能够快速生成高效的机器码,使得开发者可以更快地进行迭代和测试。

Rust的劣势:

1.陡峭的学习曲线: Rust的所有权和生命周期等概念对于一些开发者来说可能较为陌生,需要花费一定的时间和精力来学习和掌握。
2.相对较小的开发生态和社区: 相比于Java等老牌语言,Rust的开发生态和社区规模相对较小,可能会缺乏一些成熟的解决方案和第三方库。
3.严格的安全检查可能带来的负担: 虽然Rust的严格安全检查可以避免很多常见的错误,但有时也可能会导致一些开发负担,需要开发者手动管理一些内存和生命周期相关的问题。

]]>
https://www.askme-121.space/rust/feed/ 0
ansible入门详解 https://www.askme-121.space/ansible/ https://www.askme-121.space/ansible/#respond Mon, 20 May 2024 05:17:56 +0000 https://www.askme-121.pw/?p=526 Ansible 是一个配置管理和应用部署工具,功能类似于目前业界的配置管理工具 Chef,Puppet,Saltstack。Ansible 通过 Python 语言开发,Ansible 默认通过 SSH 协议管理机器,所以 不需要在客户端安装程序,只需要将 Ansible 安装在一台服务器上,就可以去管理控制其它服务器了。不需要为它配置数据库,Ansible 不会以 daemons 方式来启动或保持运行状态。

一、ansible用法

1.1.安装ansible

yum -y install epel-release
yum -y install ansible

1.2.ansible参数选项

选项解释
-m指定要执行的模块,默认为command
-a指定模块的参数
-ussh连接的用户名,默认用root,ansible.cfg中可以配置
-b,–become相当于sudo
–become-usersudo到哪个用户,默认为root
-k提示输入sudo密码,当不是NOPASSWD模式时使用
-C只是测试一下会改变什么内容,不会真正去执行
-ffork多少进程并发处理,默认为5个
-i指定hosts文件路径,默认default=/etc/ansible/hosts
–list-host只打印有哪些主机会执行这个命令,不会实际执行
–private-key私钥路径
-Tssh连接超时时间,默认是10秒
-t日志输出到该目录,日志文件名以主机命名
-vvv显示详细日志

举例:

ansible all --list-host		#列出inventory定义的所有主机
ansible all -m ping -b --become-user root	#sudo到root执行ping模块

二、相关文件介绍

2.1.配置文件

/etc/ansible/ansible.cfg		#主配置文件
/etc/ansible/hosts				#主机清单
/etc/ansible/roles/				#存放角色目录

2.1.1. /etc/ansible/ansible.cfg解释

Ansible查找ansible.cfg文件的位置及顺序

1)ANSIBLE_CONFIG:首先,Ansible命令会检查环境变量,及这个环境变量将指向的配置文件
2)./ansible.cfg:其次,将会检查当前目录下的ansible.cfg配置文件
3)~/.ansible.cfg:再次,将会检查当前用户home目录下的.ansible.cfg配置文件
4)/etc/ansible/ansible.cfg:最后,将会检查在用软件包管理工具安装Ansible时自动产生的配置文件

inventory      = /etc/ansible/hosts		#存放主机清单文件
forks          = 5						#并发执行数量
poll_interval  = 15						#回频率或轮询间隔时间,单位s
sudo_user      = root					#远程主机sudo到什么用户,默认为root
ask_sudo_pass = True					#sudo时是否需要输入密码
ask_pass      = True					#执行ansible-playbook是否需要密码.默认为no
transport      = smart
remote_port    = 22						#远程主机端口号
module_lang    = C						#模块和系统之间通信的语言
module_set_locale = False
roles_path    = /etc/ansible/roles      #默认下载的Roles存放的目录
host_key_checking = False       		#首次连接是否需要检查key认证。设置为False,第一次连接远程主机不需要输入yes
timeout = 10            				#SSH超时时间
remote_user = root      				#连接到被控端机器的用户名,默认为root
log_path = /var/log/ansible.log     	#日志文件存放路径
executable = /bin/sh        			#执行的shell环境,用户shell模块
jinja2_extensions = jinja2.ext.do,jinja2.ext.i18    #允许开启jinja2扩展模块
private_key_file = /path/to/file    	#私钥文件存储位置
system_warnings = True      			#禁用系统运行Ansible潜在问题警告
deprecation_warnings = True    			#PlayBook输出禁用“不建议使用”警告
nocolor = 1         					#输出带上颜色区别,0表示开启,1表示关闭
pipelining = False      				#是否开启pipe SSH通道优化
gathering = explicit					#不收集系统信息,默认收集
[privilege_escalation]
become=True								#是否sudo
become_method=sudo						#sudo方式
become_user=root						#sudo后变为root用户
become_ask_pass=False					#sudo后不验证密码

2.1.2. /etc/ansible/hosts格式

[DB]					#表示DB组,下面的主机均属于该组
192.168.1.2
192.168.1.3
[fastdfs]
192.168.1.2
192.168.1.3  GROUP_NAME="g1"	#定义局部变量
192.168.1.4 ansible_ssh_user=ops ansible_ssh_port=22 ansible_become=true ansible_become_method=sudo	#ansible_become是否允许提权;ansible_become_method指定提权的方式
[k8s]
192.168.1.[4:10]		#[4:10]表示连续的主机,包含4和10
[webserver]
192.168.1.11:2222		#默认为22端口,如果不是,需指定
[linux_group:children]		#children是ansible内置变量,可以实现多个组的调用,linux_group是定义的组名
DB
k8s
[DB:vars]
mysql_port="3306"		#组变量
[all:vars]				#定义全局变量
db_master_ip="{{groups['DB'][0]}}"	#取DB组中的第一个IP			
ansible_ssh_user="devops"			

指定自定义主机清单文件

ansible -i myhost.ini --list-host

2.2.可执行文件

/usr/bin/ansible			#主程序
/usr/bin/ansible-doc		#查看文档
/usr/bin/ansible-galaxy		#连接https://galaxy.ansible.com/下载相应的roles
/usr/bin/ansible-playbook	#调用playbook剧本
/usr/bin/ansible-vault		#文件加密工具

三、配置基于root用户Key的验证

以root用户登录到主控端机器,ssh-keygen生成密钥对,通过ssh-copy-id 远程主机,将公钥拷贝到远程主机

四、ansible命令执行过程

  • 加载配置文件/etc/ansible/ansible.cfg
  • 加载模块文件,如command,ping
  • 通过ansible将模块或命令生成对应的临时py文件,并将该文件传输至远程服务器对应执行用户的$HOME/.ansible/tmp/ansible-tmp-数字 目录下,命名为XXX.PY文件
  • 给文件+x执行权限
  • 执行并返回结果
  • 删除临时py文件,退出

五、执行状态

绿色:执行成功并且不需要做改变的操作
黄色:执行成功并且对目标主机做变更
红色:执行失败

六、常用模块

ansible 192.168.1.2 -m ping -k		#测试主机是否可以通信;默认通过key验证,-k指定以用户名密码验证
ansible all -m ping			#检测主机清单文件中的所有主机
ansible db -m ping			#只检测db组内的主机
ansible *ser* -m ping		#支持通配符
ansible 'db:web' -m ping	#取db和web组的并集
ansible 'db:&web' -m ping	#取db和web组的交集
ansible 'db:!web' -m ping	#在db,不在web
ansible-doc -l				#列出所有模块
ansible-doc ping			#查看ping模块
ansible-doc -s ping			#简单了解ping模块
ansible all --list			#列出主机清单所有主机
ansible all -a 'ls /root'	#默认使用command模板,不支持变量、<、>、|、&、;等,需使用shell模块
ansible all -a 'creates=/ansible mkdir /ansible'	#不存在/ansible,则执行后面的mkdir
ansible all -a 'chdir=/root ls'		#切换到/root下执行ls

ansible all -m shell -a 'echo $HOSTNAME'
ansible all -m script -a '/etc/ansible/scripts/host.sh'	#会自动把主控端的脚本拷贝到被控端,并执行
ansible all -m copy -a 'src=/etc/hosts dest=/ansible/'	#拷贝本机文件到远程主机
ansible 192.168.119.134 -m copy -a "src=/etc/ansible dest=/tmp owner=root group=root mode=0755"	# 拷贝本机目录到远程客户端
ansible all -m copy -a 'content="hello world" dest=/ansible/f1'	#把content里面的内容写入到f1文件,会覆盖之前的内容
ansible all -m fetch -a 'src=/etc/hosts dest=files/ flat=yes'	#拷贝远程主机文件到当前路径的files/目录下;flat只拷贝hosts文件本身,不拷贝hosts文件所在的绝对路径

ansible all -m file -a 'path=/ansible/f2 state=touch'	#创建文件
ansible all -m file -a 'path=/ansible/f2 state=absent'	#删除文件
ansible all -m file -a 'path=/ansible/dir1 state=directory'	#创建目录
ansible all -m file -a 'src=/etc/hosts dest=/tmp/host state=link'	#创建软链接
ansible 10.93.62.156 -m hostname -a 'name=Master'		#修改主机名

ansible all -m cron -a 'minute=* weekday=2,4 job="/usr/bin/wall test warning" name=testcron'	#定时任务
ansible all -m cron -a 'disabled=true job="/usr/bin/wall test warning" name=testcron'		#取消定时任务
ansible all -m cron -a 'state=absent job="/usr/bin/wall test warning" name=testcron'		#删除定时任务

ansible all -m yum -a 'name=vsftpd update_cache=yes'		#yum装包,更新缓存
ansible all -m yum -a 'name=vsftpd state=removed'			#卸载包

ansible all -m user -a 'name=zhangsan shell=/bin/bash'		#创建用户

ansible web -m command -a "chdir=/tmp pwd"		#切换目录,执行命令

指定远程主机用户,并sudo到root用户
注意:需提前去远程主机配置允许普通用户sudo到root

echo "devops ALL=(ALL)  NOPASSWD: ALL" >> /etc/sudoers		#配置免密sudo
ansible all -a 'ls /root' -u devops -k -b	#-a指定命令;-u指定远程主机用户;-b执行sudo,默认sudo到root用户

七、ansible-galaxy

ansible-galaxy collection install nginxinc.nginx_core -c			#下载角色;-c忽略证书认证
ansible-galaxy list			#列出本机所有角色

八、ansible-vault

ansible-vault encrypt hello.yml		#加密yaml文件,需设置密码
ansible-vault view hello.yml		#查看yaml文件内容
ansible-vault edit hello.yml		#修改yaml文件内容
ansible-vault rekey hello.yml		#修改加密口令
ansible-vault decrypt hello.yml		#解密yaml文件

九、ansible-playbook

9.1.核心元素

hosts		#执行的远程主机列表
tasks		#任务集
templates	#模板
handlers	#也是task列表,和一般的task并没有什么区别.Handlers由通知者进行notify, 如果没有被notify,handlers不会执行.不管有多少个通知者进行了notify,等到 play 中的所有 task 执行完成之后,handlers 也只会被执行一次
tags		#打标签,通过标签调用特定的任务;多个任务可以共用一个标签
vars		#定义变量

9.2.playbook格式

---                      #习惯写法,区分不同的档案
# 创建文件
- hosts: db,myql         #选择hosts文件中的主机;可以选择多个主机,使用逗号分隔
  remote_user: root      #指定远程主机用什么用户执行
  gather_facts: no		 #收集远程主机信息,默认为yes

  tasks:                        #任务列表
    - name: create file         #任务说明
      file: path=/ansible/f2 state=touch  mode=0500      #使用file模块,创建文件;mode设置权限
    - name: test1
      shell: echo "{{ groups['mysql'][0] }}" >> /tmp/t1.txt		#获取[mysql]组的第一个成员
      
- hosts: redis:!redis[0]		#排除redis组的第一个成员

9.3.常用技巧

1、ignore_errors忽略错误信息

任务执行失败,不会影响后面任务的执行

- name: create file         
  file: path=/ansible1/f2 state=touch        
  ignore_errors: True

2、拷贝文件使用相对路径

src使用相对路径,会去与该yaml文件同级的files目录下寻找,通常把要拷贝的文件统一放到files目录下

- name: copy hosts
  copy: src=hosts dest=/ansible/ backup=yes

3、handlers+notify解决修改配置文件不生效的问题

当执行copy模板后,通过notify发送通知给名字为restart httpd service的handlers,执行对应的service模板,重启httpd服务

tasks:
  - name: copy conf file
    copy: src=httpd.conf dest=/etc/httpd/conf/ backup=yes
    notify:
      - restart httpd service
  - name: start service
    service: name=httpd state=started enabled=yes
handlers:
  - name: restart httpd service
    service: name=httpd state=restarted

4、tags打标签

特殊tags:always #无论如何都会运行

- name: copy hosts
  copy: src=hosts dest=/ansible/
  tags: copyfile

调用多个标签用逗号分隔

ansible-playbook -t copyfile,sql file.yml

5、when条件判断

根据操作系统的类型来执行对应的任务

tasks:
  - name: create file for RedHat
    file: path=/ansible/redhat state=touch
    when: ansible_os_family == "RedHat"
  - name: create file for ubuntu
    file: path=/ansible/ubuntu state=touch
    when: ansible_os_family == "Ubuntu"

自定义条件判断,根据action的值去判断

6、迭代:with_items

当需要有重复性执行的任务时,可以使用迭代机制

示例:创建多个文件

tasks:
  - name: create files
    file: path=/ansible/{{ item }} state=touch
    with_items:
      - file1
      - file2

示例:迭代嵌套子变量

tasks:
  - name: create group
    group: name={{ item }}
    with_items:
      - g1
      - g2
  - name: create user
    user: name={{ item.name }} group={{ item.group }}
    with_items:
      - { name: "user1", group: "g1"}
      - { name: "user2", group: "g2"}

7、become提权

- name: copy hosts
  copy: src=hosts dest=/ansible/
  become: yes
  become_user: root		#提升为root权限,类似sudo

8、lineinfile(类似于sed)

regexp正则匹配,如果匹配上,则整行替换,类似sed的c操作;如果未匹配,则在末尾添加,类似sed的a操作

- name: modify file
  lineinfile: dest=/ansible/file1 regexp="zabbix soft" line="zabbix soft nproc 65535"

playbook的一个bug:如果内容中有:号会报错,用下面的写法可以避免

- name: add paas sudo
  lineinfile:
    path: /etc/sudoers
    regexp: "^paas "
    line: "paas ALL=(ALL) NOPASSWD: ALL"
    state: present		#present表示要添加或修改匹配的行,默认为present
    backup: yes		#备份文件

同时修改多行

- name: config /etc/sysctl.conf
  lineinfile:
    path: /etc/sysctl.conf
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
    backup: yes
  with_items:
    - { regexp: '^vm.swappiness', line: 'vm.swappiness = 10' }
    - { regexp: '^vm.dirty_ratio', line: 'vm.dirty_ratio = 60' }

9、register注册变量,返回shell脚本执行结果

使用 register 注册变量,名为 result,会把shell模块的输出存入到result
debug模块输出详情

tasks:
  - name: install mysql
    shell: cd /data/shell/ && sh InstallMySql.sh
    register: result
  - name: show install mysql debug
    debug: var=result.stdout_lines verbosity=0
result.stdout			#标准正确输出保存到字符串中
result.stdout_lines		#标准正确输出保存到列表中,输出更美观
result.stderr_lines		#标准错误输出保存到列表中
verbosity		#为1,代表-v;为2,代表-vv;为3,代表-vvv

10、failed_when关键字

’ failed_when’不会影响shell模块的执行过程,只会在条件成立时影响shell模块最终的执行状态,以便停止playbook的运行。

---
- hosts: db
  remote_user: root
  tasks:
  - debug:
      msg: "I execute normally"
  - shell: "echo 'This is a string for testing error' && exit 1"
    register: result
    failed_when: result.rc != 0		
  - debug:
      msg: "I never execute,Because the playbook has stopped

类似于shell中的

if [[ $? -ne 0 ]];then
	exit 1
fi

判断操作系统

- hosts: redhat
  gather_facts: True
  tasks:
   - name: copy redhat7 packages
     copy: 
         src: /tmp/redhat7.tar
         dest: /tmp/redhat7.tar
         mode: '0755'
     when: ansible_distribution == "RedHat" and ansible_distribution_major_version == "7"

匹配多个条件

1、两个条件都满足

failed_when: ' "error" in result.stdout and "fail" in result.stdout'

2、满足两个条件中的任意一个

failed_when: ' "error" in result.stdout or "fail" in result.stdout'

11、任务委派功能delegate_to

实现在当前playbook主机组外的机器上执行操作

run_noce: true 在一个主机上面只执行一次一个任务. ,如果没有这个参数的话,每个playbook中的组的主机都会执行一次

- name: test
  shell: echo "1" >> /root/a.txt
  delegate_to: 127.0.0.1
  run_once: true

委派Ansible所在的机器(即主控端)执行shell里面的操作

12、本地操作功能local_action或connection

Ansible默认只会对控制机器执行操作,但如果在这个过程中需要在Ansible本机执行操作呢?细心的读者可能已经想到了,可以使用delegate_to(任务委派)功能呀。没错,是可以使用任务委派功能实现。不过除了任务委派之外,还可以使用另外一种功能实现,这就是local_action关键字。

- name: add host record to center server
local_action:shell 'echo "192.168.1.100 test.xyz.com " >> /etc/hosts'

当然也可以使用connection:local方法,如下:

- name: add host record to center server
shell: 'echo "192.168.1.100 test.xyz.com " >> /etc/hosts'
connection: local

13、判断文件是否存在

如果不存在,则创建

- name: Check a.txt exists
  stat:
    path: /etc/ansible/a.txt
  register: file_status
- name: create a.txt
  shell: touch a.txt
  when: file_status.stat.exists == False

14、判断指定路径是否为一个目录

- hosts: test
  vars:
    testpath1: "/testdir/test"
    testpath2: "/testdir"
  tasks:
    - debug:
        msg: "file"
      when: testpath1 is file
    - debug:
        msg: "directory"
      when: testpath2 is directory

15、include

调用其它yml,例如:在tasks目录有main.yml和upgrade_sql.yml,则可以在main.yml中调用upgrade_sql.yml

- include: upgrade_sql.yml

16、block模块

将多个任务组合成一个块,并且可以对这个块做条件判断,以及当块里面的任务失败时,进行失败处理

1、当when的判断语句一样时,可以将任务合并

- name: block的用法
  hosts: node
  tasks:
    - debug:
      msg: "task1 not in block"
    - block:
        - debug:
            msg: "task2 in block1"
        - debug:
            msg: "task3 in block1"
      when: 2 > 1

2、”错误处理”功能

- hosts: testuser
  remote_user: root
  tasks:
  - block:
      - command: /bin/false
      - debug:
          msg: 'I never execute, due to the above task failing'
    rescue:				#当上面的模块失败时,该关键字下面的任务将被执行
      - command: /bin/false
      - debug:
          msg: 'I also never execute'
    always:				#不管block块是否执行成功,该关键字定义的任务都要被执行。
      - debug:
          msg: "This always executes"
    when: 2>1

17、pre_tasks在roles之前执行

如果想在调用roles之前执行tasks任务,得用pre_task,不然会跳过tasks,直接调用roles

 - hosts: web
   pre_tasks:
     - name: pre check
       shell: echo "预检查"
   roles:
     - nginx

18、set_fact设置变量

获取本机IP地址,并赋值给变量host_ip_address

- hosts: mysql
  tasks:
    - name: get host ip address
      shell: "ip addr |awk '/inet /' |sed -n '2p' |awk -F' ' '{print $2}' |awk -F'/' '{print $1}'"
      register: host_ip
      changed_when: false
    - name: set host_ip_address variable
      set_fact:
        host_ip_address: "{{ host_ip.stdout }}"
    - name: show host_ip_address
      debug: msg={{ host_ip_address }}

19、validate: 在复制之前执行检测,如果要引用目标文件名,则使用 %s

- name: copy nginx.conf to {{ NGINX_DATA }}/conf
  template:
    src: nginx.conf.j2
    dest: "{{ NGINX_DATA }}/conf/nginx.conf"
    backup: yes
    validate: 'nginx -t -c %s'

9.4.使用变量

变量名由字母、数字、下划线组成,只能以字母开头

打印所有变量

ansible web -m debug -a 'var=vars'

a.系统facts收集

系统变量在playbook中可以直接通过{{ 名字 }}调用,如{{ ansible_hostname }}

ansible web -m setup			#打印所有系统信息
ansible web -m setup -a 'filter=ansible_hostname'				#获取主机名
ansible web -m setup -a 'filter=ansible_all_ipv4_addresses'		#获取IP地址

在模板中使用

"{{ ansible_default_ipv4['address'] }}"		#获取第一个IPv4地址

b.在yaml文件中通过vars定义

- hosts: db
  remote_user: root
  vars:
    - filename: f4

  tasks:
    - name: create file
      file: path=/ansible/{{ filename }} state=touch

c.在/etc/ansible/group_vars/all定义[推荐使用]

定义的是全局变量,主机清单中的主机均可以调用

ANSIBLE_DIR: "/opt/ansible"     #存放ansible拷贝的文件
SERVICE_USER: "admin"           #启动服务的用户
DATA_DIR: "/app"                #数据主目录
JDK_DIR: "{{ DATA_DIR }}/jdk"   #jdk数据路径

d.在/etc/ansible/hosts中定义

http_port为针对单台主机设置的变量,nodename为针对web组所有主机设置的变量

[web]
10.93.91.80    http_port=80
10.93.65.2     http_port=81
[web:vars]
nodename=www
[all:vars]				#定义全局变量
web_master_ip="{{groups['web'][0]}}"	#取web组中的第一个IP

e.通过ansible-playbook -e传入变量,优先级高于/etc/ansible/hosts【不推荐】

playbook调用变量(两个双花括号里面写变量名)

- name: create file
  file: path=/ansible/{{ filename }} state=touch

通过-e传入变量

ansible-playbook -e 'filename=f3' file.yml

9.5.常用命令

选项解释
-f并发执行数量,默认为5
–ssh-common-args指定要传递给sftp/scp/ssh的公共参数(例如ProxyCommand)
-i指定主机清单文件,如果安装了ansible工具,不指定默认使用/etc/ansible/hosts文件
ansible-playbook -C file.yml		#只检测可能会发生的改变,并不真正执行
ansible-playbook file.yml			#执行playbook
ansible-playbook file.yml --list-hosts 			#列出运行任务的主机
ansible-playbook file.yml --limit 10.93.65.2 	#只在指定的主机执行
ansible-playbook file.yml --list-tasks			#查看任务列表
ansible-playbook file.yml --list-tags			#查看标签列表
python ./ansible-playbook -f 3 -i hosts-192.168.137.3 /tmp/test.yml	#通过python调用ansible-playbook,把ansible-playbook的内容放到ansible-playbook目录下即可
ansible-playbook -e @~/elk/secret.yml	# 将secret.yml中的内容解析成变量

9.6.模板templates

使用jinjia2语言,在与playbook同级的目录下创建templates目录,文件名以.j2结尾

1、简单运算

vi templates/nginx.conf.j2
这里面做了一个简单的计算,让远程主机CPU的个数乘以2

user nginx
workproceess {{ ansible_processor_vcpus*2 }}

playbook写法

- name: copy templates
  template: src=nginx.conf.j2 dest=/ansible/nginx.conf

2、for循环+if判断

for循环格式:

{% for port in ports %}
语句块1
语句块2
...
{% endfor %}

if判断格式:

单分支
{% if p.name is defined %}		#如果变量不为空,则执行里面的语句块
语句块
{% endif %}

#多分支
{% if db_type== "mysql" %}
DB_IP=192.168.1.2
{% elif db_type== "oracle" %}
DB_IP=192.168.1.3
{% endif %}

playbook写法:

- hosts: all
  remote_user: root
  vars:
    ports:
      - port: 80
        name: web1
      - port: 81

  tasks:
  - name: copy templates
    template: src=nginx.conf.j2 dest=/ansible/nginx.conf

模板文件:

{% for p in ports %}
server {
    listen {{ p.port }}

{% if p.name is defined %}			
    servername {{ p.name }}
{% endif %}
}
{% endfor %}

3、在模板中获取主机组的IP

groups[‘web’]可以获取hosts文件中的web主机组定义的主机

{% if groups['web'] is defined and groups['web'][0] is defined %}		#如果web组定义,且至少有一个主机
upstream web {
{% for ip in groups['web'] %}
    server {{ ip }}:{{ WEB_PORT}} fail_timeout=5s max_fails=3;
{% endfor %}
{% endif %}

4、在模板中定义列表

{% for service_name in ['web','mysql'] %}
{{ service_name }}
{% endfor %}

5、获取主机组数量

"{{ groups['master'] | length }}"

十、roles

用于层次性、结构化地组织playbook,可以看成是一大堆playbook的集合。分别将变量、文件、模板、任务放置于单独的目录中

]]>
https://www.askme-121.space/ansible/feed/ 0
PHP-FPM是如何工作的? https://www.askme-121.space/php-fpm/ https://www.askme-121.space/php-fpm/#respond Sat, 20 Apr 2024 05:43:41 +0000 https://www.askme-121.pw/?p=518 首先了解一下几个知识点。
CGI:是 Web Server 与 Web Application 之间数据交换的一种协议。
FastCGI:同 CGI,是一种通信协议,但比 CGI 在效率上做了一些优化。
PHP-CGI:是 PHP (Web Application)对 Web Server 提供的 CGI 协议的接口程序。
PHP-FPM:是 PHP(Web Application)对 Web Server 提供的 FastCGI 协议的接口程序,额外还提供了相对智能一些任务管理。

CGI工作流程

1.如果客户端请求的是 index.html,那么Web Server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。

2.当Web Server收到 index.php 这个请求后,会启动对应的 CGI 程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以CGI规定的格式返回处理后的结果,退出进程,Web server再把结果返回给浏览器。

FastCGI工作流程

1.如果客户端请求的是 index.html,那么Web Server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。

2.当Web Server收到 index.php 这个请求后,FastCGI程序(FastCGI在启动时就初始化执行执行环境,每个CGI进程池各个CGI进程共享执行环境)在CGI进程池中选择一个CGI进程处理请求,再以CGI规定的格式返回处理后的结果,继续等待下一个请求。

PHP-FPM基本实现

PHP-FPM的实现就是创建一个master进程,在master进程中创建worker pool并让其监听socket,然后fork出多个子进程(work)。

这些子进程各自accept请求,子进程的处理非常简单,它在启动后阻塞在accept上,有请求到达后开始读取请求数据,读取完成后开始处理然后再返回。

在这期间是不会接收其它请求的,也就是说PHP-FPM的子进程同时只能响应一个请求,只有把这个请求处理完成后才会accept下一个请求

PHP-FPM的master进程与worker进程之间不会直接进行通信,master通过共享内存获取worker进程的信息。

比如worker进程当前状态、已处理请求数等,当master进程要杀掉一个worker进程时则通过发送信号的方式通知worker进程。

PHP-FPM可以同时监听多个端口,每个端口对应一个worker pool,而每个pool下对应多个worker进程

Worker工作流程

1.等待请求:worker进程阻塞在fcgi_accept_request()等待请求;

2.解析请求:fastcgi请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达;

3.请求初始化:执行php_request_startup(),此阶段会调用每个扩展的:PHP_RINIT_FUNCTION();

4.编译、执行:由php_execute_script()完成PHP脚本的编译、执行;

5.关闭请求:请求完成后执行php_request_shutdown(),此阶段会调用每个扩展的:PHP_RSHUTDOWN_FUNCTION(),然后进入步骤(1)等待下一个请求。

Master进程管理

1.static: 这种方式比较简单,在启动时master按照pm.max_children配置fork出相应数量的worker进程,即worker进程数是固定不变的

2.dynamic: 动态进程管理,首先在fpm启动时按照pm.start_servers初始化一定数量的worker。

运行期间如果master发现空闲worker数低于pm.min_spare_servers配置数(表示请求比较多,worker处理不过来了)则会fork worker进程,但总的worker数不能超过pm.max_children。

如果master发现空闲worker数超过了pm.max_spare_servers(表示闲着的worker太多了)则会杀掉一些worker,避免占用过多资源,master通过这4个值来控制worker数

3.ondemand: 这种方式一般很少用,在启动时不分配worker进程,等到有请求了后再通知master进程fork worker进程。

总的worker数不超过pm.max_children,处理完成后worker进程不会立即退出,当空闲时间超过pm.process_idle_timeout后再退出

PHP-FPM事件管理器

1.sp管道可读事件:这个事件是master用于处理信号的

2.fpm_pctl_perform_idle_server_maintenance_heartbeat():这是进程管理实现的主要事件。

master启动了一个定时器,每隔1s触发一次,主要用于dynamic、ondemand模式下的worker管理,master会定时检查各worker pool的worker进程数,通过此定时器实现worker数量的控制

3.fpm_pctl_heartbeat():这个事件是用于限制worker处理单个请求最大耗时的,php-fpm.conf中有一个request_terminate_timeout的配置项,如果worker处理一个请求的总时长超过了这个值那么master将会向此worker进程发送kill -TERM信号杀掉worker进程,此配置单位为秒,默认值为0表示关闭此机制

4.fpm_pctl_on_socket_accept():ondemand模式下master监听的新请求到达的事件,因为ondemand模式下fpm启动时是不会预创建worker的,有请求时才会生成子进程,所以请求到达时需要通知master进程

]]>
https://www.askme-121.space/php-fpm/feed/ 0
PHP开发api签名验证 https://www.askme-121.space/php-api-sign/ https://www.askme-121.space/php-api-sign/#respond Sat, 20 Apr 2024 04:17:40 +0000 https://www.askme-121.pw/?p=517 开发过程中,我们经常会与接口打交道,有的时候是调取别人网站的接口,有的时候是为他人提供自己网站的接口,但是在这调取的过程中都离不开签名验证。

我们在设计签名验证的时候,请注意要满足以下几点:

  • 可变性:每次的签名必须是不一样的。
  • 时效性:每次请求的时效,过期作废等。
  • 唯一性:每次的签名是唯一的。
  • 完整性:能够对传入数据进行验证,防止篡改。

这里介绍一种方式,是目前国内互联网公司常用的一种方式,其中淘宝的支付宝支付接口、淘宝开放平台接口、腾讯开放平台等应用的一种方式。

一、签名参数sign生成的方法

第1步: 将所有参数(注意是所有参数),除去sign本身,以及值是空的参数,按参数名字母升序排序。

第2步: 然后把排序后的参数按参数1值1参数2值2…参数n值n(这里的参数和值必须是传输参数的原始值,不能是经过处理的,如不能将”转成”后再拼接)的方式拼接成一个字符串。

第3步: 把分配给接入方的验证密钥key拼接在第2步得到的字符串前面。

第2步: 在上一步得到的字符串前面加上验证密钥key(这里的密钥key是接口提供方分配给接口接入方的),然后计算md5值,得到32位字符串,然后转成大写.

第4步: 计算第3步字符串的md5值(32位),然后转成大写,得到的字符串作为sign的值。

举例:

假设传输的数据是/interface.php?sign=sign_value&p2=v2& p1=v1&method=cancel&p3=&pn=vn(实际情况最好是通过post方式发送),其中sign参数对应的sign_value就是签名的值。

第一步,拼接字符串,首先去除sign参数本身,然后去除值是空的参数p3,剩下p2=v2&p1=v1&method=cancel& amp;pn=vn,然后按参数名字符升序排序,method=cancel&p1=v1&p2=v2&pn=vn.

第二步,然后做参数名和值的拼接,最后得到methodcancelp1v1p2v2pnvn
第三步,在上面拼接得到的字符串前加上验证密钥key,我们假设是abc,得到新的字符串abcmethodcancelp1v1p2v2pnvn

第四步,然后将这个字符串进行md5计算,假设得到的是abcdef,然后转为大写,得到ABCDEF这个值即为sign签名值。

注意,计算md5之前请确保接口与接入方的字符串编码一致,如统一使用utf-8编码或者GBK编码,如果编码方式不一致则计算出来的签名会校验失败。

二、签名验证方法

根据前面描述的签名参数sign生成的方法规则,计算得到参数的签名值,和参数中通知过来的sign对应的参数值进行对比,如果是一致的,那么就校验通过,如果不一致,说明参数被修改过。

三、下面直接看代码

<?php

// 设置一个公钥(key)和私钥(secret),公钥用于区分用户,私钥加密数据,不能公开
$key = "c4ca4238a0b923820dcc509a6f75849b";
$secret = "28c8edde3d61a0411511d3b1866f0636";

// 待发送的数据包
$data = array(
    'username' => 'abc@qq.com',
    'sex' => '1',
    'age' => '16',
    'addr' => 'guangzhou',
    'key' => $key,
    'timestamp' => time(),
);

// 获取sign
function getSign($secret, $data) {
    // 对数组的值按key排序
    ksort($data);
    // 生成url的形式
    $params = http_build_query($data);
    // 生成sign
    $sign = md5($params . $secret);
    return $sign;
}

// 发送的数据加上sign
$data['sign'] = getSign($secret, $data);

/**
 * 后台验证sign是否合法
 * @param  [type] $secret [description]
 * @param  [type] $data   [description]
 * @return [type]         [description]
 */
function verifySign($secret, $data) {
    // 验证参数中是否有签名
    if (!isset($data['sign']) || !$data['sign']) {
        echo '发送的数据签名不存在';
        die();
    }
    if (!isset($data['timestamp']) || !$data['timestamp']) {
        echo '发送的数据参数不合法';
        die();
    }
    // 验证请求, 10分钟失效
    if (time() - $data['timestamp'] > 600) {
        echo '验证失效, 请重新发送请求';
        die();
    }
    $sign = $data['sign'];
    unset($data['sign']);
    ksort($data);
    $params = http_build_query($data);
    // $secret是通过key在api的数据库中查询得到
    $sign2 = md5($params . $secret);
    if ($sign == $sign2) {
        die('验证通过');
    } else {
        die('请求不合法');
    }
}
]]>
https://www.askme-121.space/php-api-sign/feed/ 0
用PHP实现SSO单点登录 https://www.askme-121.space/php-sso/ https://www.askme-121.space/php-sso/#respond Sat, 20 Apr 2024 02:33:31 +0000 https://www.askme-121.pw/?p=516 SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制。它是目前比较流行的企业业务整合的解决方案之一,下面我们来看看吧。

简单讲一下 SSO 单点登录系统的接入的原理,前提是系统本身有完善的用户认证功能,即基本的用户登录功能,那实现起来就很方便了。

SSO 登录请求接口往往是接口加上一个回调地址,访问这个地址会跳转到回调地址并带上一个 ticket 参数,拿着这个 ticket 参数再请求接口可以获取到用户信息,如果存在用户则自动登录,不存在就新增用户并登录。

要使用 PHP 实现单点登录(Single Sign-On,SSO),可以借助一些标准和协议,如OAuth 2.0、OpenID Connect 或 SAML(Security Assertion Markup Language)。

下面是一个使用 OAuth 2.0 实现 SSO 的简单示例:

创建认证服务器(Authorization Server):该服务器负责处理用户认证和发放访问令牌。

// authorization_server.php

// 用户登录验证逻辑,验证用户名和密码是否正确
function validateUser($username, $password) {
    // 进行用户验证逻辑,返回验证结果
    // ...
}

// 发放访问令牌给授权成功的用户
function issueAccessToken($username) {
    // 生成访问令牌并返回
    // ...
}

// 处理用户登录请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'];
    $password = $_POST['password'];

    // 验证用户名和密码
    if (validateUser($username, $password)) {
        // 发放访问令牌
        $accessToken = issueAccessToken($username);

        // 将访问令牌返回给客户端
        echo json_encode(['access_token' => $accessToken]);
        exit;
    } else {
        // 用户认证失败
        http_response_code(401);
        exit;
    }
}

创建客户端应用(Client Application):该应用将使用认证服务器发放的访问令牌来验证用户身份。

// client_application.php

// 验证访问令牌是否有效
function validateAccessToken($accessToken) {
    // 进行访问令牌验证逻辑,返回验证结果
    // ...
}

// 处理用户登录请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $accessToken = $_POST['access_token'];

    // 验证访问令牌
    if (validateAccessToken($accessToken)) {
        // 用户已验证通过,执行单点登录逻辑
        // ...
        echo 'Login successful';
        exit;
    } else {
        // 访问令牌验证失败
        http_response_code(401);
        exit;
    }
}

用户登录和单点登录流程:

  • 用户访问 Client Application,检查用户是否已登录。
  • 如果用户未登录,则跳转到认证服务器的登录页面。
  • 用户在认证服务器上提供用户名和密码进行登录认证。
  • 认证服务器验证用户名和密码,并发放访问令牌。
  • 用户被重定向回 Client Application,并将访问令牌发送到 Client Application。
  • Client Application 验证访问令牌的有效性,确认用户已成功登录,执行相应的单点登录逻辑。

请注意,上述示例只是一个简单的演示,实际的 SSO 实现可能更复杂,涉及到会话管理、安全性等方面的考虑。您可以根据具体的需求和使用的协议,进一步完善和扩展这个基本示例。

]]>
https://www.askme-121.space/php-sso/feed/ 0
Swoole协程与Go协程有哪些区别? https://www.askme-121.space/swoole-go/ https://www.askme-121.space/swoole-go/#respond Mon, 29 Jan 2024 11:23:23 +0000 https://www.askme-121.pw/?p=504 一、进程、线程、协程

进程是什么?

进程就是应用程序的启动实例。
例如:打开一个软件,就是开启了一个进程。
进程拥有代码和打开的文件资源,数据资源,独立的内存空间。

线程是什么?

线程属于进程,是程序的执行者。
一个进程至少包含一个主线程,也可以有更多的子线程。
线程有两种调度策略,一是:分时调度,二是:抢占式调度。

协程是什么?

协程是轻量级线程, 协程的创建、切换、挂起、销毁全部为内存操作,消耗是非常低的。
1 协程是属于线程,协程是在线程里执行的。
2 协程的调度是用户手动切换的,所以又叫用户空间线程。
3 协程的调度策略是:协作式调度。

为什么要用协程

目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念就是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务。其实不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。因为协程是用户自己来编写调度逻辑的,对于我们的CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。

协程相对于多线程的优点

多线程编程是比较困难的, 因为调度程序任何时候都能中断线程, 必须记住保留锁, 去保护程序中重要部分, 防止多线程在执行的过程中断。而协程默认会做好全方位保护, 以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说, 无需保留锁, 而在多个线程之间同步操作, 协程自身就会同步, 因为在任意时刻, 只有一个协程运行。总结下大概下面几点:

  • 无需系统内核的上下文切换,减小开销;
  • 无需原子操作锁定及同步的开销,不用担心资源共享的问题;
  • 单线程即可实现高并发,单核 CPU 即便支持上万的协程都不是问题,

所以很适合用于高并发处理,尤其是在应用在网络爬虫中。

二、Swoole 协程

swoole协程为什么是单线程

在swoole中,因为协程的切换是串行的,在同一个时间点只能运行一个协程,一个协程正在运行时,其他协程会停止工作,所以swoole的协程是基于单线程的。

swoole协程的调度方式是什么

线程的调度方式为系统调度,常用的调度策略有分时调度、抢占调度。说白就是线程的调度完全不受自己控制,协程的调度方式为协作式调度,不受内核控制由自由策略调度切换。上述说了协程是用户态的,所以所谓的协作式调度直接可以理解为是程序员写的调度方式,也就是我想怎么调度就怎么调度,而不用通过系统内核被调度。

Swoole 的协程客户端必须在协程的上下文环境中使用。

// 第一种情况:Request 回调本身是协程环境
$server->on('Request', function($request, $response) {
    // 创建 Mysql 协程客户端
    $mysql = new Swoole\Coroutine\MySQL();
    $mysql->connect([]);
    $mysql->query();
});

// 第二种情况:WorkerStart 回调不是协程环境
$server->on('WorkerStart', function() {
    // 需要先声明一个协程环境,才能使用协程客户端
    go(function(){
        // 创建 Mysql 协程客户端
        $mysql = new Swoole\Coroutine\MySQL();
        $mysql->connect([]);
        $mysql->query();
    });
});

Swoole 的协程是基于单线程的, 无法利用多核CPU,同一时间只有一个在调度。

// 启动 4 个协程
$n = 4;
for ($i = 0; $i < $n; $i++) {
    go(function () use ($i) {
        // 模拟 IO 等待
        Co::sleep(1);
        echo microtime(true) . ": hello $i " . PHP_EOL;
    });
};
echo "hello main \n";

// 每次输出的结果都是一样
$ php test.php 
hello main 
1558749158.0913: hello 0 
1558749158.0915: hello 3 
1558749158.0915: hello 2 
1558749158.0915: hello 1

Swoole 协程使用示例及详解

// 创建一个 Http 服务
$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

// 调用 onRequest 事件回调函数时,底层会调用 C 函数 coro_create 创建一个协程,
// 同时保存这个时间点的 CPU 寄存器状态和 ZendVM stack 信息。
$server->on('Request', function($request, $response) {
    // 创建一个 Mysql 的协程客户端
    $mysql = new Swoole\Coroutine\MySQL();

    // 调用 mysql->connect 时发生 IO 操作,底层会调用 C 函数 coro_save 保存当前协程的状态,
    // 包括 Zend VM 上下文以及协程描述的信息,并调用 coro_yield 让出程序控制权,当前的请求会挂起。
    // 当协程让出控制权之后,会继续进入 EventLoop 处理其他事件,这时 Swoole 会继续去处理其他客户端发来的 Request。
    $res = $mysql->connect([
        'host'     => '127.0.0.1',
        'user'     => 'root',
        'password' => 'root',
        'database' => 'test'
    ]);

    // IO 事件完成后,MySQL 连接成功或失败,底层调用 C 函数 coro_resume 恢复对应的协程,恢复 ZendVM 上下文,继续向下执行 PHP 代码。
    if ($res == false) {
        $response->end("MySQL connect fail");
        return;
    }

    // mysql->query 的执行过程和 mysql->connect 一致,也会进行一次协程切换调度
    $ret = $mysql->query('show tables', 2);

    // 所有操作完成后,调用 end 方法返回结果,并销毁此协程。
    $response->end('swoole response is ok, result='.var_export($ret, true));
});

// 启动服务
$server->start();

三、Go 的协程 goroutine

1. goroutine 是轻量级的线程,Go 语言从语言层面就支持原生协程。
2. Go 协程与线程相比,开销非常小。
3. Go 协程的堆栈开销只用2KB,它可以根据程序的需要增大和缩小,
而线程必须指定堆栈的大小,并且堆栈的大小都是固定的。
4. goroutine 是通过 GPM 调度模型实现的。
M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。
G: 表示一个 goroutine,它有自己的栈。
P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。Go 在 runtime、系统调用等多个方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或进行系统调用时,会主动把当前协程的 CPU 转让出去,让其他协程调度执行。

Go 语言原生层面就支持协层,不需要声明协程环境。

package main

import "fmt"

func main() {
    // 直接通过 Go 关键字,就可以启动一个协程。
    go func() {
        fmt.Println("Hello Go!")
    }()
}

Go 协程是基于多线程的,可以利用多核 CPU,同一时间可能会有多个协程在执行。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 设置这个参数,可以模拟单线程与 Swoole 的协程做比较
    // 如果这个参数设置成 1,则每次输出的结果都一样。
    // runtime.GOMAXPROCS(1)

    // 启动 4 个协程
    var i int64
    for i = 0; i < 4; i++ {
        go func(i int64) {
            // 模拟 IO 等待
            time.Sleep(1 * time.Second)
            fmt.Printf("hello %d \n", i)
        }(i)
    }

    fmt.Println("hello main")

    // 等待其他的协程执行完,如果不等待的话,
    // main 执行完退出后,其他的协程也会相继退出。
    time.Sleep(10 * time.Second)
}

// 第一次输出的结果
$ go run test.go
hello main
hello 2 
hello 1 
hello 0 
hello 3 

// 第二次输出的结果
$ go run test.go
hello main
hello 2 
hello 0 
hello 3 
hello 1 

// 依次类推,每次输出的结果都不一样

Go 协程使用示例及详解

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    "net/http"
    "time"
)
import _ "github.com/go-sql-driver/mysql"

func main() {
    dsn := fmt.Sprintf("%v:%v@(%v:%v)/%v?charset=utf8&parseTime=True&loc=Local",
        "root",
        "root",
        "127.0.0.1",
        "3306",
        "fastadmin",
    )
    db, err := gorm.Open("mysql", dsn)
    if err != nil {
        fmt.Printf("mysql connection failure, error: (%v)", err.Error())
        return
    }
    db.DB().SetMaxIdleConns(10)  // 设置连接池
    db.DB().SetMaxOpenConns(100) // 设置与数据库建立连接的最大数目
    db.DB().SetConnMaxLifetime(time.Second * 7)

    http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) {
        // http Request 是在协程中处理的
        // 在 Go 源码 src/net/http/server.go:2851 行处 `go c.serve(ctx)` 给每个请求启动了一个协程
        var name string
        row := db.Table("fa_auth_rule").Where("id = ?", 1).Select("name").Row()
        err = row.Scan(&name)
        if err != nil {
            fmt.Printf("error: %v", err)
            return
        }
        fmt.Printf("name: %v \n", name)
    })
    http.ListenAndServe("0.0.0.0:8001", nil)
}

四、案例分析

背景:在我们的积分策略服务系统中,使用到了 mongodb 存储,但是 swoole 没有提供 mongodb 协程客户端。那么这种场景下,在连接及操作 Mongodb 时会发生同步阻塞,无法发生协程切换,导致整个进程都会阻塞。在这段时间内,进程将无法再处理新的请求,这使得系统的并发性大大降低。

使用同步的 mongodb 客户端

$server->on('Request', function($request, $response) {
    // swoole 没有提供协程客户端,那么只能使用同步客户端
    // 这种情况下,进程阻塞,无法切换协程
    $m = new MongoClient();    // 连接到mongodb
    $db = $m->test;            // 选择一个数据库
    $collection = $db->runoob; // 选择集合
    // 更新文档
    $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
    $cursor = $collection->find();
    foreach ($cursor as $document) {
        echo $document["title"] . "\n";
    }
}}

通过使用 Server->taskCo 来异步化对 mongodb 的操作

$server->on('Task', function (swoole_server $serv, $task_id, $worker_id, $data) {
    $m = new MongoClient();    // 连接到mongodb
    $db = $m->test;            // 选择一个数据库
    $collection = $db->runoob; // 选择集合
    // 更新文档
    $collection->update(array("title"=>"MongoDB"), array('$set'=>array("title"=>"Swoole")));
    $cursor = $collection->find();
    foreach ($cursor as $document) {
        $data = $document["title"];
    }
    return $data;
});

$server->on('Request', function ($request, $response) use ($server) {
    // 通过 $server->taskCo() 把对 mongodb 的操作,投递到异步 task 中。
    // 投递到异步 task 后,将发生协程切换,可以继续处理其他的请求,提供并发能力。
    $tasks[] = "hello world";
    $result = $server->taskCo($tasks, 0.5);
    $response->end('Test End, Result: '.var_export($result, true));
});

上面两种使用方式就是 Swoole 中常用的方法了。
那么我们在 Go 中怎么处理这种同步的问题呢 ?

实际上在 Go 语言中就不用担心这个问题了,如我们之前所说到的,
Go 在语言层面就已经支持协程了,只要是发生 IO 操作,网络请求都会发生协程切换。这也就是 Go 语言天生以来就支持高并发的原因了。

package main

import (
    "fmt"
    "gopkg.in/mgo.v2"
    "net/http"
)

func main() {
    http.HandleFunc("/test", func(writer http.ResponseWriter, request *http.Request) {
        session, err := mgo.Dial("127.0.0.1:27017")
        if err != nil {
            fmt.Printf("Error: %v \n", err)
            return
        }
        session.SetMode(mgo.Monotonic, true)
        c := session.DB("test").C("runoob")
        fmt.Printf("Connect %v \n", c)
    })
    http.ListenAndServe("0.0.0.0:8001", nil)
}

并行:同一时刻,同一个 CPU 只能执行同一个任务,要同时执行多个任务,就需要有多个 CPU。

并发:CPU 切换时间任务非常快,就会感觉到有很多任务在同时执行。

五、协程 CPU 密集场景调度

我们上面说到都是基于 IO 密集场景的调度。
那么如果是 CPU 密集型的场景,应该怎么处理呢?在 Swoole v4.3.2 版本中,已经支持了协程 CPU 密集场景的调度。想要支持 CPU 密集调度,需要在编译时增加编译选项 --enable-scheduler-tick 开启 tick 调度器。其次还需要我们手动声明 declare(tick=N) 语法功能来实现协程调度。

<?php
declare(ticks=1000);

$max_msec = 10;
Swoole\Coroutine::set([
    'max_exec_msec' => $max_msec,
]);

$s = microtime(1);
echo "start\n";
$flag = 1;
go(function () use (&$flag, $max_msec){
    echo "coro 1 start to loop for $max_msec msec\n";
    $i = 0;
    while($flag) {
        $i ++;
    }
    echo "coro 1 can exit\n";
});

$t = microtime(1);
$u = $t-$s;
echo "shedule use time ".round($u * 1000, 5)." ms\n";
go(function () use (&$flag){
    echo "coro 2 set flag = false\n";
    $flag = false;
});
echo "end\n";

// 输出结果
start
coro 1 start to loop for 10 msec
shedule use time 10.2849 ms
coro 2 set flag = false
end
coro 1 can exit

Go 在 CPU 密集运算时,有可能导致协程无法抢占 CPU 会一直挂起。这时候就需要显示的调用代码 runtime.Gosched() 挂起当前协程,让出 CPU 给其他的协程。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 如果设置单线程,则第一个协程无法让出时间片
    // 第二个协程一直得不到时间片,阻塞等待。
    // runtime.GOMAXPROCS(1)

    flag := true

    go func() {
        fmt.Printf("coroutine one start \n")
        i := 0
        for flag {
            i++
            // 如果加了这行代码,协程可以让时间片
            // 这个因为 fmt.Printf 是内联函数,这是种特殊情况
            // fmt.Printf("i: %d \n", i)
        }
        fmt.Printf("coroutine one exit \n")
    }()

    go func() {
        fmt.Printf("coroutine two start \n")
        flag = false
        fmt.Printf("coroutine two exit \n")
    }()

    time.Sleep(5 * time.Second)
    fmt.Printf("end \n")
}

// 输出结果
coroutine one start 
coroutine two start 
coroutine two exit 
coroutine one exit 
end

注:time.sleep() 模拟 IO 操作,for i++ 模拟 CPU 密集运算。

总结

  • 协程是轻量级的线程,开销很小。
  • Swoole 的协程客户端需要在协程的上下文环境中使用。
  • 在 Swoole v4.3.2 版本之后,已经支持协程 CPU 密集场景调度。
  • Go 语言层面就已经完全支持协程了。
]]>
https://www.askme-121.space/swoole-go/feed/ 0
静态化API是什么?用Swoole如何去实现呢? https://www.askme-121.space/static-api/ https://www.askme-121.space/static-api/#respond Mon, 29 Jan 2024 06:25:32 +0000 https://www.askme-121.pw/?p=503 什么是静态化API?
静态化API可以理解成把一些接口的数据存储在服务器本地。常用的是存成json文件,也可以是放在swoole的table中,总之是用户不从数据库直接读取数据,而是从本地加载的方式来大幅提高性能,因为很多系统的性能瓶颈是在数据库的位置。

解决方案
方案1 easySwoole + crontab
方案2 easySwoole定时器
方案3 Swoole table
方案4 Redis

实现
这里做的分页的场景,不包含分页的源码,只从拿到了分页的数据看定时生成json和获取json的部分

原始的方法 – 读取mysql

这是原始的方法,每个用户访问都会去数据库里面读取一次,每一次分页也会访问数据库,会造成大量的资源开销。

public function lists0(){
    $condition = [];
    if(!empty($this->params['cat_id'])){
        $condition['cat_id'] = intval($this->params['cat_id']);
    }

    try {
        $videoModel = new VideoModel();
        $data = $videoModel->getVideoData($condition, $this->params['page'], $this->params['size']);
    } catch (\Exception $e) {
        //$e->getMessage();
        return $this->writeJson(Status::CODE_BAD_REQUEST,"服务异常");
    }

    if(!empty($data['lists'])){
        foreach ($data['lists'] as &$list){
            $list['create_time'] = date("Ymd H:i:s",$list['create_time']);
            $list['video_duration'] = gmstrftime("%H:%M:%S",$list["video_duration"]);
        }
    }

    return $this->writeJson(Status::CODE_OK,'OK',$data);
}

知识点:

1 获取以秒为单位的时间长度使用gmstrftime(“%H:%M:%S”,$list[“video_duration”])
2 在做模型的时候写一个基类,把连接数据库的工作放在这个基类的构造方法当中,这样每次实例化的时候就自动连接了,提高代码的复用性

<?php


namespace App\Model;

use EasySwoole\Core\Component\Di;

class Base
{
    public $db = "";

    public function __construct()
    {
        if(empty($this->tableName)){
            throw new \Exception("table error");
        }
        $db = Di::getInstance()->get("MYSQL");
        if($db instanceof \MysqliDb){
            $this->db = $db;
        }else{
            throw new \Exception("db error");
        }
    }

    public function add($data){
        if(empty($data) || !is_array($data)){
            return false;
        }
        return $this->db->insert($this->tableName,$data);
    }

}

方案一、方案二 使用easySwoole定时器以及contab的生成静态化API

这是直接读取静态化API的方法,其实就是读文件然后返回给前端

/**
 * 第二套方法 - 直接读取静态化json数据
 * @return bool
 */
public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    $videoFile = EASYSWOOLE_ROOT."/webroot/video/json/".$catId.".json";
    $videoData = is_file($videoFile) ? file_get_contents($videoFile) : [];
    var_dump($videoData);
    $videoData = !empty($videoData) ? json_decode($videoData,true) : [];

    $count = count($videoData);
    // var_dump($videoData);
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}

这里的getPagingData使用了array_slice来切割数组来做分页。

/**
 * 获取分页信息
 * @param $count
 * @param $data
 * @return array
 */
public function getPagingData($count, $data){
    $totalPage = ceil($count / $this->params['size']);
    $data = $data ?? [];
    $data = array_slice($data, $this->params['from'], $this->params['size']);
    return [
        'total_page' => $totalPage,
        'page_size' => $this->params['page'],
        'count' => intval($count),
        'list' => $data
    ];

}

定时生成Json文件代码 – 核心部分 全局事件easySwooleEvent->mainServiceCreate中执行,这样easySwoole启动了之后就会执行,这里使用的原生的crontab,只能够精确到分

$cacheVideoObj = new VideoCache();
// 使用cronTab处理定时任务
// 这里是闭包 要use $cacheVideoObj之后才能获取,实例化不放在function中是为了防止每次都实例化浪费资源
CronTab::getInstance()
    ->addRule("test_bing_crontab", '*/1 * * * *', function () use($cacheVideoObj) {
        $cacheVideoObj->setIndexVideo();
    });

也可以使用easySwoole的定时器来实现也是放在mainServiceCreate中执行,这里的代码一定要注意要注册一个onWorkerStart。
然后指定一个进程去执行这个定时任务注意一下这里闭包里面又有一个闭包,外面的变量要use两次。
一定要注意easwoole定时器的使用,这里的坑比较多一定要注意,优势是swoole的定时器可以到毫秒级而contab只能到分级

// easySwoole自带定时器
$register->add(EventRegister::onWorkerStart,function (\swoole_server $server,$workerId)use($cacheVideoObj){
    //让第一个进程去执行,否则每个进程都会执行
    if($workerId == 0){
        Timer::loop(1000*2,function ()use ($cacheVideoObj){
            $cacheVideoObj->setIndexVideo();
        });
    }
});

方案三 Swoole table的解决方案

swoole_table一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程/多线程数据共享和同步加锁问题。性能十分强悍。使用起来有一点像是缓存。这里不再是生成一个json文件去读取了,读取table中数据给前端的方法,注意要use cache这个类,这里直接使用get就可以根据key获取到table中的数据,注意这个是一个单例模式,因此需要getInstance

public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    $videoFile = EASYSWOOLE_ROOT."/webroot/video/json/".$catId.".json";

    //  第三套方案 table
    $videoData = Cache::getInstance()->get("index_video_data_cat_id".$catId);
    $videoData = !empty($videoData) ? $videoData : [];

    $count = count($videoData);;
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}

设置直接set(key,data)就可以了,注意这里的代码不是很严谨,比如要判断一下是不是设置成功了,没有设置成功发短信之类的,也要处理一下空值的场景,这里只是做一个演示。

<?php

namespace App\Lib\Cache;

use App\Model\Video as VideoModel;
use EasySwoole\Config;
use EasySwoole\Core\Component\Cache\Cache;

class Video
{
    public function setIndexVideo()
    {
        $catIds = array_keys(Config::getInstance()->getConf("category"));
        array_unshift($catIds, 0);

        $modelObj = new VideoModel();

        foreach ($catIds as $catId) {
            $condition = [];
            if (!empty($catId)) {
                $condition['cat_id'] = $catId;
            }

            try {
                $data = $modelObj->getVideoCacheData($condition);
            } catch (\Exception $e) {
                // 短信报警
                $data = [];
            }
            if (empty($data)) {

            }
            foreach ($data as &$list) {
                $list['create_time'] = date("Ymd H:i:s", $list["create_time"]);
                $list["video_duration"] = gmstrftime("%H:%M:%s", $list["video_duration"]);
            }
            // 由于table存在内存所以重启服务器时数据会丢失,要将配置中的PERSISTENT_TIME设置为1进行落盘操作
            Cache::getInstance()->set("index_video_data_cat_id".$catId, $data);
        }
    }
}

注意,一定要去config里面开启

'PERSISTENT_TIME'=>1//如果需要定时数据落地,请设置对应的时间周期,单位为秒

否则会在重启服务的时候没有数据,因为每次启动的时候swoole table会清空,要等到定时去set table的时候才会有数据,因此要开启数据落盘,这样会生成两个文件:

每次启动的时候由于还没有执行定时任务,就会先读取这两个落盘的文件中的数据,防止服务启动时等待table生成造成业务中断。

方案四 Redis

用redis来做数据缓存,每次从缓存里面度读先重写一下set方法,更加严谨一点

/**
 * 重写set方法 处理一下失效时间以及数组转json
 * @param $key
 * @param $value
 * @param $time
 * @return bool|string
 */
public function set($key, $value, $time){
    if(empty($key)){
        return '';
    }
    if(is_array($value)){
        $value = json_encode($value);
    }
    if(!$time){
        return $this->redis->set($key,$value);
    }
    return $this->redis->setex($key, $time, $value);
}

使用起来很简单啦,在之前的代码中

Di::getInstance()->get("REDIS")->set("index_video_data_cat_id".$catId, $data);

然后取出的数据的部分

public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    //redis
    $videoData = Di::getInstance()->get("REDIS")->get("index_video_data_cat_id".$catId);
    $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
    $count = count($videoData);
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}

高度封装

只需要在配置文件中进行配置即可选择相应方法。设置静态化API和获取静态化API的方法

<?php


namespace App\Lib\Cache;

use App\Model\Video as VideoModel;
use EasySwoole\Config;
use EasySwoole\Core\Component\Cache\Cache;
use EasySwoole\Core\Component\Di;
use EasySwoole\Core\Http\Message\Status;

class Video
{
    /**
     * 设置静态API的方法
     * @throws \Exception
     */
    public function setIndexVideo()
    {
        $catIds = array_keys(Config::getInstance()->getConf("category"));
        array_unshift($catIds, 0);

        // 获取配置
        try {
            $cacheType = Config::getInstance()->getConf("base.indexCacheType");
        } catch (\Exception $e) {
            return $this->writeJson(Status::CODE_BAD_REQUEST,"请求失败");
        }

        $modelObj = new VideoModel();

        foreach ($catIds as $catId) {
            $condition = [];
            if (!empty($catId)) {
                $condition['cat_id'] = $catId;
            }

            try {
                $data = $modelObj->getVideoCacheData($condition);
            } catch (\Exception $e) {
                // 短信报警
                $data = [];
            }
            if (empty($data)) {

            }

            foreach ($data as &$list) {
                $list['create_time'] = date("Ymd H:i:s", $list["create_time"]);
                $list["video_duration"] = gmstrftime("%H:%M:%s", $list["video_duration"]);
            }

            switch ($cacheType) {
                case 'file':
                    $res = file_put_contents($this->getVideoCatIdFile($catId), json_encode($data));
                    break;
                case 'table':
                    $res = Cache::getInstance()->set($this->getCatKey($catId), $data);
                    break;
                case 'redis':
                    $res = Di::getInstance()->get("REDIS")->set($this->getCatKey($catId), $data);
                    break;
                default:
                    throw new \Exception("请求不合法");
                    break;
            }

            if(empty($res)){
            //    记录日志
            //    报警

            }

        }

    }

    /**
     * 获取数据的方法
     * @param $catId
     * @return array|bool|mixed|null|string
     * @throws \Exception
     */
    public function getCache($catId){
        $cacheType = Config::getInstance()->getConf("base.indexCacheType");
        switch ($cacheType){
            case 'file':
                $videoFile = $this->getVideoCatIdFile($catId);
                $videoData = is_file($videoFile) ? file_get_contents($videoFile) : [];
                $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
                break;
            case 'table':
                $videoData = Cache::getInstance()->get($this->getCatKey($catId));
                $videoData = !empty($videoData) ? $videoData : [];
                break;
            case 'redis':
                $videoData = Di::getInstance()->get("REDIS")->get($this->getCatKey($catId));
                $videoData = !empty($videoData) ? json_decode($videoData,true) : [];
                break;
            default:
                throw new \Exception("请求不合法");
                break;
        }
        return $videoData;
    }

    public function getVideoCatIdFile($catId = 0){
        return EASYSWOOLE_ROOT . "/webroot/video/json/" . $catId . ".json";
    }


    public function getCatKey($catId = 0){
        return "index_video_data_cat_id".$catId;
    }
}

只需修改配置文件

<?php

return [
    "indexCacheType" => "redis" // redis file table
];

控制器获取数据给前端

public function lists(){
    $catId = !empty($this->params['cat_id']) ? intval($this->params['cat_id']) : 0;
    $videoData = (new VideoCache())->getCache($catId);
    $count = count($videoData);
    return $this->writeJson(Status::CODE_OK,'OK',$this->getPagingData($count,$videoData));
}
]]>
https://www.askme-121.space/static-api/feed/ 0
异步定时多任务消息推送,用php怎么做 ? https://www.askme-121.space/message-push/ https://www.askme-121.space/message-push/#respond Sun, 21 Jan 2024 07:52:01 +0000 https://www.askme-121.pw/?p=501 要实现异步定时多任务消息推送,可以使用多种技术来实现,如Redis、RabbitMQ、Beanstalkd等消息队列服务。

在设计数据表时,可以考虑创建一个 tasks 表,用于存储所有需要推送的任务信息。该表可以包含以下字段:
id:任务ID,自增长整数类型;
name:任务名称,用于描述该任务的作用;
type:任务类型,用于区分不同类型的任务;
data:任务数据,可以存储该任务需要用到的数据信息,可以是序列化后的字符串或JSON格式的数据;
status:任务状态,用于标识该任务的状态,如待处理、已处理等;
created_at:任务创建时间,用于记录该任务的创建时间;
updated_at:任务更新时间,用于记录该任务的最后更新时间;

在添加任务时,可以向 tasks 表中插入一条记录,设置好任务的名称、类型、数据等信息。然后,在需要推送任务时,可以通过查询 tasks 表,找到需要推送的任务信息,并将其加入到消息队列中,等待消息队列服务处理。

在处理任务时,可以使用类似下面的代码:

$queue = new RedisQueue('my_queue', $redis);

// 处理队列中的任务
while ($job = $queue->pop()) {
    $task = unserialize($job->payload);

    // 执行任务处理操作
    handleTask($task);

    // 标记任务为已处理
    $task->status = Task::STATUS_COMPLETED;
    $task->save();
}

其中,RedisQueue 是一个自定义的 Redis 队列类,用于将任务加入到 Redis 队列中。handleTask 函数则是用于处理任务的函数,根据实际需求进行编写即可。

可以使用 cron 或类似的定时任务工具来定时检查 tasks 表,并将需要推送的任务加入到消息队列中。具体的实现方式可以根据实际情况选择不同的技术方案和工具。

需要注意的是,在异步任务处理过程中,需要考虑异常情况的处理,例如任务执行失败、队列服务异常等情况。因此,在编写异步任务时,需要进行充分的异常处理和错误日志记录。

下面介绍几个具体的例子:

1、 使用 Laravel 框架提供的队列和计划任务功能实现消息推送:

// 定义消息推送任务类
class PushMessage implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $message;

    public function __construct($message)
    {
        $this->message = $message;
    }

    public function handle()
    {
        // 处理推送消息的逻辑
    }
}

// 在计划任务中添加消息推送任务
$schedule->call(function () {
    PushMessage::dispatch('hello')->delay(now()->addMinutes(10));
})->everyMinute();

在上面的示例代码中,我们使用 Laravel 框架提供的队列和计划任务功能,定义了一个 PushMessage 消息推送任务类,并在计划任务中添加了一个延迟 10 分钟执行的消息推送任务。

2、 使用第三方的消息队列服务实现消息推送:

// 使用 beanstalkd 消息队列
use Pheanstalk\Pheanstalk;

// 连接到 beanstalkd 服务
$pheanstalk = new Pheanstalk('127.0.0.1');

// 向队列添加消息
$pheanstalk->useTube('push_message')->put('hello', Pheanstalk::DEFAULT_PRIORITY, 60);

// 处理消息队列中的消息
while (true) {
    $job = $pheanstalk->watch('push_message')->ignore('default')->reserve();
    $message = $job->getData();
    // 处理推送消息的逻辑
    $pheanstalk->delete($job);
}

在上面的示例代码中,我们使用了第三方的消息队列服务 beanstalkd,并实现了向队列添加消息和处理消息队列中的消息的逻辑。

3、 使用 Swoole 扩展实现异步定时任务和消息推送:

// 创建 Swoole 定时器和 HTTP 服务器
$timer = swoole_timer_tick(1000, function () {
    // 处理定时任务的逻辑
});
$http = new swoole_http_server('127.0.0.1', 9501);
$http->on('request', function ($request, $response) use ($timer) {
    // 处理 HTTP 请求的逻辑
    $response->end('hello');

    // 添加异步任务
    swoole_timer_after(10000, function () use ($response) {
        $response->end('world');
    });

    // 关闭定时器
    swoole_timer_clear($timer);
});
$http->start();

在上面的示例代码中,我们使用 Swoole 扩展创建了一个定时器和 HTTP 服务器,并在定时器回调函数中处理定时任务的逻辑,在 HTTP 请求回调函数中处理 HTTP 请求的逻辑,并添加了一个 10 秒后执行的异步任务。

]]>
https://www.askme-121.space/message-push/feed/ 0