大主宰小说思路客_异界玄幻小说完本_免费的言情小说阅读网 http://www.cnqigong.com zh_CN 2020-04-02 23:45:33 2020-04-02 23:45:33 YBlog RSS Generator 5 <![CDATA[负载均衡,你该如何配置?]]> http://www.cnqigong.com/post/7213/


盛京棋牌


在计算机的世界,这就是大家耳熟能详的负载均衡(load balancing),所谓负载均衡,就是说如果一组计算机节点(或者一组进程)提供相同的(同质的)服务,那么对服务的请求就应该均匀的分摊到这些节点上。

 

这里的服务是广义的,可以是简单的计算,也可能是数据的读取或者存储。负载均衡也不是新事物,这种思想在多核CPU时代就有了,只不过在分布式系统中,负载均衡更是无处不在,这是分布式系统的天然特性决定的,分布式就是利用大量计算机节点完成单个计算机无法完成的计算、存储服务,既然有大量计算机节点,那么均衡的调度就非常重要。

 

负载均衡的意义在于,让所有节点以最小的代价、最好的状态对外提供服务,这样系统吞吐量最大,性能更高,对于用户而言请求的时间也更小。而且,负载均衡增强了系统的可靠性,最大化降低了单个节点过载、甚至crash的概率。


不难想象,如果一个系统绝大部分请求都落在同一个节点上,那么这些请求响应时间都很慢,而且万一节点降级或者崩溃,那么所有请求又会转移到下一个节点,造成雪崩。


attachments-2020-04-3Shdx4mc5e85409261435.jpg


 

盛京棋牌


回答可以如下:

在nginx里面配置一个upstream,然后把相关的服务器ip都配置进去。然后采用轮询的方案,然后在nginx里面的配置项里,proxy-pass指向这个upstream,这样就能实现负载均衡。

 

nginx的负载均衡有4种模式:


1)、轮询(默认)

每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

 

2)、weight

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

 

3)、ip_hash

每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。

 

4)、fair , url_hash(第三方)

按后端服务器的响应时间来分配请求,响应时间短的优先分配。

 

负载均衡配置方法


打开nginx.conf文件,在http节点下添加upstream节点:

upstream webname {  
  server 192.168.0.1:8080;  
  server 192.168.0.2:8080;  
}  

其中webname是自己取的名字,最后会通过这个名字在url里访问的,像上面这个例子一样什么都不加就是默认的轮询,第一个请求过来访问第一个server,第二个请求来访问第二个server。依次轮着来。

upstream webname {  
  server 192.168.0.1:8080 weight 2;  
  server 192.168.0.2:8080 weight 1;  
}  

这个weight也很好理解,权重大的被访问的概率就大,上面这个例子的话,访问2次server1,访问一次server2

upstream webname {  
  ip_hash;  
  server 192.168.0.1:8080;  
  server 192.168.0.2:8080;  
}   

ip_hash的配置也很简单,直接加一行就可以了,这样只要是同一个ip过来的都会到同一台server上。然后在server节点下进行配置:

location /name {  
    proxy_pass http://webname/name/;  
    proxy_http_version 1.1;  
    proxy_set_header Upgrade $http_upgrade;  
    proxy_set_header Connection "upgrade";  
}  

proxy_pass里面用上面配的webname代替了原来的ip地址。

这样就基本完成了负载均衡的配置。

 

下面是主备的配置:

还是在upstream里面

upstream webname {  
  server 192.168.0.1:8080;  
  server 192.168.0.2:8080 backup;  
} 

设置某一个节点为backup,那么一般情况下所有请求都访问server1,当server1挂掉或者忙的的时候才会访问server2

upstream webname {  
  server 192.168.0.1:8080;  
  server 192.168.0.2:8080 down;  
}  

设置某个节点为down,那么这个server不参与负载。

 

实现实例


1 测试环境

由于没有服务器,所以本次测试直接host指定域名,然后在VMware里安装了三台CentOS。

 

  • 测试域名 :http://a.com

  • A服务器IP :192.168.5.149 (主)

  • B服务器IP :192.168.5.27

  • C服务器IP :192.168.5.126

 

2 部署思路

A服务器做为主服务器,域名直接解析到A服务器(192.168.5.149)上,由A服务器负载均衡到B服务器(192.168.5.27)与C服务器(192.168.5.126)上。

 

3 域名解析

由于不是真实环境,域名就随便使用一个http://a.com用作测试,所以http://a.com的解析只能在hosts文件设置。

打开:C:Windows\System32\drivers\etc\hosts

在末尾添加

192.168.5.149    a.com 

保存退出,然后启动命令模式ping下看看是否已设置成功

 

A、服务器nginx.conf设置

打开nginx.conf,文件位置在nginx安装目录的conf目录下。

在http段加入以下代码

upstream a.com { 
      server  192.168.5.126:80; 
      server  192.168.5.27:80; 
} 

server{ 
    listen 80; 
    server_name a.com; 
    location / { 
        proxy_pass        http://a.com; 
        proxy_set_header  Host            $host; 
        proxy_set_header  X-Real-IP        $remote_addr; 
        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for; 
    } 
}

保存重启nginx

 

B、C服务器nginx.conf设置

打开nginx.conf,在http段加入以下代码

server{ 
    listen 80; 
    server_name a.com; 
    index index.html; 
    root /data0/htdocs/www; 
}

保存重启nginx

 

测试

当访问a.com的时候,为了区分是转向哪台服务器处理我分别在B、C服务器下写一个不同内容的index.html文件,以作区分。

打开浏览器访问a.com结果,刷新会发现所有的请求,均分别被主服务器(192.168.5.149)分配到B服务器(192.168.5.27)与C服务器(192.168.5.126)上,实现了负载均衡效果。


attachments-2020-04-QcZ1kbAh5e85431571c38.jpg


]]>
2020-04-02
<![CDATA[PHP中Session ID的实现原理]]> http://www.cnqigong.com/post/7212/ attachments-2020-04-cQME5ZjP5e85a2629b3b9.jpg



盛京棋牌


为每个访问者创建一个唯一的 id (UID),并基于这个 UID 来存储变量。UID 存储在 cookie 中,亦或通过 URL 进行传导。


盛京棋牌


hash_func = md5 / sha1 #可由php.ini配置

PHPSESSIONID = hash_func(客户端IP + 当前时间(秒)+ 当前时间(微妙)+ PHP自带的随机数生产器)


从以上hash_func(*)中的数据采样值的内容分析,多个用户在同一台服务器时所生产的PHPSESSIONID重复的概率极低(至少为百万份之一),设想,但台动态Web Server能到2000/rps已经很强悍了。


另外,黑客如果要猜出某一用户的PHPSESSIONID,则他也必须知道“客户端IP、当前时间(秒、微妙)、随机数”等数据方可模拟。


php.ini配置如下:


; Select a hash function for use in generating session ids.
; Possible Values
;   0  (MD5 128 bits)
;   1  (SHA-1 160 bits)
; This option may also be set to the name of any hash function supported by
; the hash extension. A list of available hashes is returned by the hash_algos()
; function.
; http://php.net/session.hash-function
session.hash_function=0


以下以cookie传输PHPSESSID描述


1. 客户端请求一个php的服务端地址。


2. 服务端收到请求,此次php脚本中包含session_start()


3. 服务端会生成一个PHPSESSID。

(默认session存储方式为session.save_handler=files,文件形式存储。

生成的session文件名规则即为sess_PHPSESSID,session文件存在session.save_path中。)


4. 服务端响应首部Response Headers:

Set-Cookie:PHPSESSID=37vjjasgjdv2ouk1uomhgqkv50; path=/。

在客户端生成一个cookie保存此PHPSESSID。


5. 此时,客户端的cookie里面包含了PHPSESSID,之后客户端的每次请求首部Request HeadersCookie:PHPSESSID=37vjjasgjdv2ouk1uomhgqkv50。

服务端之后每次接收到客户端的请求就都能根据这个PHPSESSID来找到服务端的session文件,通过对这个session文件的读写操作即实现了session的超全局变量属性。

如果客户端禁用了cookie,由于无法使用cookie传递PHPSESSID,那么客户端每次请求,服务端都会重新建立一个session文件,而无法通过通过PHPSESSID来重用session文件,所以session也就失效了。

这种情况可以设置session.use_trans_sid来传输PHPSESSID,具体实现方式与cookie的区别就是将PHPSESSID通过HTTP的GET传输。每次请求的地址里面都会补全PHPSESSID参数”urlPHPSESSID=37vjjasgjdv2ouk1uomhgqkv50”来实现。


【PHPcli模式通过session_id()使用session】


可以通过它来获取当前会话的PHPSESSID,也可以通过它来设置当前的会话PHPSESSID。

PHPcli模式下可以通过设置这个,达到使用session的目的,非常方便。


例如:

<?php
// session_id('vingbrv8m64asth0nhplu9gmb7');
session_start();
$_SESSION[md5(rand(100,999))] = rand(100,999);
var_dump($_SESSION);
]]>
2020-04-02
<![CDATA[PHP消息队列实现及应用讲述]]> http://www.cnqigong.com/post/7211/

attachments-2020-04-kTQZDjwu5e85a43b6fdf6.jpg


盛京棋牌


1.1 消息对列概念

  从本质上说消息对列就是一个队列结构的中间件,也就是说消息放入这个中间件之后就可以直接返回,并不需要系统立即处理,而另外会有一个程序读取这些数据,并按顺序进行逐次处理。

  也就是说当你遇到一个并发特别大并且耗时特别长同时还不需要立即返回处理结果,使用消息队列可以解决这类问题。


1.2 核心结构

v2-3cc18fd891dc8911c67885add75b41e8_720w.jpg

由一个业务系统进行入队,把消息逐次插入到消息队列中,插入成功之后直接返回成功的结果,后续会有一个消息处理系统,这个系统会把消息系统中的记录逐次进行取出并进行处理,完成一个出队的流程。


盛京棋牌

  数据冗余:比如订单系统,后续需要严格的进行数据转换和记录,消息队列可以把这些数据持久化的存储在队列中,然后有订单,后续处理程序进行获取,后续处理完之后在把这条记录进行删除来保证每一条记录都能够处理完成。

  系统解耦:使用消息系统之后,入队系统和出队系统是分开的,也就说只要一天崩溃了,不会影响另外一台系统正常运转。

  流量削峰:例如秒杀和抢购,我们可以配合缓存来使用消息队列,能够有效的顶住瞬间访问量,防止服务器承受不住导致崩溃。

  异步通信:消息本身使用入队之后可以直接返回。

  扩展性:例如订单队列,不仅可以处理订单,还可以给其他业务使用。

  排序保证:有些场景需要按照产品的顺序进行处理比如单进单出从而保证数据按照一定的顺序处理,使用消息队列是可以的。

以上都是消息队列常见的使用场景,当然消息队列只是一个中间件,可以配合其他产品进行使用。


1.4 常见队列实现优缺点

  队列介质

    1、数据库,例如mysql(可靠性高,易实现,速度慢)

    2、缓存, 例如redis (速度快,单个消息报包过大时效率低)

    3、消息系统,例如rabbitMq (专业性强,可靠,学习成本高)

  消息处理触发机制

    1、死循环方式读?。阂资迪?,故障时无法及时恢复;(比较适合做秒杀,比较集中,运维集中维护)

    2、定时任务:压力均分,有处理上限;目前比较流行的处理触发机制。(唯一的缺点是间隔和数据需要注意,不要等上一个任务没有完成下一个任务又开始了)

    3、守护进程:类似于php-fpm 和php-cg,需要shell基础


二、解藕案例:队列处理“订单系统”和“配送系统”

  简单说一下程序解耦:程序解耦就是避免出现你老婆和你妈同时掉到水里先去救谁的问题

  对于订单流程,我们可以设计两个系统,一个是“订单系统” 另外一个是 “配送系统”, 在网购的时候我们应该都见过,当我提交了一个订单之后,我在后台可以看到我的货物正在配送中。这个时候就要参与进来一个“配送系统”。

  如果我们在做架构的时候把 “订单系统” 和 “配送系统” 设计在一起的话就会出现一些问题,首先对于订单系统来说,因为系统的压力会比较大,但是 "配送系统" 没必要为这些压力做一些即时的反应。

  第二个我们也不希望在订单系统出现故障之后导致配送系统也出现故障,这个时候就会同时影响到两个系统的正常运转。所以我们希望把这两个系统进行解耦。这两系统分开之后我们可以通过一个中间的 “队列表” 进行这两个系统的沟通。


2.1 架构设计


v2-f4b41fc300d4d726d0ef7249e0fd167c_720w.jpg

  1、首先订单系统会接收用户的订单,然后进行订单的处理。

  2、然后会把这些订单信息写到队列表中,这个队列表是沟通这两个系统的关键。

  3、由配送系统定时执行的一个程序来读取队列表进行处理。

  4、配送系统处理之后,会把已处理的记录进行标记。


2.2 程序流程

v2-7fc21f190c53bd947a3245c1efe53e2e_720w.jpg


三、流量削峰案例:Redis 的 list 类型实现秒杀

  redis 基于内存,它的速度会非???,redis 对数据库有一个非常好的补充作用因为它是可持久化的,redis会周期性的把数据写到硬盘里,所以它不用担心断电的问题,从这方面说它比另一款缓存 memcache 更有优势些,另外 redis 提供五种数据类型(字符串,双向链表,哈希,集合,有序集合)

  一般情况下,做秒杀案例,抢购,瞬间高并发,需要排队 的案例中 redis是一个很好的选择。


3.1 redis数据类型中的 list 类型

  redis 的list 是一个双向链表,可以从头部或者尾部追加数据。

  * LPUSH/LPUSHX :将值插入到(/存在的)列表头部

  * RPUSH/RPUSHX: 将值插入到(/存在的)列表尾部

  * LPOP : 移除并获取列表的第一个元素

  * RPOP: 移除并获取列表的最后一个元素

  * LTRIM: 保留指定区间内的元素

  * LLEN: 获取列表长度

  * LSET: 通过索引设置列表元素的值

  * LINDEX: 通过索引获取列表中的元素

  * LRANGE: 获取列表指定范围内的元素


3.2 架构设计

  一个简单结构秒杀的程序设计。


v2-1f2cd37d3de4ba88d10dca7deab0dad8_720w.jpg

  1、首先记录是哪一个用户参与了秒杀同时记录他的时间。

  2、将用户的id存到redis列表中,让它排队。如果规定只有前10个用户可以参与成功,如果列表中的个数已经够了就不会让它继续追加数据。这样redis的列表长度就只会是10个

  3、最后在慢慢的将redis中的数据写入到数据库中,以减少数据的压力


3.3 代码级设计

  1、当用户开始秒杀时,将秒杀程序的请求写入Redis (uid, time_stamp)中。

  2、假使规定只有10人可以秒杀成功,检查 Redis 已经存放数据的长度,超出上限直接丢弃说明秒杀完成。

  3、最后在死循环处理存入Redis中的10条数据,然后在慢慢的取数据并存入到mysql数据库中。

在秒杀这一块对于数据库的压力特别的大,如果我们没有这样的设计,会造成mysql的写入瓶颈。我们通过Redis的一个对列list,然后把秒杀的请求放入到Redis里面, 最后通过入库程序,把数据慢慢的写入到数据库,这样的话就可以实现流量的均衡,对mysql不会造成太大的压力?!?


四、RabbitMQ

  这里讲解一些RabbitMQ的使用,首先我们之前讲秒杀案例的时候提到了锁的机制,防止其他程序处理同一条记录,如果我们的系统架构非常的复杂,有多个程序实时的读取一个队列,或者我有多个发送程序,同时来操作一个或多个队列,甚至我还想这些程序分布在不同的机器上,这种情况下用redis队列就有些力不从心了。这种时候怎么办呢,我们就需要来引入一些更专业的消息队列系统,这些系统可以更好的来解决问题。


4.1 RabbitMQ的架构和原理

v2-6c7c4cd4eb61a2cda91f5a047af730e1_720w.jpg

特点:完整的实现了AMQP,集群简化,持久化,跨平台


  RabbitMQS使用

    1、RabbitMQ安装 (rabbitmq-server, php-amqplib)

    2、生产者向消息通道发送消息

    3、消费者处理消息


  工作队列

v2-71fff4fd96186587b22489ab65703e8a_720w.jpg

思想:

由生产者发送给消息系统,消息系统把任务封装成消息队列之后,然后供多个消费者使用同一个队列。这不仅解决了生产者和消费者之间的解耦,还可以实现了消费者和任务的共享,减缓了服务器的压力。

]]>
2020-04-02
<![CDATA[nginx与php-fpm通信的两种方式]]> http://www.cnqigong.com/post/7210/

attachments-2020-04-VNlSNKcX5e85a555c2890.jpg

盛京棋牌


在linux中,nginx服务器和php-fpm可以通过tcp socket和unix socket两种方式实现。

unix socket是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。这种方式需要再nginx配置文件中填写php-fpm的pid文件位置,效率要比tcp socket高。

tcp socket的优点是可以跨服务器,当nginx和php-fpm不在同一台机器上时,只能使用这种方式。

windows系统只能使用tcp socket的通信方式


tcp socket:tcp socket通信方式,需要在nginx配置文件中填写php-fpm运行的ip地址和端口号。

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
}


unix socket:unix socket通信方式,需要在nginx配置文件中填写php-fpm运行的pid文件地址。

//service php-fpm start生成.sock文件
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;;
    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
}


php-fpm的运行端口号和socket文件的地址都是在php-fpm.conf中配置的。

php-fpm.conf文件在php安装文件的/etc目录下,比如你的php安装在/opt/php目录,则应该是/opt/php/php-fpm.conf。

; The address on which to accept FastCGI requests.
; Valid syntaxes are:
;   'ip.add.re.ss:port'    - to listen on a TCP socket to a specific IPv4 address on
;                            a specific port;
;   '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
;                            a specific port;
;   'port'                 - to listen on a TCP socket to all IPv4 addresses on a
;                            specific port;
;   '[::]:port'            - to listen on a TCP socket to all addresses
;                            (IPv6 and IPv4-mapped) on a specific port;
;   '/path/to/unix/socket' - to listen on a unix socket.
; Note: This value is mandatory.
listen = 127.0.0.1:9000
listen = /var/run/php-fpm.sock


通过注释可以看到,php-fpm的listen指令可以通过五种方式处理FastCGI请求,分别是:

ipv4:端口号

ipv6:端口号

port相当于 0.0.0.0:port,本机所有ipv4对应的端口号

unix socket文件


直接配置使用unix socket文件之后,会遇到access deny的问题,由于socket文件本质上还是一个文件,存在权限控制问题,默认由root用户创建,因此nginx进程无权限访问,应该配置如下命令:

; Set permissions for unix socket, if one is used. In Linux, read/write
; permissions must be set in order to allow connections from a web server. Many
; BSD-derived systems allow connections regardless of permissions.
; Default Values: user and group are set as the running user
;                 mode is set to 0660
listen.owner = www
listen.group = www 
listen.mode = 0660


可以配置nginx和php-fpm都是用www用户,这样就不会存在权限问题,当然也可以创建不同的用户,然后加入同一个组,便于分配权限。

]]>
2020-04-02
<![CDATA[复习下Linux去除重复项命令uniq]]> http://www.cnqigong.com/post/7209/

attachments-2020-04-vMUJjTBU5e85a6d516762.png


盛京棋牌


在介绍uniq命令之前,我们先来新建在下面的案例中需要用到的文件/tmp/uniq.txt,内容如下:

默认情况下uniq只会检索相邻的重复数据从而去重。在/tmp/uniq.txt中虽然“onmpw web site” 有三条,但是其中一条是和其他两条不相邻的,所以只去重了一条,同理“error php function”也是这种情况。


盛京棋牌

# sort 1.txt | uniq

alpha css web
cat linux command
error php function
hello world
onmpw web site
recruise page site
repeat no data
wello web site

现在再看是不是所有的重复项都已经经过去重处理了。


好了,小试牛刀一把以后,下面我们开始对uniq命令的选项进行简单的介绍。

-c 统计每一行数据的重复次数

sort 1.txt | uniq -c
alpha css web
cat linux command
error php function
hello world
onmpw web site
recruise page site
repeat no data
wello web site

我们看 “error php function”出现了两次,“onmpw web site”出现了三次。其余的都没有重复项所以为1。


-i 忽略大小写

在1.txt中添加一行数据 “Error PHP function”

cat 1.txt

alpha css web
cat linux command
error php function
hello world
onmpw web site
onmpw web site
wello web site
Error PHP function
recruise page site
error php function
repeat no data
onmpw web site


sort 1.txt | uniq –c
alpha css web
cat linux command
error php function
Error PHP function
hello world
onmpw web site
recruise page site
repeat no data
wello web site

我们看结果,uniq默认是区分大小写的。使用-i可以忽略掉大小写问题

sort 1.txt | uniq –c –i
alpha css web
cat linux command
error php function
hello world
onmpw web site
recruise page site
repeat no data
wello web site

现在再看是不是大小写已经忽略掉了。


-u 只输出没有重复的数据

 sort 1.txt | uniq –iu

alpha css web
cat linux command
hello world
recruise page site
repeat no data
wello web site

看到没,结果中的“error php function”和“onmpw web site”都没有被输出。


-w N 表示从第一个字符开始只检索N个字符来判重。

sort 1.txt | uniq –iw 2

alpha css web
cat linux command
error php function
hello world
onmpw web site
recruise page site
wello web site

这里我们让uniq只对前两个字符进行检索,recruit 和 repeat前两个字符都是re,所以这两行也被认为是重复的。


-f N 表示略过前面N个字段,从第N+1个字段开始检索重复数据。以空格符或者tab键为分隔符。

sort 1.txt | uniq –icf 2
alpha css web
cat linux command
error php function
hello world
onmpw web site
repeat no data
wello web site

我们在结果中可以看到,这是略过前面的2个字段,从第三个字段开始判重的。

“recruise page site” 和 “onmpw web site”的第三个字段相同,所以被认为是相同的数据。

但是我们看到,“wello web site”和“onmpw web site”不但第三个字段相同,第二个也相同。

那为什么它不被计入“onmpw web site”的重复数据中呢。

对于这个问题就要回到前面说的,uniq只检测相邻的数据是否是重复的。

要解决这个问题还需要在sort命令上着手?;辜堑胹ort命令的-k选项吗,没错,我们就用它来解决。

sort –k 2 1.txt | uniq –icf 2
alpha css web
cat linux command
repeat no data
recruise page site
error php function
onmpw web site
hello world

我们看,是不是解决了。


-s N表示略过前面N个字符,关于这个选项的例子我们这里就不再举了,该选项和-f N的用法差不多。只不过-f N是略过前面N个字段;-s是略过前面N个字符。


-d 只输出有重复项的第一条的数据。

sort 1.txt | uniq -idw 2

repeat no data
error php function
onmpw web site

结果只有这三条。为什么会有“repeat no data”这条数据,这里注意-w 2的应用。


-D 对于重复项全部输出

sort 1.txt | uniq –iDw 2

repeat no data
recruise page site
error php function
error php function
Error PHP function
onmpw web site
onmpw web site
onmpw web site

好了,关于uniq的选项的所有常用的命令已经都介绍完了。

关于uniq更详细的信息可以使用命令info uniq。


]]>
2020-04-02
<![CDATA[php SESSION入库的实现]]> http://www.cnqigong.com/post/7208/ attachments-2020-04-Q63B3leq5e85a7a7ee89f.jpg


盛京棋牌在session的周期内,获得到session的数据并记录到数据库

Session默认是存放到服务器上的文件中,不方便管理,如果能把session存放到数据库中就可以方便的对数据库进行管理了


盛京棋牌

1. 可以解决跨域操作

2. 可以实现单点登陆

3. 可以统计在线人数

4. 可以实现同一时只允许一个用户在线


session_set_save_handler的回掉函数描述


实现session入库


第一步:在php.ini配置文件中设置session.save_headler=user(默认是file)

或者使用ini_set设置ini_set(‘session.save_handler’,’user’);


第二步:创建一个存放session的数据表

session_id用于存放session_id的,字段类型为字符型,长度为32

session_value用于存放session的内容,字段类型为textsession_life 用于存放session的生存期


第三步:session_set_save_handler ( callback $open , callback $close ,callback $read , callback $write , callback $destroy , callback $gc )


 第四步:代码实现

<?php
header('content-type:text/html;charset=utf-8');

//将session存储方式设置为存入数据库的方式
//session.save_handler = files
ini_set("session.save_handler", "user");

session_set_save_handler("open", "close", "read", "write", "destroy", "gc");

//打开并连接数据库
function open()
{
    //使用pdo
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=yii','root','root');
    $pdo->exec('set names utf8');
}

//关闭连接
function close()
{
    //使用pdo
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=yii','root','root');
    $pdo->exec('set names utf8');
    $pdo = null;
}

//从表中中读信息
function read($session_id)
{
    //使用pdo
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=yii','root','root');
    $pdo->exec('set names utf8');
    $read = $pdo->query("select * from session where session_id='$session_id'")->fetch(PDO::FETCH_ASSOC);
    return $read["session_info"];
}

//将session存入数据库
function write ($session_id,$session_info)
{
    // echo $session_id. "        " . $session_info;
    //使用pdo
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=yii','root','root');
    $pdo->exec('set names utf8');
    $read = $pdo->query("select * from session where session_id='$session_id'")->fetch(PDO::FETCH_ASSOC);

    if(empty($read))
    {
        $time = time();
        $pdo->exec("insert into session (session_id, session_info, session_life) values ('$session_id', '$session_info', '$time')");
    }
    else
    {
        $sql = "update session set session_info='$session_info' where session_id='$session_id'";
        $pdo->exec($sql);
    }
}

//销毁指定session
function destroy($session_id)
{
    //使用pdo
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=yii','root','root');
    $pdo->exec('set names utf8');
    $pdo->exec("delete from session where session_id='$session_id'");
}

//删除所有过期的session
function gc()
{

}

session_start();
//判断是否有session
if(isset($_SESSION['name']))
{

    $status = 1;
}
else
{
    $status = 0;

}

//获取需要接收的值
$user_name = isset($_POST['user_name'])?$_POST['user_name']:null;
$user_pwd = isset($_POST['user_pwd'])?$_POST['user_pwd']:null;
//使用pdo
$pdo = new PDO('mysql:host=127.0.0.1;dbname=yii','root','root');
$pdo->exec('set names utf8');
$sql = "select * from user where username = '$user_name' and password = '$user_pwd' ";
$re = $pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
if($re)
{
    $status = 1;
    $_SESSION['name'] = $user_name;
}

?>


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<?php if($status == 0){?>
    <h3>服务器3</h3>
    <form action="a.php" method="post">
        <table>
            <tr>
                <td>姓名:</td>
                <td><input type="text" name="user_name"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="user_pwd"></td>
            </tr>
            <tr>
                <td></td>
                <td><input type="submit" value="登录"></td>
            </tr>
        </table>
    </form>
<?php } else{?>
    <h2>欢迎<?=$_SESSION['name']?>登录</h2>
    <h3>服务器3</h3>
<?php }
?>
</body>
</html>
]]>
2020-04-02
<![CDATA[socket编程之websocket实现]]> http://www.cnqigong.com/post/7207/


attachments-2020-04-2PcOu8lR5e844359c4044.png



盛京棋牌我们需要了解一下websocket。

websocket是一种可以双向通讯的长连接协议,http是获取完数据就关闭,websocket则可以一直连接,就像铺了一条管道一样,水可以一直流着。


盛京棋牌


var ws = new WebSocket("ws://127.0.0.1.com:8282");
    ws.onopen=function(){
        var msg = JSON.stringify({
            type: "login",
            content: "login"
        });
        ws.send(msg);
    }
    
    ws.onmessage = function (e){ 
        console.log(e);
        //服务器发送的内容
        var res = JSON.parse(e.data);
        switch(res.type){
            case "login":
                
                break;
            case "pm":
                
                break;
            case "groupPm":
                
                break;
                
        }
    }
    ws.onerror=function (e){ 
        console.log(e);
    }
    ws.onclose=function (e){ 
        console.log(e);
    }


二、服务端


v2-1f14bd1d8a477326466c9820e60241cb_720w.jpg


客户端发送http请求,带上Sec-WebSocket-Key,服务端握手 加密key,发送给客户端。双方能进行交流。

发送接收消息需要进行打包encode 解包decode。


<?php

class SocketService
{
    public $host="tcp://0.0.0.0:8000";
    private $address;
    private $port;
    private $_sockets;
    public $clients;
    public $maxid=1000;
    public function __construct($address = '', $port='')
    {
            if(!empty($address)){
                $this->address = $address;
            }
            if(!empty($port)) {
                $this->port = $port;
            }
    }
    
    public function onConnect($client_id){
        echo  "Client client_id:{$client_id}   \n";
         
    }
    
    public function onMessage($client_id,$msg){
        //发给所有的
        foreach($this->clients as $kk=>$cc){
            if($kk>0){
                $this->send($cc, $msg);
            }                                
        }    
    }
    
    public function onClose($client_id){
        echo "$client_id close \n";
    }
    
    public function service(){
        //获取tcp协议号码。
        $tcp = getprotobyname("tcp");
        $sock = stream_socket_server($this->host, $errno, $errstr);;
        
        if(!$sock)
        {
            throw new Exception("failed to create socket: ".socket_strerror($sock)."\n");
        }
        stream_set_blocking($sock,0);
        $this->_sockets = $sock;
         echo "listen on $this->address $this->host ... \n";
    }
 
    public function run(){
        $this->service();
        $this->clients[] = $this->_sockets;
        while (true){
            $changes = $this->clients;
            //$write = NULL;
            //$except = NULL;
            stream_select($changes,  $write,  $except, NULL);
            foreach ($changes as $key => $_sock){
                if($this->_sockets == $_sock){ //判断是不是新接入的socket
                    if(($newClient = stream_socket_accept($_sock))  === false){
                        unset($this->clients[$key]);
                        continue;
                    }
                    $line = trim(stream_socket_recvfrom($newClient, 1024));
                    //握手
                    $this->handshaking($newClient, $line);
                    $this->maxid++;
                    $this->clients[$this->maxid] = $newClient;
                    $this->onConnect($this->maxid);
                } else {
                    $res=@stream_socket_recvfrom($_sock,  2048);
                    //客户端主动关闭
                    if(strlen($res) < 9) {
                        stream_socket_shutdown($this->clients[$key],STREAM_SHUT_RDWR);
                        unset($this->clients[$key]);
                        $this->onClose($key);
                    }else{
                        //解密
                        $msg = $this->decode($res);
                        $this->onMessage($key,$msg);
                    }
                     
                    
                }
            }
        }
    }
 
    /**
     * 握手处理
     * @param $newClient socket
     * @return int  接收到的信息
     */
    public function handshaking($newClient, $line){
 
        $headers = array();
        $lines = preg_split("/\r\n/", $line);
        foreach($lines as $line)
        {
            $line = chop($line);
            if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
            {
                $headers[$matches[1]] = $matches[2];
            }
        }
        $secKey = $headers['Sec-WebSocket-Key'];
        $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
        $upgrade  = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
            "Upgrade: websocket\r\n" .
            "Connection: Upgrade\r\n" .
            "WebSocket-Origin: $this->address\r\n" .
            "WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n".
            "Sec-WebSocket-Accept:$secAccept\r\n\r\n";
        return stream_socket_sendto($newClient, $upgrade);
    }
    
    
    
    
    /**
     * 发送数据
     * @param $newClinet 新接入的socket
     * @param $msg   要发送的数据
     * @return int|string
     */
    public function send($newClinet, $msg){
        $msg = $this->encode($msg);
        stream_socket_sendto($newClinet, $msg);
    }
    /**
     * 解析接收数据
     * @param $buffer
     * @return null|string
     */
    public function decode($buffer){
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;
        if ($len === 126)  {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else if ($len === 127)  {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        } else  {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        return $decoded;
    }
 
    
    /**
    *打包消息
    **/
     public function encode($buffer) {
        $first_byte="\x81";
        $len=strlen($buffer);
        if ($len <= 125) {
            $encode_buffer = $first_byte . chr($len) . $buffer;
        } else {
            if ($len <= 65535) {
                $encode_buffer = $first_byte . chr(126) . pack("n", $len) . $buffer;
            } else {
                $encode_buffer = $first_byte . chr(127) . pack("xxxxN", $len) . $buffer;
            }
        }
        return $encode_buffer;
    }
 
    /**
     * 关闭socket
     */
    public function close(){
        return socket_close($this->_sockets);
    }
}
 
$sock = new SocketService('127.0.0.1','9000');
$sock->run();


三、常见应用


1.聊天室、群聊 实现类似QQ群的web版本

http://2.im私聊、客服 实现类似qq聊天,和即时客服交流

3.消息推送 建立即时的web消息推送


前端格式


var msg = JSON.stringify({
type: "login",
content: "login"
});
var msg = JSON.stringify({
type: "group",
content: "login",
gid:123
});

var msg = JSON.stringify({
type: "pm",
content: "login",
uid:123
});



]]>
2020-04-01
<![CDATA[php 实现密码错误三次锁定账号10分钟]]> http://www.cnqigong.com/post/7206/


盛京棋牌


  /**
     * 登录
     * 1、接收数据
     * 2、正则判断接收到的数据是否合理
     * 3、根据用户名获取用户数据
     *      获取到数据 -> 继续执行
     *      没有获取到数据 -> 提示:用户名密码错误
     * 4、判断锁定时间
     *      当前时间和锁定时间差 大于 10分钟 或者 没有锁定时间 -> 继续执行
     *      当前时间和锁定时间差 小于 10分钟 -> 提示:账号锁定中、请10分钟后再试
     * 5、判断密码
     *      == 
     *          次数=0
     *          登录成功
     *      != 
     *          次数 大于等于 2 -> 锁定操作、次数=0  -> 账号已经锁定
     *          次数 小于 2  次数+1 -> 账号密码错误
     */

    public function login()
    {
        $name = request()->post('name', '');
        $pwd = request()->post('pwd', '');
        if ( $name == '' || $pwd == '' || $name == null || $pwd == null) {
            $arr['code'] = 1;
            $arr['msg'] = '参数错误、用户名或密码不能为空';
            $arr['data'] = [];
            return json($arr);
        }

        $preg_name = '/^[\x{4e00}-\x{9fa5}]{2,5}$/u';
        if( !preg_match( $preg_name, $name ) )
        {
            $arr['code'] = 1;
            $arr['msg'] = '用户名要求必须是2到5位的汉字';
            $arr['data'] = [];
            return json($arr);
        }

        $preg_pwd = '/^\S{5,18}$/';
        if (!preg_match($preg_pwd, $pwd)) {
            $arr['code'] = 1;
            $arr['msg'] = '密码要求必须5到18位非空字符串';
            $arr['data'] = [];
            return json($arr);
        }

        $where['user_name'] = $name;
        $res = Db::table('user')->field('user_id,user_name,user_pwd_login,user_lock_time,user_pwd_num')->where($where)->find();
        if (!$res) {
            $arr['code'] = 1;
            $arr['msg'] = '用户名或密码错误、请重试';
            $arr['data'] = [];
            return json($arr);
        }

        if($res['user_lock_time'] != '' && time() - strtotime($res['user_lock_time']) < 1*60 ) 
        {
            $arr['code'] = 1;
            $arr['msg'] = '该账号已被锁定、请10分钟后重试';
            $arr['data'] = [];
            return json($arr);
        }

        $upd_where['user_id'] = $res['user_id'];
        if( $pwd != $res['user_pwd_login'] )
        {
            // 次数 大于等于 2 -> 锁定操作、次数=0 -> 账号已经锁定
            if( $res['user_pwd_num'] >= 2 )
            {
                $upd_data['user_lock_time'] = date('Y-m-d H:i:s', time() );
                $upd_data['user_pwd_num'] = 0;
                Db::table('user')->where($upd_where )->update( $upd_data );
                $arr['code'] = 1;
                $arr['msg'] = '账号密码错误次数超过3次、账号锁定10分钟、请稍后重试';
                $arr['data'] = [];
                return json($arr);
            }
            else
            {
                // 次数 小于2 次数+1 -> 账号密码错误
                Db::table('user')->where($upd_where)->setInc('user_pwd_num');
                $arr['code'] = 1;
                $arr['msg'] = '账号密码错误、剩余'. (3 - ($res['user_pwd_num'] + 1) ) .'次、请稍后重试';
                $arr['data'] = [];
                return json($arr);
            }
        }

        Db::table('user')->where($upd_where)->update(['user_pwd_num'=>0]);
        
        Session::set('user', $res);
        $arr['code'] = 0;
        $arr['msg'] = '登录成功';
        $arr['data'] = $res;
        return json($arr);
    }



]]>
2020-04-01
<![CDATA[Swoole4创建Mysql连接池]]> http://www.cnqigong.com/post/7205/


attachments-2020-04-sa4SBJ4p5e843dd23d973.jpg



盛京棋牌


场景:每秒同时有1000个并发,但是这个mysql同时只能处理400个连接,mysql会宕机。


解决方案:连接池,这个连接池建立了200个和mysql的连接,这1000个并发就有顺序的共享这连接池中的200个连接。这个连接池能够带来额外的性能提升,因为这个和mysql建立连接的这个过程消耗较大,使用连接池只需连接一次mysql。


连接池定义:永不断开,要求我们的这个程序是一个常驻内存的程序。数据库连接池(Connection pooling)是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地对池中的连接进行申请,使用,释放。


盛京棋牌


查找用户表数据库最新注册的3个会员?


(1)小提示

show processlist #mysql查看连接数


(2)创建10个mysql连接示例代码

<?php
/**
 * Created by PhpStorm.
 * User: Luke
 * Date: 2019/10/30
 * Time: 14:12
 */
//编写mysql连接池,这个类只能被实例化一次(单例)
class MysqlConnectionPool
{
    private static $instance;//单例对象
    private $connection_num = 10;//连接数量
    private $connection_obj = [];
 
    //构造方法连接mysql,创建20mysql连接
    private function __construct()
    {
        for($i=0;$i<$this->connection_num;$i++){
            $dsn = "mysql:host=127.0.0.1;dbnane=swoole";
            $this->connection_obj[] =  new Pdo($dsn,'root','rootmysql123');
        }
    }
    private function __clone()
    {
        // TODO: Implement __clone() method.
    }
    public static function getInstance()
    {
        if(is_null(self::$instance)){
            self::$instance = new self();
        }
    }
}
MysqlConnectionPool::getInstance();
//创建swool的http服务器对象
$serv = new swoole_http_server('0.0.0.0',8000);
//当浏览器链接点这个http服务器的时候,向浏览器发送helloworld
$serv->on('request', function($request,$response){
    //$request包含这个请求的所有信息,比如参数
    //$response包含返回给浏览器的所有信息,比如helloworld
 
    //(2.3)向浏览器发送helloworld
    $response->end("hello world");
});
//启动http服务器
$serv->start();


(3)效果

v2-9f0e6d0e2385159a015e2d583b39e8f2_720w.jpg


(4)完善mysql连接池

<?php
/**
 * Created by PhpStorm.
 * User: Luke
 * Date: 2019/10/30
 * Time: 14:12
 */
//编写mysql连接池,这个类只能被实例化一次(单例)
class MysqlConnectionPool
{
    private static $instance;//单例对象
    private $connection_num = 20;//连接数量
    private $connection_obj = [];
    private $avil_connection_num = 20;//可用连接
 
    //构造方法连接mysql,创建20mysql连接
    private function __construct()
    {
        for($i=0;$i<$this->connection_num;$i++){
            $dsn = "mysql:host=127.0.0.1;dbname=swoole";
            $this->connection_obj[] =  new Pdo($dsn,'root','rootmysql123');
        }
    }
    private function __clone()
    {
        // TODO: Implement __clone() method.
    }
    public static function getInstance()
    {
        if(is_null(self::$instance)){
            self::$instance = new self();
        }
        return self::$instance;
    }
 
    //执行sql操作
    public function query($sql)
    {
        if($this->avil_connection_num==0){
            throw new Exception("暂时没有可用的连接诶,请稍后");
        }
        //执行sql语句
        $pdo = array_pop($this->connection_obj);
        //可用连接数减1
        $this->avil_connection_num --;
        //使用从连接池中取出的mysql连接执行查询,并且把数据取成关联数组
        $rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
        //把mysql连接放回连接池,可用连接数+1
        array_push($this->connection_obj,$pdo);
        $this->avil_connection_num ++;
        return $rows;
    }
}
//创建swool的http服务器对象
$serv = new swoole_http_server('0.0.0.0',8000);
//当浏览器链接点这个http服务器的时候,向浏览器发送helloworld
$serv->on('request', function($request,$response){
    //$request包含这个请求的所有信息,比如参数
    //$response包含返回给浏览器的所有信息,比如helloworld
    //向浏览器发送helloworld
    $stop = false;
    while (!$stop){
        try{
            $sql = "SELECT * FROM user ORDER BY id  DESC LIMIT 5";
            $rows = MysqlConnectionPool::getInstance()->query($sql);
            $response->end(json_encode($rows));
            $stop = true;
        }catch (Exception $e){
            usleep(100000);
        }
    }
 
});
//启动http服务器
$serv->start();



]]>
2020-04-01
<![CDATA[为什么要分库分表?]]> http://www.cnqigong.com/post/7204/





盛京棋牌而越来越多的数据存入了数据库中。当使用MySQL数据库的时候,单表超出了2000万数据量就会出现性能上的分水岭。

并且物理服务器的CPU、内存、存储、连接数等资源有限,某个时段大量连接同时执行操作,会导致数据库在处理上遇到性能瓶颈。

为了解决这个问题,行业先驱门充分发扬了分而治之的思想,对大表进行分割,然后实施更好的控制和管理,同时使用多台机器的CPU、内存、存储,提供更好的性能。而分而治之则有两种方式:垂直拆分和水平拆分。


盛京棋牌


垂直拆分分为垂直分库和垂直分表。先说说垂直分库。垂直分库其实是一种简单逻辑分割。比如我们的数据库中有商品表Products、还有对订单表Orders,还有积分表Scores。接下来我们就可以创建三个数据库,一个数据库存放商品,一个数据库存放订单,一个数据库存放积分。如下图所示:

attachments-2020-04-TMv1Lkq85e8440cdf3b94.jpg

垂直分库有一个优点,他能够根据业务场景进行孵化,比如某一单一场景只用到某2-3张表,基本上应用和数据库可以拆分出来做成相应的服务。

再来说说垂直分表,比较适用于那种字段比较多的表,假设我们一张表有100个字段,我们分析了一下当前业务执行的SQL语句,有20个字段是经常使用的,而另外80个字段使用比较少。

这样我们就可以把20个字段放在主表里面,我们在创建一个辅助表,存放另外80个字段。当然主表和辅助表都是有主键的。他们通过主键进行关联合并,就可以凑成100个字段的表。


attachments-2020-04-loyzpLv65e8441024bb29.jpg


垂直分表可以解决跨页的问题。在Oracle中叫行链接。怎么理解呢?就是你字段少的情况下,原本一行数据只需要存在一个页里面就行了,但是字段多的情况就存不下了,就需要跨页。

这样就会造成额外寻址,造成性能上的开销。另外将这么长的一行数据载到内存中,往往是几个页面,结果咱们经常只访问其中的几个字段,对内存也是一个极大的开销。所以为了让内存缓存更多数据,减少磁盘I/O,垂直分表就是很好的手段。


总体来说:垂直拆分有以下优点:


  1. 跟随业务进行分割,和最近流行的微服务概念相似,方便解耦之后的管理及扩展。
  2. 高并发的场景下,垂直拆分使用多台服务器的CPU、I/O、内存能提升性能,同时对单机数据库连接数、一些资源限制也得到了提升。
  3. 能实现冷热数据的分离。


垂直拆分的缺点:


  1. 部分业务表无法join,应用层需要很大的改造,只能通过聚合的方式来实现。增加了开发的难度。
  2. 当单库中的表数据量增大的时候依然没有得到有效的解决。
  3. 分布式事务也是一个难题。


水平拆分


当某张表数据量达到一定的程度的时候,前面曾说过MySQL单表出现2000万以上数据就会出现性能上的分水岭。此时发现没有办法根据业务规则再进行拆分了,就会导致单库上的读写性能出现瓶颈。此时就只能进行水平拆分了。


水平拆分又分为库内分表和分库分表。先说说库内分表。假设当我们的Orders表达到了5000万行记录的时候,非常影响数据库的读写效率,怎么办呢?


我们可以考虑按照订单编号的order_id进行rang分区,就是把订单编号在1-1000万的放在order1表中,将编号在1000万-2000万的放在order2中,以此类推,每个表中存放1000万数据。如下图所示:


attachments-2020-04-l6gWcE6i5e84415a89320.jpg


虽然我们可以通过库内分表把单表的容量固定在1000万,但是这些表的数据仍然存放在一个库内,使用的是该主机的CPU、IO、内存。

单库的连接数也有限制。并不能完全的降低系统的压力。此时,我们就要考虑另外一种技术叫分库分表。

分库分表在库内分表的基础上,将分的表挪动到不同的主机和数据库上??梢猿浞值氖褂闷渌骰腃PU、内存和IO资源。并且分库之后,单库的连接数限制也不在成为瓶颈。


但是“成也萧何败也萧何”,如果你执行一个扫描不带分片键,则需要在每个库上查一遍。

刚刚我们按照order_id分成了5个库,但是我们查询是name='AAA'的条件并且不带order_id字段时,它并不知道在哪个分片上查,则会创建5个连接,然后每个库都检索一遍。这种广播查询则会造成连接数增多。

因为它需要在每个库上都创立连接。如果是高并发的系统,执行这种广播查询,系统的thread很快就会告警。


attachments-2020-04-LKE7LJgM5e8441762d16e.jpg


总体来说:水平拆分的优点有以下:


  1. 水平扩展能无线扩展。不存在某个库某个表过大的情况。
  2. 能够较好的应对高并发,同时可以将热点数据打散。
  3. 应用侧的改动较小,不需要根据业务来拆分。


水平拆分的缺点:


  1. 路由是个问题,需要增加一层路由的计算,而且像前面说的一样,不带分片键查询会产生广播SQL。
  2. 跨库join的性能比较差。
  3. 需要处理分布式事务的一致性问题。


一起使用


当前我们的系统,垂直拆分和水平拆分都在使用,垂直拆分主要是做业务上的分割,把业务的各个子系统都规划好,能解耦就解耦。而垂直拆分之后。我们再做水平分库分表。通过取模算法将大表数据拆到若干个库中。


逻辑库和物理库


介绍了上述的分库分表,我们有必要说一下几个概念,一个是逻辑库和物理库的概念。我们还是拿水平拆分中的分库分表来说。我们在物理层面,将一个库的数据分割到了5个数据库中。这5个数据库就是物理库,而它们对上层应用的展现则是一个库。这个对上层展现的库就叫逻辑库。逻辑库对应用层是透明的。应用不需要了解底层的情况,直接使用就行了。


还是拿水平拆分中的分库分表来说,orders表总共被分成了5份,分别在底层是orders_1~5。这底层的5个表就是物理表。但是对应用层面来说,只有orders表。这就是逻辑表。


总结:这一篇主要是讲述一些分库分表之后的概念。需要加深一些理解,因为我们的项目也才是刚刚开始拆分,所以有写的不对的地方还希望提出意见指正。



]]>
2020-04-01
<![CDATA[php+laravel依赖注入浅析]]> http://www.cnqigong.com/post/7203/ attachments-2020-04-cuVerfns5e843cafedb50.jpg


盛京棋牌用起来就是,先把对象bind好,需要时可以直接使用make来取就好。


盛京棋牌


$config = $container->make('config');
$connection = new Connection($this->config);

比较好理解,这样的好处就是不用直接 new 一个实例了,方法传值没啥改变,还可以多处共享此实例。

但这跟依赖注入有什么关系,真正的依赖注入是不需给方法传递任何参数值,只需要指明方法参数类型,代码自动查找关系依赖自动注入。

这个特性在 laravel 的 Controller、Job 等处可以体现,如下:

class TestController extends Controller
{
public function anyConsole(Request $request, Auth $input)
{
//todo
}
}


我们来看下他是怎么实现自动依赖注入的:


由 index.php 调用 Kernel ,经过多层 Kernel 管道调用,再到 Router ,经过多层中间件管道调用。最终定位到

Illuminate/Routing/Route.php 第124行。

public function run(Request $request)
{
$this->container = $this->container ?: new Container;
try {
if (! is_string($this->action['uses'])) {
return $this->runCallable($request);
}

if ($this->customDispatcherIsBound()) {
return $this->runWithCustomDispatcher($request);
}

return $this->runController($request);
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}


判断 $this->action['uses'](格式行如:\App\Http\Controller\Datacenter\RealTimeController@anyConsole)是否字符串, $this->customDispatcherIsBound判断是否绑定了用户自定义路由。然后跳转到 $this->runController($request)。

protected function runController(Request $request)
{
list($class, $method) = explode('@', $this->action['uses']);

$parameters = $this->resolveClassMethodDependencies(
$this->parametersWithoutNulls(), $class, $method
);

if (! method_exists($instance = $this->container->make($class), $method)) {
throw new NotFoundHttpException;
}

return call_user_func_array([$instance, $method], $parameters);
}

$this->resolveClassMethodDependencies 这个方法一看名字就知道是我们要找的方法。$this->parametersWithoutNulls()是过滤空字符,$class、$method分别行如:\App\Http\Controller\Datacenter\RealTimeController 与 anyConsole。

protected function resolveClassMethodDependencies(array $parameters, $instance, $method)
{
if (! method_exists($instance, $method)) {
return $parameters;
}

return $this->resolveMethodDependencies(
$parameters, new ReflectionMethod($instance, $method)
);
}

new ReflectionMethod($instance, $method) 是拿到类方法的反射对象,参见文档:http://www.php.net/manual/zh/class.reflectionmethod.php

下面跳转到Illuminate/Routing/RouteDependencyResolverTrait.php 第54行。

public function resolveMethodDependencies(array $parameters, ReflectionFunctionAbstract $reflector)
{
$originalParameters = $parameters;

foreach ($reflector->getParameters() as $key => $parameter) {
$instance = $this->transformDependency(
$parameter, $parameters, $originalParameters
);

if (! is_null($instance)) {
$this->spliceIntoParameters($parameters, $key, $instance);
}
}

return $parameters;
}

通过反射类方法得到类参数数组,然后遍历传递给 $this->transformDependency 方法。如果实例获取不到则调用 $this->spliceIntoParameters 清楚该参数。

protected function transformDependency(ReflectionParameter $parameter, $parameters, $originalParameters)
{
$class = $parameter->getClass();
if ($class && ! $this->alreadyInParameters($class->name, $parameters)) {
return $this->container->make($class->name);
}
}

终于看到了容器的影子,没错最终对象还是通过容器的 make 方法取出来的。至此参数就构造好了,然后最终会被 runController 方法的 call_user_func_array 回调。


总结:


1. 依赖注入原理其实就是利用类方法反射,取得参数类型,然后利用容器构造好实例。然后再使用回调函数调起。
2. 注入对象构造函数不能有参数。否则会报错。Missing argument 1
3. 依赖注入故然好,但它必须要由 Router 类调起,否则直接用 new方式是无法实现注入的。所以这就为什么只有 Controller 、Job 类才能用这个特性了。



attachments-2020-04-ACspWNsM5e843cd594af2.jpg


]]>
2020-04-01
<![CDATA[redis哨兵模式主从切换后,php实现自动切换]]> http://www.cnqigong.com/post/7202/

attachments-2020-04-zAPS6xbr5e8444c76388f.jpg


盛京棋牌会通过选举将对应的从服务器切换为主服务器,以此来达到服务的高可用性。


在业务层面如果主从做了切换可能相对应的服务器IP地址会发生改变,这样会带来程序的的正常运行。


盛京棋牌但是在部分情况下,虚拟机并不支持VIP,这样就无法保证业务的正常运行。所以在此情况下,通过业务本身来实现连接新的主的IP。


本文主要以PHP为例,相关代码如下:


<?php

class SRedis
{

    /**
     * 哨兵地址,支持多哨兵地址
     * @var array
     * eg:  [ [ 'host' => '127.0.0.1' , 'port' => 26379 ] ]
     */
    private $_sentinelAddr = [];

    private $_sentinelConn = null;

    private $_timeout = 10; //超时时间

    private $_masterName = 'mymaster'; //主节点名称

    private static $_handle = []; //存放redis连接实例

    public function __construct(array $iplist, string $masterName = null)
    {
        $this->_sentinelAddr = $iplist;
        $masterName !== null && $this->_masterName = $masterName;
        $this->_getSentinelConn();
    }

    /**
     * 获取redis主节点的实例
     * @return bool|Redis
     * @throws Exception
     */
    public function getInstansOf()
    {
        $masterInfo = $this->getMasterInfo();
        if ($masterInfo) {
            $instansof = $this->_connection($masterInfo[0], $masterInfo[1], $this->_timeout);
            return $instansof;
        }
        return false;
    }

    /**
     * 获取主节点的ip地址
     * @return array
     */
    public function getMasterInfo()
    {
        $masterInfo = [];
        if ($this->_sentinelConn != null) {
            $masterInfo = $this->_sentinelConn->rawcommand("sentinel", 'get-master-addr-by-name', $this->_masterName);
        }
        return $masterInfo;

    }

    /**
     * 设置哨兵连接句柄
     */
    private function _getSentinelConn()
    {
        if (is_array($this->_sentinelAddr) && $this->_sentinelAddr) {
            $this->_sentinelConn = $this->_RConnect($this->_sentinelAddr);
        }
    }

    /**
     * 获取redis句柄(如果是多主机,保证连接的是可用的哨兵服务器)
     * @param array $hosts
     * @return null|Redis
     */
    private function _RConnect(array $hosts)
    {
        $count = count($hosts);
        $redis = null;
        if ($count == 1) {
            $this->_connection($hosts[0]['host'], $hosts[0]['port'], $this->_timeout);
        } else {
            $i = 0;
            while ($redis == null && $i < $count) {
                $redis = $this->_connection($hosts[$i]['host'], $hosts[$i]['port'], $this->_timeout);
                $i++;
            }
        }
        return $redis;
    }

    /**
     * redis 连接句柄
     * @param string $host
     * @param int $port
     * @param int $timeout
     * @return null|Redis
     */
    private function _connection(string $host, int $port, int $timeout)
    {
        if (isset(self::$_handle[$host . ':' . $port])) {
            return self::$_handle[$host . ':' . $port];
        }
        try {
            $redis = new Redis();
            $redis->connect($host, $port, $timeout);
            self::$_handle[$host . ':' . $port] = $redis;
        } catch (\Exception $e) {
            $redis = null;
        }
        return $redis;
    }
}


$hosts = [
    [
        'host' => '127.0.0.1',
        'port' => 26381
    ],
    [
        'host' => '127.0.0.1',
        'port' => 26380
    ]
];
$masterName = 'mymaster';
$sredis = new SRedis($hosts, $masterName);
$masterRedis = $sredis->getInstansOf();
if ($masterRedis) {
    print_r($masterRedis->hgetall("iplist"));
} else {
    echo "redis 服务器连接失败";
}
]]>
2020-04-01
<![CDATA[一致性hash算法--负载均衡]]> http://www.cnqigong.com/post/7201/


有没有好奇过redis、memcache等是怎么实现集群负载均衡的呢?

其实他们都是通过一致性hash算法实现节点调度的。


讲一致性hash算法前,先简述一下求余hash算法:

hash(object)%N

  • 一个缓存服务器宕机了,这样所有映射到这台服务器的对象都会失效,我们需要把属于该服务器中的缓存移除,这时候缓存服务器是 N-1 台,映射公式变成了 hash(object)%(N-1) ;
  • 由于QPS升高,我们需要添加多一台服务器,这时候服务器是 N+1 台,映射公式变成了 hash(object)%(N+1) 。


1 和 2 的改变都会出现所有服务器需要进行数据迁移。


一致性HASH算法

一致性HASH算法的出现有效的解决了上面普通求余算法在节点变动后面临全部缓存失效的问题:

type Consistent struct {

  numOfVirtualNode int 

  hashSortedNodes []uint32

  circle map[uint32]string

  nodes map[string]bool

}

简单地说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某空间哈希函数H的值空间是0-2^32-1(即哈希值是一个32位无符号整形),整个哈??占淙缦拢?/span>

v2-aef0c79a38a74e46de3ae8ac886ae415_720w.jpg

下一步将各个服务器使用哈希算法计算出每台机器的位置,具体可以使用服务器的IP地址或者主机名作为关键字,并且是按照顺时针排列:

//这里我选择crc32,具体情况具体安排

  func hashKey(host string) uint32 {

   scratch := []byte(host) 

  return crc32.ChecksumIEEE(scratch)

}

这里我们假设三台节点memcache经计算后位置如下:

v2-93ccc6b7b7ccb2c715489035845e51c9_720w.jpg

//add the node 

  c.Add("Memcache_server01") 

  c.Add("Memcache_server02") 

  c.Add("Memcache_server03")

  func (c *Consistent) Add(node string) error { 

  if _, ok := c.nodes[node]; ok { 

  return errors.New("host already existed") 

}

  c.nodes[node] = true 

  // add virtual node 

  for i := 0; i < c.numOfVirtualNode; i++ {

  virtualKey := getVirtualKey(i, node)

  c.circle[virtualKey] = node

  c.hashSortedNodes = append(c.hashSortedNodes, virtualKey)

 } 

  sort.Slice(c.hashSortedNodes, func(i, j int) bool {

  return c.hashSortedNodes[i] < c.hashSortedNodes[j]

  })

  return nil

}

接下来使用相同算法计算出数据的哈希值,并由此确定数据在此哈?;飞系奈恢?/span>


假如我们有数据A、B、C和D,经过哈希计算后位置如下:

v2-db61fcbce76462c7d9ffccd38968c23f_720w.jpg


根据一致性哈希算法,数据A就被绑定到了server01上,D被绑定到了server02上,B、C在server03上,是按照顺时针找最近服务节点方法


这样得到的哈?;返鞫确椒?,有很高的容错性和可扩展性:


假设server03宕机

v2-6d2a7518c0121da9b39451d7f089c308_720w.jpg

可以看到此时A、C、B不会受到影响,只是将B、C节点被重定位到Server 1。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即顺着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。


考虑另外一种情况,如果我们在系统中增加一台服务器Memcached Server 04:

v2-62fdba7bd0f02a07c0698a459dd48686_720w.jpg

此时A、D、C不受影响,只有B需要重定位到新的Server 4。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即顺着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。


]]>
2020-03-31
<![CDATA[Kafka为什么吞吐量大、速度快?]]> http://www.cnqigong.com/post/7200/


Kafka是大数据领域无处不在的消息中间件,目前广泛使用在企业内部的实时数据管道,并帮助企业构建自己的流计算应用程序。


Kafka虽然是基于磁盘做的数据存储,但却具有高性能、高吞吐、低延时的特点,其吞吐量动辄几万、几十上百万。


但是很多使用过Kafka的人,经?;岜晃实秸庋桓鑫侍?Kafka为什么速度快,吞吐量大;大部分被问的人都是一下子就懵了,或者是只知道一些简单的点,本文就简单的介绍一下Kafka为什么吞吐量大,速度快。


盛京棋牌

众所周知Kafka是将消息记录持久化到本地磁盘中的,一般人会认为磁盘读写性能差,可能会对Kafka性能如何保证提出质疑。实际上不管是内存还是磁盘,快或慢关键在于寻址的方式,磁盘分为顺序读写与随机读写,内存也一样分为顺序读写与随机读写?;诖排痰乃婊列慈肥岛苈?,但磁盘的顺序读写性能却很高,一般而言要高出磁盘随机读写三个数量级,一些情况下磁盘顺序读写性能甚至要高于内存随机读写。


这里给出著名学术期刊 ACM Queue 上的性能对比图: https://queue.acm.org/detail.cfm?id=1563874

v2-e6e7b4c9b7b3dd1a71c2920a9f6f12bf_720w.jpg


盛京棋牌并且操作系统也对这种模式做了大量优化,Kafka就是使用了磁盘顺序读写来提升的性能。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升 。


v2-7544cbc3cf9a676c787b55de64929673_720w.jpg

上图就展示了Kafka是如何写入数据的, 每一个Partition其实都是一个文件 ,收到消息后Kafka会把数据插入到文件末尾(虚框部分)。

这种方法有一个缺陷—— 没有办法删除数据 ,所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据 。

v2-6e1de2cce71df75f7a330bb5514f749a_720w.jpg

两个消费者,Consumer1有两个offset分别对应Partition0、Partition1(假设每一个Topic一个Partition);Consumer2有一个offset对应Partition2。这个offset是由客户端SDK负责保存的,Kafka的Broker完全无视这个东西的存在;一般情况下SDK会把它保存到zookeeper里面。(所以需要给Consumer提供zookeeper的地址)。


如果不删除硬盘肯定会被撑满,所以Kakfa提供了两种策略来删除数据。一是基于时间,二是基于partition文件大小。具体配置可以参看它的配置文档。


二、Page Cache

为了优化读写性能,Kafka利用了操作系统本身的Page Cache,就是利用操作系统自身的内存而不是JVM空间内存。这样做的好处有:


  • 避免Object消耗:如果是使用 Java 堆,Java对象的内存消耗比较大,通常是所存储数据的两倍甚至更多。
  • 避免GC问题:随着JVM中数据不断增多,垃圾回收将会变得复杂与缓慢,使用系统缓存就不会存在GC问题


相比于使用JVM或in-memory cache等数据结构,利用操作系统的Page Cache更加简单可靠。首先,操作系统层面的缓存利用率会更高,因为存储的都是紧凑的字节结构而不是独立的对象。其次,操作系统本身也对于Page Cache做了大量优化,提供了 write-behind、read-ahead以及flush等多种机制。再者,即使服务进程重启,系统缓存依然不会消失,避免了in-process cache重建缓存的过程。

通过操作系统的Page Cache,Kafka的读写操作基本上是基于内存的,读写速度得到了极大的提升。


三、零拷贝

linux操作系统 “零拷贝” 机制使用了sendfile方法, 允许操作系统将数据从Page Cache 直接发送到网络,只需要最后一步的copy操作将数据复制到 NIC 缓冲区, 这样避免重新复制数据 。示意图如下:

v2-8578d40b28c8ac6a2ee0a78c3ce1e8cf_720w.jpg


通过这种 “零拷贝” 的机制,Page Cache 结合 sendfile 方法,Kafka消费端的性能也大幅提升。这也是为什么有时候消费端在不断消费数据时,我们并没有看到磁盘io比较高,此刻正是操作系统缓存在提供数据。


当Kafka客户端从服务器读取数据时,如果不使用零拷贝技术,那么大致需要经历这样的一个过程:


  • 操作系统将数据从磁盘上读入到内核空间的读缓冲区中。
  • 应用程序(也就是Kafka)从内核空间的读缓冲区将数据拷贝到用户空间的缓冲区中。
  • 应用程序将数据从用户空间的缓冲区再写回到内核空间的socket缓冲区中。
  • 操作系统将socket缓冲区中的数据拷贝到NIC缓冲区中,然后通过网络发送给客户端。


v2-e4013fd945b96ad8215fb396d5b43709_720w.jpg

从图中可以看到,数据在内核空间和用户空间之间穿梭了两次,那么能否避免这个多余的过程呢?当然可以,Kafka使用了零拷贝技术,也就是直接将数据从内核空间的读缓冲区直接拷贝到内核空间的socket缓冲区,然后再写入到NIC缓冲区,避免了在内核空间和用户空间之间穿梭。

v2-f841fd2a5b808bddad55f95320c414df_720w.jpg

可见,这里的零拷贝并非指一次拷贝都没有,而是避免了在内核空间和用户空间之间的拷贝。如果真是一次拷贝都没有,那么数据发给客户端就没了不是?不过,光是省下了这一步就可以带来性能上的极大提升。


四、分区分段+索引

Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的。这也非常符合分布式系统分区分桶的设计思想。


通过这种分区分段的设计,Kafka的message消息实际上是分布式存储在一个一个小的segment中的,每次文件操作也是直接操作的segment。为了进一步的查询优化,Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。


五、批量读写

Kafka数据读写也是批量的而不是单条的。

除了利用底层的技术外,Kafka还在应用程序层面提供了一些手段来提升性能。最明显的就是使用批次。在向Kafka写入数据时,可以启用批次写入,这样可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。假设网络带宽为10MB/S,一次性传输10MB的消息比传输1KB的消息10000万次显然要快得多。


六、批量压缩

在很多情况下,系统的瓶颈不是CPU或磁盘,而是网络IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。进行数据压缩会消耗少量的CPU资源,不过对于kafka而言,网络IO更应该需要考虑。


1>如果每个消息都压缩,但是压缩率相对很低,所以Kafka使用了批量压缩,即将多个消息一起压缩而不是单个消息压缩

2>Kafka允许使用递归的消息集合,批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩

3>Kafka支持多种压缩协议,包括Gzip和Snappy压缩协议

Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。

]]>
2020-03-31
<![CDATA[php创建多个请求实现多进程]]> http://www.cnqigong.com/post/7199/ attachments-2020-03-F96m3hrI5e82df18edf8d.jpg



1:引入第三方类库

v2-f30bc40a8617e91fbbaf1fe223bd11ba_720w.jpg

vendor下:

<?php
namespace Curlroll;
class CurlRoll
{
    /**
     * @var int
     * 并发请求数,设置此值过大,同一时间内如果请求远端主机会很容易被判定为DDos攻击
     */
    private $window_size = 5;
    /**
     * @var float
     * curl_multi_select 处理超时时间.
     */
    private $timeout = 10;
    /**
     * @var array
     * 请求对象 CurlRequest 实例数组
     */
    private $requests = array();
    /**
     * @var array
     * 并发请求map
     */
    private $requestMap = array();
    /**
     * @var string|array
     * callback function,结果处理回调函数.
     */
    private $callback;
    /**
     * @var array
     * HTTP request default options.
     */
    private $options = array(
        CURLOPT_SSL_VERIFYPEER => 0, //不开启https请求
        CURLOPT_RETURNTRANSFER => 1, //请求信息以文件流方式返回
        CURLOPT_CONNECTTIMEOUT => 10, //连接超时时间
        CURLOPT_TIMEOUT => 20, //设置curl执行最大时间
        CURLOPT_FOLLOWLOCATION => 1, //curl允许根据response location的值重定向请求
        CURLOPT_MAXREDIRS => 5, //CURLOPT_FOLLOWLOCATION为真后,此值设定重定向递归最大次数
        CURLOPT_HEADER => 0, //设置为true,请求返回的文件流中就会包含response header
        CURLOPT_AUTOREFERER => true, //当根据Location重定向时,自动设置header中的referer信息
        CURLOPT_ENCODING => "", //HTTP请求头中"Accept-Encoding"的值,为空发送所有支持的编码类型
    );
    /**
     * @var array
     * HTTP Request发送的header信息
     */
    private $headers = array(
        'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3',
        'Connection: close',
        'Cache-Control: max-age=0',
        //'X-FORWARD-FOR:8.8.8.8',      //代理ip地址
        //'CLIENT-IP:3.3.3.3',          //客户端ip,REMOTE_ADDR不为空的情况下,是比较真是ip,不好伪造
    );
    private static $agent = array(
        //google chrome
        'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36',
        'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36',
        'Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36',
        'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36',
        'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0',
        //firefox
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:25.0) Gecko/20100101 Firefox/25.0',
        'Mozilla/5.0 (Windows NT 6.0; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0',
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0',
        'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
        //ie
        'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)',
        'Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)',
        'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; GTB7.4; InfoPath.2; SV1; .NET CLR 3.3.69573; WOW64; en-US)',
    );
    /**
     * @param int
     * $window_size
     */
    public function __construct($window_size = 5)
    {
        $this->window_size = (int)$window_size ? : 5;
    }
    /**
     * @return void
     */
    public function __destruct()
    {
        unset($this->window_size, $this->callback, $this->options, $this->headers, $this->requests);
    }
    /**
     * @param string $name
     * @return mixed
     */
    public function __get($name)
    {
        return isset($this->{$name}) ? $this->{$name} : null;
    }
    /**
     * @param string $name
     * @param mixed $value
     * @return bool
     */
    public function __set($name, $value)
    {
        // append the base options & headers
        if ($name == "options" || $name == "headers")
        {
            $this->{$name} = $value + $this->{$name};
        } else
        {
            $this->{$name} = $value;
        }
        return true;
    }
    /**
     * Add a request to the request queue
     *
     * @param $url
     * @return bool
     */
    public function add($url)
    {
        $this->requests[] = $this->createRequest($url, 'GET', $this->headers, $this->options);
        return true;
    }
    /**
     * Perform GET request
     *
     * @param string $url
     * @param  $headers 不是key-value数组,http请求request header部分的内容
     * $headers = array(
     * "POST ".$page." HTTP/1.0",
     * "Content-type: text/xml;charset=\"utf-8\"",
     * "Accept: text/xml",
     * "Cache-Control: no-cache",
     * "Pragma: no-cache",
     * "SOAPAction: \"run\"",
     * "Content-length: ".strlen($xml_data),
     * "Authorization: Basic " . base64_encode($credentials)
     * );
     * @param  $options
     * @return bool
     */
    public function get($url, $headers = array(), $options = array())
    {
        $this->requests[] = $this->createRequest($url, "GET", $headers, $options);
        return true;
    }
    /**
     * Perform POST request
     *
     * @param string $url
     * @param  $post_data
     * @param  $headers
     * @param  $options
     * @return bool
     */
    public function post($url, $headers = array(), $options = array(), $post_data)
    {
        $this->requests[] = $this->createRequest($url, "POST", $headers, $options, $post_data);
        return true;
    }
    /**
     * Execute processing
     *
     * @param mixed $callback
     * @return string|null
     */
    public function execute($callback = null)
    {
        $ret = null;
        if ($callback)
        {
            $this->callback = $callback;
        }
        if (count($this->requests) == 1)
        {
            $ret = $this->single_curl();
        } else
        {
            $ret = $this->rolling_curl();
        }
        //clear all request once time
        $this->requests = $this->requestMap = array();
        return $ret;
    }
    /**
     * Performs a single curl request
     *
     * @access private
     * @return string
     */
    private function single_curl()
    {
        $ch = curl_init();
        $request = array_shift($this->requests);
        $options = $this->get_options($request);
        curl_setopt_array($ch, $options);
        $output = curl_exec($ch);
        $info = curl_getinfo($ch);
        if ($this->callback && is_callable($this->callback))
        {
            $callback = $this->callback;
            return call_user_func($callback, $output, $info, $request);
        } else
        {
            return $output;
        }
    }
    /**
     * Performs multiple curl requests
     *
     * @access private
     * @return bool
     */
    private function rolling_curl()
    {
        $n = count($this->requests);
        if ($n < $this->window_size)
        {
            $this->window_size = $n;
        }
        if ($this->window_size < 2)
        {
            return false;
        }
        $master = curl_multi_init();
        // start the first batch of requests
        //注意变量i的作用域不是for循环体内,在后续还是可以使用的
        for($i = 0; $i < $this->window_size; $i++)
        {
            $ch = curl_init();
            $options = $this->get_options($this->requests[$i]);
            curl_setopt_array($ch, $options);
            curl_multi_add_handle($master, $ch);
            $key = (string)$ch;
            $this->requestMap[$key] = $i;
        }
        do
        {
            while (($execrun = curl_multi_exec($master, $running)) == CURLM_CALL_MULTI_PERFORM) ;
            if ($execrun != CURLM_OK)
            {
                break;
            }
            // a request was just completed -- find out which one
            while ($done = curl_multi_info_read($master))
            {
                // get the info and content returned on the request
                $info = curl_getinfo($done['handle']);
                $output = curl_multi_getcontent($done['handle']);
                // send the return values to the callback function.
                $callback = $this->callback;
                if (is_callable($callback))
                {
                    $key = (string)$done['handle'];
                    $request = $this->requests[$this->requestMap[$key]];
                    unset($this->requestMap[$key]);
                    call_user_func($callback, $output, $info, $request);
                }
                // start a new request (it's important to do this before removing the old one)
                $n = count($this->requests);
                if (($i < $n) && isset($this->requests[$i]))
                {
                    $ch = curl_init();
                    $options = $this->get_options($this->requests[$i]);
                    curl_setopt_array($ch, $options);
                    curl_multi_add_handle($master, $ch);
                    // Add to our request Maps
                    $key = (string)$ch;
                    $this->requestMap[$key] = $i;
                    $i++;
                }
                // remove the curl handle that just completed
                curl_multi_remove_handle($master, $done['handle']);
            }
            // Block for data in / output; error handling is done by curl_multi_exec
            if ($running)
            {
                curl_multi_select($master, $this->timeout);
            }
        } while ($running);
        return true;
    }
    /**
     * Helper function to set up a new request by setting the appropriate options
     *
     * @access private
     * @param Request $request
     * @return array
     */
    private function get_options($request)
    {
        $options = $this->__get('options');
        $headers = $this->__get('headers');
        // set the request URL
        $options[CURLOPT_URL] = $request->url;
        // set the request method
        // curl默认就是get,设定post_data,既可认为请求是post请求
        // posting data w/ this request?
        if ($request->post_data)
        {
            $options[CURLOPT_POST] = true;
            $options[CURLOPT_POSTFIELDS] = $request->post_data;
        }
        // append custom options for this specific request
        if ($request->options)
        {
            $options = $options + $request->options;
        }
        // 添加个性header
        if ($request->headers)
        {
            $headers = $headers + $request->headers;
        }
        $options[CURLOPT_HTTPHEADER] = $headers;
        return $options;
    }
    private function createRequest($url, $method, $headers, $options, $data = array())
    {
        $o = new \stdClass();
        $o->url = $url;
        $o->method = $method;
        $o->headers = $headers;
        $o->options = $options;
        $o->post_data = $data;
        if (!isset($options[CURLOPT_USERAGENT]))
        {
            $o->options[CURLOPT_USERAGENT] = self::$agent[array_rand(self::$agent)];
        }
        return $o;
    }
}


2:创建对象

v2-946baf4f01c155bca5bbf8408c2091cc_720w.jpg


]]>
2020-03-31
<![CDATA[PHP实现智能语音播报]]> http://www.cnqigong.com/post/7198/ attachments-2020-03-B3LQGpY95e82e070f2e08.png

盛京棋牌叫你起床...甚至能够接受语音指令!所谓的人工智能音响,听起来很高大上,都说PHP是最好的编程语言,今天我就带大家来实现一个语音播报功能,写个美女叫你早上起床!

先大体说一个思路,PHP怎么实现语音播报呢?其实就是调个API(接口)的事情,这个就尴尬了。

实际上,现在很多AI平台都提供一些成熟的接口供你使用,比如语音转文字,文字转语音,语音唤醒等等,思路就是使用PHP获取当前的时间和天气状况,然后调用接口转换成甜美的妹子语音播放出来。

盛京棋牌


第一步:获取时间信息

举个例子,文字内容可能是这样:“主人,早上好,今天是2017年12月18号上午8点整,星期一”,这样的内容用PHP自带的几个时间函数就能搞定,然后拼接成字符文字!下面是一些简单实例代码:

v2-1f294342f2a1900b53db94520a3fb7a6_720w.jpg


PHP实现智能语音播报天气

获取时间


第二步:获取天气状况

举个例子,文字内容可能是这样:“今天天气多云转晴,温度5-15度,湿度80%,空气污染指数69”。要想找到一个靠谱而又免费的api还有点麻烦,很多免费的api提供的天气信息都比较简单,只有天气状况和温度,没有未来天气状况,最后我就找了个凑合用,谁有更好的api留个爪。实例代码如下:

v2-39c0894e1f9932ea1da430cc2c0739d5_720w.jpg


PHP实现智能语音播报天气

天气状况


第三步:语音合成

这个是调用的百度的接口,首先呢,你得去百度那注册一个账号,获取开发者的key和secret,会有一些免费的调用次数,不拿去商用的话完全够了!然后下载百度提供的SDK,用法非常简单,实例代码如下:

v2-68089a76c3c5048ab3d391ae108806f5_720w.jpg


PHP实现智能语音播报天气

大家可以看到最后的返回的内容被我存到/tmp/audio.mp3这个文件里面去了(这里使用的是Ubuntu系统),这里可能会有一个写入权限问题,建议大家最后执行脚本的时候加上sudo。


第四步:播放合成之后的语音文件

我们不可能去用音乐播放器手动播放,其实Linux在命令行下也可以播放音乐,需要安装一个软件,直接给大家Ubuntu下的安装命令:

sudo apt-get install sox libsox-fmt-all

安装完成之后就可以使用play命令播放音乐,举个例子:play hello.mp3

所以接下来我们就可以使用PHP去执行播放命令,实例如下:

exec('sudo /usr/bin/play /tmp/audio.mp3');

最后,在Linux里面运行脚本,让脚本常驻后台,示例如下:

/usr/bin/php /var/www/demo/BaiduSound/index.php > /dev/null 2>&1 &

以上就是全部步骤,剩下的大家发挥想象力,比如定时给你播报一些股票信息、播放歌曲、早上定时叫你起床。

从理论上说我们还可以调用百度API接口去识别我们的语音命令,然后根据命令去执行操作,这样岂不是就是一个AI音响了?

哈哈,纯属娱乐,这个方案有一个问题就是你得保证你的电脑一直是开机状态,有点浪费电,有兴趣的童鞋可以买个类似树莓派这样的低功耗设备去运行。


]]>
2020-03-31
<![CDATA[PHP实现财务审核通过后返现金额到客户]]> http://www.cnqigong.com/post/7197/ attachments-2020-03-s9D3CQkP5e82e19526f4b.jpg

应用场景:

有这么一个返现的系统,当前端客户发起提现的时候,后端就要通过审核这笔返现订单,才可以返现到客户的账号里。


来看看下面的截图

v2-a65c7d410eb59d4873b040b87047f15d_720w.jpg

这里的业务场景就是经过两轮审核:销售审核,财务审核都通过后,后端就会付款一笔钱到客户,当然,这里财务审核会有很多种情况,不通过与通过,通过后直接付款又有很多种情况,详细可以查看微信付款到零钱的文档。下面就来看看具体你的代码实现


微信支付配置


1,数据表大概如下

CREATE TABLE `zmq_weixin_config` (
  `id` int(1) unsigned NOT NULL AUTO_INCREMENT COMMENT '微信公众平台编号,自增id',
  `weixin_name` varchar(10) NOT NULL COMMENT '微信公众平台名称',
  `token` varchar(100) NOT NULL,
  `appid` char(18) NOT NULL,
  `appsecret` char(32) NOT NULL,
  `access_token` varchar(300) NOT NULL,
  `api_ticket` varchar(300) DEFAULT NULL COMMENT '微信卡包api_ticket',
  `api_ticket_expired_at` datetime DEFAULT NULL COMMENT '微信卡包api_ticket过期时间',
  `mchid` varchar(20) DEFAULT NULL COMMENT '商户号',
  `mchkey` varchar(50) DEFAULT NULL COMMENT '支付密钥',
  `expired_at` timestamp NULL DEFAULT NULL COMMENT 'access_token过期时间,会自动更新',
  `updated_at` timestamp NULL DEFAULT NULL COMMENT '记录更新时间',
  `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间',
  `sort_order` smallint(5) NOT NULL DEFAULT '0' COMMENT '排序',
  `points_url` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC COMMENT='微信配置表';

2 .config的配置方法

/**
 * 获取微信支付配置
 * 这里我是把配置信息存储在数据表里,方便调用
 * $param是从controller里传值过来:weixin_config_id,notify_url
 * @return array
 */
public function getWechatConfig($param)
{
    $weixin = WeixinConfigBaseModel::find($param['weixin_config_id']);
    if (empty($weixin)) {
        throw new Exception('微信配置ID错误');
    }

    return [
        'wechat' =>[
            $app_id => $weixin->appid,
            'mch_id' => $weixin->mchid,
            'notify_url' => empty($param['notify_url']) ? '' : $param['notify_url'],  //回调url
            'key' => $weixin->mchkey,
            'cert_client' => resource_path().'/wechat/'.$weixin->id.'/apiclient_cert.pem',  //证书与key
            'cert_key' => resource_path().'/wechat/'.$weixin->id.'/apiclient_key.pem',
        ]
    ];
}

企业付款到个人零钱核心代码

/**
 * Function:企业付款到个人零钱
 * Author:cyw0413
 * @param $openid
 * @param $trade_no
 * @param $money
 * @param $desc
 * @return array
 */
public function weixinPay($input){

    $config = $this->getWechatConfig($input);

    $params["mch_appid"]= $config['wechat']['app_id'];
    $params["mchid"] = $config['wechat']['mch_id'];
    $params["nonce_str"]= date("YmdHis").mt_rand(100,999);
    $params["partner_trade_no"] = $input['trade_no'];    //商户订单号
    $params["amount"]   = $input['amount'];
    $params["desc"]     = $input['desc'];
    $params["openid"]   = $input['openid'];
    $params["check_name"]= 'NO_CHECK';
    $params['spbill_create_ip'] = $_SERVER['SERVER_ADDR'];

    //生成签名
    $str = 'amount='.$params["amount"].'&check_name='.$params["check_name"].'&desc='.$params["desc"].'&mch_appid='.$params["mch_appid"].'&mchid='.$params["mchid"].'&nonce_str='.$params["nonce_str"].'&openid='.$params["openid"].'&partner_trade_no='.$params["partner_trade_no"].'&spbill_create_ip='.$params['spbill_create_ip'].'&key='.$config['wechat']['key'];
    //md5加密 转换成大写
    $sign = strtoupper(md5($str));
    //生成签名
    $params['sign'] = $sign;

    //构造XML数据
    $xmldata = $this->array_to_xml($params); //数组转XML
    $url='https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';

    //发送post请求
    $res = $this->curl_post_ssl($url, $xmldata, $input['weixin_config_id']); //curl请求
    if(!$res){
        throw new \Exception("服务器连接失败");
    }

    //付款结果分析
    $content = $this->xml_to_array($res); //xml转数组
    return $content;
}

/**
 * curl请求
**/
public function curl_post_ssl($url, $xmldata, $weixin_config_id,$second=30,$aHeader=[]){
    $ch = curl_init();
    //超时时间
    curl_setopt($ch,CURLOPT_TIMEOUT,$second);
    curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch,CURLOPT_URL,$url);
    curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
    curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,false);

    //默认格式为PEM,可以注释
    curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
    //绝对地址可使用 dirname(__DIR__)打印,如果不是绝对地址会报 58 错误
    curl_setopt($ch,CURLOPT_SSLCERT, resource_path().'/wechat/'.$weixin_config_id.'/apiclient_cert.pem');
    curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
    curl_setopt($ch,CURLOPT_SSLKEY,resource_path().'/wechat/'.$weixin_config_id.'/apiclient_key.pem');
    if( count($aHeader) >= 1 ){
        curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader);
    }
    curl_setopt($ch,CURLOPT_POST, 1);
    curl_setopt($ch,CURLOPT_POSTFIELDS,$xmldata);
    $data = curl_exec($ch);
    if($data){
        curl_close($ch);
        return $data;
    } else {
        $error = curl_errno($ch);
        echo "call faild, errorCode:$error\n";
        //die();
        curl_close($ch);
        return false;
    }
}

/**
 * array 转 xml
 * 用于生成签名
 */
public function array_to_xml($arr){
    $str='<xml>';
    foreach($arr as $k=>$v) {
        $str.='<'.$k.'>'.$v.'</'.$k.'>';
    }
    $str.='</xml>';
    return $str;

}

/**
 * xml 转化为array
 */
public function xml_to_array($xml){
    //禁止引用外部xml实体
    libxml_disable_entity_loader(true);
    $xmlString = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
    $val = json_decode(json_encode($xmlString),true);
    return $val;
}

财务审核,也就是微信返现到零钱,这个时候会返回成功结果,或者是各种不成功的结果


这里我用一个方法封装

 //财务审核
if($param['status'] == 2){

    //判断返现金额与修改后的金额
    if($before_rebate_amount != $param['rebate_amount']){
        //返现金额不相等,则出款金额改变
        $out_amount = $param['rebate_amount'] - $before_rebate_amount ;
        $this->outMount($business->business_id,$out_amount);
    }

    if($param['rebate_status'] == 9){
        //财务拒绝通过
        $business->audit_status = $param['rebate_status'];
        $business->rebate_amount = $param['rebate_amount'];
        $business->status = 6;
        $business->save();

        //生成日志
        $this->insertWithdrawLog($param['withdraw_id'], $business->status, $business->audit_status, $param['rebate_remark'], $param['admin_id']);

    }else{
        //提现的各种返回结果
        $this->payReturnResult($business,$param);
    }
}


/**
 * Function:微信提现返回的各种结果
 * Author:cyw0413
 * @param $res
 * @param $business
 * @param $param
 */
public function payReturnResult($business,$param)
{
    $input = [
        'weixin_config_id' => 20 ,
        'openid'           => $business->business->open_id,
        'amount'           => $param['rebate_amount'] * 100,
        'trade_no'         => $business->order_sn, //商户订单号
        'desc'             => "微信提现"
    ];

    $pay = new PayLogBaseService();
    $res = $pay->weixinPay($input);

    if($res['result_code']=="SUCCESS"){
        //提现成功
        $business->audit_status = 4;
        $business->status = 4;
        $business->rebate_amount = $param['rebate_amount'];
        $param['rebate_remark'] = "已付款(".$param['rebate_amount'].")";

    }elseif ($res['err_code'] == "MONEY_LIMIT"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,已达到付款给此用户额度上限";
        //throw new \Exception($param['rebate_remark']);

    }elseif ($res['err_code'] == "AMOUNT_LIMIT"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,低于最低付款金额或者高于最高付款金额";
        //throw new \Exception($param['rebate_remark']);
egdf
    }elseif ($res['err_code'] == "NOTENOUGH"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,付款帐号余额不足或资金未到账";
        //throw new \Exception($param['rebate_remark']);

    }elseif ($res['err_code'] == "SIGN_ERROR"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,签名错误";

    }elseif ($res['err_code'] == "PARAM_ERROR"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,参数错误";

    }elseif ($res['err_code'] == "OPENID_ERROR"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,Openid错误";

    }elseif ($res['err_code'] == "FATAL_ERROR"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,两次请求参数不一致";

    }elseif ($res['err_code'] == "CA_ERROR"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,商户API证书校验出错";

    }elseif ($res['err_code'] == "V2_ACCOUNT_SIMPLE_BAN"){
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,无法给非实名用户付款";

    }else{
        $business->audit_status = 3;
        $business->status = 3;
        $param['rebate_remark'] = "提现失败,服务器繁忙,请稍后再试";
        //throw new \Exception($param['rebate_remark']);
    }

    $business->save();

}

当微信平台余额不足或者出现各种错误而提现失败的时候,这里还有支持重新付款的功能:其实就是点击按钮后重新调用付款到零钱的功能,知道成功付款

/**
 * Function:重新付款
 * Author:cyw0413
 * @param $param
 * @throws \Exception
 */
public function repay($param)
{
    if(empty($param)){
        throw new \Exception("参数错误");
    }

    $business = GroupBusinessWithdrawBaseModel::find($param['withdraw_id']);
    if(empty($business)){
        throw new \Exception("不存在!");
    }

    if($business->audit_status != 3){
        throw new \Exception("状态有错误");
    }

    //提现的各种返回结果
    $this->payReturnResult($business,$param);

}

]]>
2020-03-31
<![CDATA[Redis主从复制]]> http://www.cnqigong.com/post/7196/


盛京棋牌

主从复制的原理以及过程必须要掌握,这样我们才知道为什么会出现这些问题

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段

在从节点执行 slaveof 命令后,复制过程便开始运作,下面图示大概可以看到,从图中可以看出复制过程大致分为6个过程

v2-f71382b7c4dc316d78feeae3ebb2e7ce_720w.jpg

主从配置之后的日志记录也可以看出这个流程。


盛京棋牌

执行 slaveof 后 Redis 会打印日志。


2)从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接

v2-f2c6c2c4d7adb0a33fdd8b99567c4916_720w.jpg


从节点与主节点建立网络连接

从节点会建立一个 socket 套接字,从节点建立了一个端口为51234的套接字,专门用于接受主节点发送的复制命令。从节点连接成功后打印日志

如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行 slaveof no one 取消复制
关于连接失败,可以在从节点执行 info replication 查看 master_link_down_since_seconds 指标,它会记录与主节点连接失败的系统时间。从节点连接主节点失败时也会每秒打印如下日志,方便发现问题:

# Error condition on socket for SYNC: {socket_error_reason}


3)发送 ping 命令。

连接建立成功后从节点发送 ping 请求进行首次通信,ping 请求主要目的如下:

  • 检测主从之间网络套接字是否可用。
  • 检测主节点当前是否可接受处理命令。


如果发送 ping 命令后,从节点没有收到主节点的 pong 回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制连接,下次定时任务会发起重连。

v2-2bc393d122573d42174ea3576396e0c7_720w.jpg

从节点发送的 ping 命令成功返回,Redis 打印如下日志,并继续后续复制流程。

4)权限验证。如果主节点设置了 requirepass 参数,则需要密码验证,从节点必须配置 masterauth 参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程。

5)同步数据集。主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。

6)命令持续复制。当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

]]>
2020-03-31
<![CDATA[PHP FFI详解——一种全新的PHP扩展方法]]> http://www.cnqigong.com/post/7195/


随着PHP7.4而来的有一个我认为非常有用的一个扩展:PHP FFI(Foreign Function interface),引用一段PHP FFI RFC中的一段描述:

对于PHP,FFI提供了一种在纯PHP中编写PHP扩展和对C库的绑定的方法。



盛京棋牌而对于PHP而言,FFI让我们可以方便的调用C语言写的各种库。

其实现有大量的PHP扩展是对一些已有的C库的包装,某些常用的mysqli,curl,gettext等,PECL中也有大量的类似扩展。


盛京棋牌我们需要用C语言写包装器,把他们包装成扩展,这个过程中就需要大家去学习PHP的扩展怎么写,当然现在也有一些方便的方式,某种Zephir。但总还是有一些学习成本的,而有了FFI之后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了。

而C语言几十年的历史中,积累积累的优秀的库,FFI直接让我们可以方便的享受这个庞大的资源了。


言归正传,今天我用一个例子来介绍,我们如何使用PHP来调用libcurl,来抓取一个网页的内容,为什么要用libcurl呢?

PHP不是已经有了curl扩展了么?嗯,首先因为libcurl的api我比较熟,其次呢,正是因为有了,才好对比,传统扩展方式和FFI方式直接的易用性不是?


首先,某些我们就拿当前你看的这篇为例,我现在需要写一段代码来抓取它的内容,如果用传统的PHP的curl扩展,我们大概会这么写:

<?php
 
$ url  =  “ https://www.laruence.com/2020/03/11/5475.html” ;
$ ch  =  curl_init ();
 
curl_setopt ($ ch , CURLOPT_URL , $ url );
curl_setopt ($ ch , CURLOPT_SSL_VERIFYPEER , 0 );
 
curl_exec ($ ch );
 
curl_close ($ ch );

(因为我的网站是https的,所以会多一个设置SSL_VERIFYPEER的操作)


那如果是用FFI呢?

首先要启用PHP7.4的ext / ffi,需要注意的是PHP-FFI要求libffi-3以上。

然后,我们需要告诉PHP FFI我们要调用的函数原型是咋样的,这个我们可以使用FFI :: cdef,它的原型是:

FFI :: cdef ([ string $ cdef  =  “”  [, string $ lib  = null ]]): FFI


在字符串$ cdef中,我们可以写C语言函数式申明,FFI会parse它,了解到我们要在字符串$ lib这个库中调用的函数的签名是啥样的,在这个例子中,我们用到三一个libcurl的函数,它们的申明我们都可以在libcurl的文档里找到。

具体到这个例子,我们写一个curl.php,包含所有要申明的东西,代码如下:

$ libcurl  = FFI :: cdef (<<< CTYPE
无效* curl_easy_init ();
int curl_easy_setopt ( void * curl , int选项, ...);
int curl_easy_perform ( void * curl );
void curl_easy_cleanup ( void * handle );
类型
 , “ libcurl.so”
 );


这里有个地方是,文档中写的是返回值是CURL *,但事实上因为我们的示例中不会解引用它,只是传递,那就避免麻烦就用void *代替。

然而还有个麻烦的事情是,PHP预定义好了:

<?php
const CURLOPT_URL =  10002 ;
const CURLOPT_SSL_VERIFYPEER =  64 ;
 
$ libcurl  = FFI :: cdef (<<< CTYPE
无效* curl_easy_init ();
int curl_easy_setopt ( void * curl , int选项, ...);
int curl_easy_perform ( void * curl );
void curl_easy_cleanup ( void * handle );
类型
 , “ libcurl.so”
 );


好了,定义部分就算完成了,现在我们完成实际逻辑部分,整个下来的代码会是:

<?php
需要 “ curl.php” ;
 
$ url  =  “ https://www.laruence.com/2020/03/11/5475.html” ;
 
$ ch  =  $ libcurl- > curl_easy_init ();
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER , 0 );
 
$ libcurl- > curl_easy_perform ($ ch );
 
$ libcurl- > curl_easy_cleanup ($ ch );


怎么样,比例使用curl扩展的方式,是不是一样简练呢?

接下来,我们稍微弄的复杂一点,也直到,如果我们不想要结果直接输出,而是返回成一个字符串呢,对于PHP的curl扩展来说,我们只需要调用curl_setop把CURLOPT_RETURNTRANSFER为1,但在libcurl中其实并没有直接返回字符串的能力,或者提供了一个WRITEFUNCTION的替代函数,在有数据返回的时候,libcurl会调用这个函数,实际上PHP curl扩展也是这样做的。


目前我们并不能直接把一个PHP函数作为附加函数通过FFI传递给libcurl,那我们都有俩种方式来做:

1.采用WRITEDATA,默认的libcurl会调用fwrite作为一个变量函数,而我们可以通过WRITEDATA给libcurl一个fd,让它不要写入stdout,而是写入到这个fd
2.我们自己编写一个C到简单函数,通过FFI日期进来,传递给libcurl。

我们先用第一种方式,首先我们需要使用fopen,这次我们通过定义一个C的头文件来申明原型(file.h):

void * fopen ( char *文件名, char *模式);
void fclose ( void * fp );


像file.h一样,我们把所有的libcurl的函数申明也放到curl.h中去

#定义 FFI_LIB “libcurl.so”
 
无效 * curl_easy_init ();
int  curl_easy_setopt (void  * curl , int选项, ...);
int  curl_easy_perform (void  * curl );
void  curl_easy_cleanup (CURL * handle ); 复制代码


然后我们就可以使用FFI :: load来加载.h文件:

静态 函数 加载(字符串$ filename ): FFI ;

但是怎么告诉FFI加载那个对应的库呢?如上面,我们通过定义了一个FFI_LIB的宏,来告诉FFI这些函数来自libcurl.so,当我们用FFI :: load加载这个h文件的时候,PHP FFI就会自动加载libcurl.so

那为什么fopen不需要指定加载库呢,那是因为FFI也会在变量符号表中查找符号,而fopen是一个标准库函数,它早就存在了。


好,现在整个代码会是:

<?php
const CURLOPT_URL =  10002 ;
const CURLOPT_SSL_VERIFYPEER =  64 ;
const CURLOPT_WRITEDATA =  10001 ;
 
$ libc  = FFI :: load (“ file.h” );
$ libcurl  = FFI :: load (“ curl.h” );
 
$ url  =  “ https://www.laruence.com/2020/03/11/5475.html” ;
$ tmpfile  =  “ /tmp/tmpfile.out” ;
 
$ ch  =  $ libcurl- > curl_easy_init ();
$ fp  =  $ libc- > fopen ($ tmpfile , “ a” );
 
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER , 0 );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEDATA , $ fp );
$ libcurl- > curl_easy_perform ($ ch );
 
$ libcurl- > curl_easy_cleanup ($ ch );
 
$ libc- > fclose ($ fp );
 
$ ret  =  file_get_contents ($ tmpfile );
@unlink ($ tmpfile );


但这种方式呢就是需要一个临时的中转文件,还是不够优雅,现在我们用第二种方式,要用第二种方式,我们需要自己用C写一个替代函数传递给libcurl:

#include  <stdlib.h> #include  <string.h> #include  “ write.h” size_t own_writefunc (void * ptr ,size_t size ,size_t nmember ,void * data ){         
        own_write_data * d = ( own_write_data *)数据;  
        size_t  total =大小* nmember ;
 
        如果 ( d- > buf == NULL ) {
                d- > buf =  malloc ( total );
                如果 ( d- > buf == NULL ) {
                        返回 0 ;
                }
                d- > size = total ;
                memcpy ( d- > buf , ptr , total );
        }  其他 {
                d- > buf =重新 分配( d- > buf , d- > size + total );
                如果 ( d- > buf == NULL ) {
                        返回 0 ;
                }
                memcpy ( d- > buf + d- > size , ptr , total );
                d- > size + = total ;
        }
 
        回报总额;
}
 
无效 *  init () { return & own_writefunc ;
}

注意此处的初始函数,因为在PHP FFI中,就目前的版本(2020-03-11)我们没有办法直接获得一个函数指针,所以我们定义了这个函数,返回own_writefunc的地址。


最后我们定义上面用到的头文件write.h:

#定义 FFI_LIB “write.so”
 
typedef  struct _writedata {  
        无效 * buf ;
        size_t 大小;
} own_write_data ;
 
无效 * init ();

注意到我们在头文件中也定义了FFI_LIB,这样这个头文件就可以同时被write.c和接下来我们的PHP FFI共同使用了。

然后我们编译write函数为一个动态库:

gcc -O2 -fPIC -shared -g write.c -o write.so

好了,现在整个的代码会变成:

<?php
const CURLOPT_URL =  10002 ;
const CURLOPT_SSL_VERIFYPEER =  64 ;
const CURLOPT_WRITEDATA =  10001 ;
const CURLOPT_WRITEFUNCTION =  20011 ;
 
$ libcurl  = FFI :: load (“ curl.h” );
$ write   = FFI :: load (“ write.h” );
 
$ url  =  “ https://www.laruence.com/2020/03/11/5475.html” ;
 
$ data  =  $ write- > new (“ own_write_data” );
 
$ ch  =  $ libcurl- > curl_easy_init ();
 
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER , 0 );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEDATA , FFI :: addr ($ data )); 
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEFUNCTION , $ write- > init ());
$ libcurl- > curl_easy_perform ($ ch );
 
$ libcurl- > curl_easy_cleanup ($ ch );
 
ret = FFI :: 字符串($ data- > buf , $ data- > size );


此处,我们使用FFI :: new($ write-> new)来分配了一个结构_write_data的内存:

函数 FFI :: 新(混合$ type [, bool $ own  = true [, bool $ persistent  = false ]]): FFI \ CData

$ own表示这个内存管理是否采用PHP的内存管理,有时的情况下,我们申请的内存会经过PHP的生命周期管理,不需要主动释放,但是有的时候你也可能希望自己管理,那么可以设置$ own为flase,那么在适当的时候,你需要调用FFI :: free去主动释放。

然后我们把$ data作为WRITEDATA传递给libcurl,这里我们使用了FFI :: addr来获取$ data的实际内存地址:

静态 函数 地址( FFI \ CData $ cdata ): FFI \ CData ;


然后我们把own_write_func作为WRITEFUNCTION传递给了libcurl,这样再有返回的时候,libcurl就会调用我们的own_write_func来处理返回,同时会把write_data作为自定义参数传递给我们的替代函数。

最后我们使用了FFI :: string来把一段内存转换成PHP的string:

静态 函数 FFI :: 字符串( FFI \ CData $ src  [, int $ size ]):字符串

当不提供$ size的时候,FFI :: string会在遇到Null-byte的时候停止。

好了,跑一下吧?

然而毕竟直接在PHP中每次请求都加载so的话,会是一个很大的性能问题,所以我们也可以采用preload的方式,这种模式下,我们通过opcache.preload来在PHP启动的时候就加载好:

ffi.enable = 1
opcache.preload = ffi_preload.inc

ffi_preload.inc:

<?php
FFI :: load (“ curl.h” );
FFI :: load (“ write.h” );


但我们引用加载的FFI呢?因此我们需要修改一下这俩个.h头文件,加入FFI_SCOPE,比如curl.h:

#定义 FFI_LIB “libcurl.so”
#定义 FFI_SCOPE “的libcurl”
 
无效 * curl_easy_init ();
int  curl_easy_setopt (void  * curl , int选项, ...);
int  curl_easy_perform (void  * curl );
void  curl_easy_cleanup (void  * handle );

对应的我们给write.h也加入FFI_SCOPE为“ write”,然后我们的脚本现在看起来应该是这样的:

<?php
const CURLOPT_URL =  10002 ;
const CURLOPT_SSL_VERIFYPEER =  64 ;
const CURLOPT_WRITEDATA =  10001 ;
const CURLOPT_WRITEFUNCTION =  20011 ;
 
$ libcurl  = FFI :: 范围(“ libcurl” );
$ write   = FFI :: 范围(“ write” );
 
$ url  =  “ https://www.laruence.com/2020/03/11/5475.html” ;
 
$ data  =  $ write- > new (“ own_write_data” );
 
$ ch  =  $ libcurl- > curl_easy_init ();
 
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_URL , $ url );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_SSL_VERIFYPEER , 0 );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEDATA , FFI :: addr ($ data )); 
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT_WRITEFUNCTION , $ write- > init ());
$ libcurl- > curl_easy_perform ($ ch );
 
$ libcurl- > curl_easy_cleanup ($ ch );
 
ret = FFI :: 字符串($ data- > buf , $ data- > size );

也就是,我们现在使用FFI :: scope来代替FFI :: load,引用对应的函数。

静态 函数 范围(字符串$ name ): FFI ;

然后还有另外一个问题,FFI虽然给了我们很大的规模,但是毕竟直接调用C库函数,还是非常具有风险性的,我们应该只允许用户调用我们确认过的函数,于是,ffi.enable = preload就该上场了,当我们设置ffi.enable = preload的话,那就只有在opcache.preload的脚本中的函数才能调用FFI,而用户写的函数是没有办法直接调用的。

我们稍微修改下ffi_preload.inc变成ffi_safe_preload.inc

<?php
CURLOPT 类{
     const URL =  10002 ;
     const SSL_VERIFYHOST =  81 ;
     const SSL_VERIFYPEER =  64 ;
     const WRITEDATA =  10001 ;
     const WRITEFUNCTION =  20011 ;
}
 
FFI :: load (“ curl.h” );
FFI :: load (“ write.h” );
 
函数 get_libcurl () : FFI {
     返回 FFI :: 范围(“ libcurl” );
}
 
函数 get_write_data ($ write ) : FFI \ CData {
     返回 $ write- > new (“ own_write_data” );
}
 
函数 get_write () : FFI {
     返回 FFI :: 范围(“ write” );
}
 
函数 get_data_addr ($ data ) : FFI \ CData {
     返回 FFI :: addr ($ data );
}
 
函数 paser_libcurl_ret ($ data ) :字符串{
     返回 FFI :: 字符串($ data- > buf , $ data- > size );
}

也就是,我们把所有会调用FFI API的函数都定义在preload脚本中,然后我们的示例会变成(ffi_safe.php):

<?php
$ libcurl  =  get_libcurl ();
$ write   =   get_write ();
$ data  =  get_write_data ($ write );
 
$ url  =  “ https://www.laruence.com/2020/03/11/5475.html” ;
 
 
$ ch  =  $ libcurl- > curl_easy_init ();
 
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: URL , $ url );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: SSL_VERIFYPEER , 0 );
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: WRITEDATA , get_data_addr ($ data ));复制代码
$ libcurl- > curl_easy_setopt ($ ch , CURLOPT :: WRITEFUNCTION , $ write- > init ());
$ libcurl- > curl_easy_perform ($ ch );
 
$ libcurl- > curl_easy_cleanup ($ ch );
 
$ ret  =  paser_libcurl_ret ($ data );

这样一来通过ffi.enable = preload,我们就可以限制,所有的FFI API只能被我们可控制的preload脚本调用,用户不能直接调用。从而我们可以在这些函数内部做好适当的安全保证工作,从而保证一定的安全性。


]]>
2020-03-30
<![CDATA[RabbitMQ的持久化]]> http://www.cnqigong.com/post/7194/


RabbitMQ的持久化主要体现在三个方面,即交换机持久化,队列持久化及消息持久化

注意,因公司使用php-amqplib来实现RabbitMQ,故之后举例说明的代码均使用的php-amqplib,而非php的amqp扩展

盛京棋牌

交换机的持久化其实就是相当于将交换机的属性在服务器内部保存,当MQ的服务器发生意外或关闭之后,重启RabbitMQ时不需要重新手动或执行代码去建立交换机,交换机会自动建立,相当于一直存在。

创建交换机的方法为exchange_declare($exhcange_name,$type,$passive,$durable,$auto_delete);,当$durable这个参数为true时,该交换机就会被存储到内存里,当RabbitMQ服务器重启时,会将该交换机自动重新创建,如果为false,重启后该交换机则会被从交换机队列里删掉。

v2-73fe3eb4c6d15118d9f880954bf6ae18_720w.jpg

盛京棋牌

队列持久化类似于交换机持久化,创建队列方法queue_declare中也有一个参数是$durable,也代表着RabbitMQ服务器重启时,是否自动创建队列,图中参数注释与方法中的参数(除队列名外)顺序一一对应

盛京棋牌

3、消息持久化

众所周知,RabbitMQ的消息是依附于队列存在的,所以想要消息持久化,那么前提是队列也要持久化。

消息的持久化与交换机持久化与队列持久化有所不同,消息的持久化在于创建消息的时候,加一个持久化消息的属性,创建消息的方法是new AMQPMessage($data,$properties),其中$properties是个数组,里面可以设置对消息的各种属性,如持久化,优先级等属性。

持久化的key值为"delivery_mode",当"delivery_mode"为1时表示消息不持久化,为2时则表示消息持久化,且把消息存在磁盘里

v2-b19dad6be1d7715e0f4c1760df985d0b_720w.jpg

你也注意到了客户端的参数配置从某种程度上讲也是方便你的IDE易于搜索。

$client 对象下的所有核心方法(索引,搜索,获取等)都是可用的。

索引管理和集群管理分别在 $client->indices() 和 $client->cluster() 中。

]]>
2020-03-30