您的位置:首页 > 编程语言 > PHP开发

PHP 如何发起异步请求

2013-11-19 10:02 232 查看
有人说,限制激发创造力。如果真这样,PHP就是成熟的创造性解决方案。我刚上周构建了调用Segment.io的API的PHP库,发现了各种不同的方法可以提高服务端请求性能。

设计客户端类向API发送数据时,我们的首要任务之一就是保证我的代码不影响到你的核心程序。这是很棘手的,尤其是使用单线程,无共享的语言,如PHP。

服务商PHP安装方式很多,让问题更复杂。幸运的,你的服务商允许你创建进程,写入文件和安装自己的扩展。不幸的话,你就得和一些纠结的邻居分享同一个安装配置,只能上传文件。

理想状态,我们喜欢用最小的满足实现各种情况。当运行PHP时(可能就一两个脚本),你应该深入理解这些。

我们尝试用三种主要方法实现PHP发出请求,以下就是。

一:快速打开一个套接字(Socket)

搜索PHP异步请求,最先的结果都是相同的方法:写一个Socket然后在等待返回前关闭它。

这个想法是开启一次连接到服务端,连接好就写入内容。Socket写入是很快的,而且你不要返回信息,写入后直接关闭连接。这就节省了等待一次往返的事件。

但是当你看StackOverflow上的评论,Socket到底发生了什么有一些争论。也让我疑问:Socket怎么实现的异步?

下面是我们的Socket实现:

<?php

private function request($body) {
    $protocol ="ssl";    $host ="api.segment.io";    $port =443;    $path ="/v1/" .$body;    $timeout =$this->options['timeout'];    try {

      # Open our socket to the API Server.

      $socket =fsockopen($protocol ."://" .$host,$port,                          $errno,$errstr,$timeout);      # Create the request body,and make the request.

      $req =$this->create_body($host,$path,$content);      fwrite($socket,$req);      # ...
    }catch (Exception $e) {
      # ...
    }

}

?>最初的结果并不乐观。一次fsockopen花了300毫秒,偶尔更长。

 

事实证明,fsockopen是阻塞的——不是异步的!要了解到底发生了什么,需要深入研究fsockopen是怎么工作。当fsockopen选择协议时,需要考虑使用哪种socket。这个过程在连接完成前是阻塞的

复习一下,internet的基本协议是TCP。它使电脑之间的信息传递可靠并有序。几乎所有HTTP都运行于TCP上。我们用HTTP来简化自定义的客户端使用。

这是TCP Socket创建连接

客户端发送SYN消息给服务端
服务端返回SYN-ACK消息确认包
客户端发送最终ACK包及传送数据
作为计时的部分,这是传输数据之前完成的完整来回,在fsockopen之前这就已经返回。一旦连接开启,我们可以为socket写入数据。通常,需要30-100ms连接到我们的服务器。

TCP连接比较快,罪魁祸首是SSL需要的额外握手。SSL也实现在TCP上。TCP握手后又开始TLS握手

光SSL连接就需要三次握手,更不要说加上创建公共密匙的时间。

浏览器的SSL连接可以共享密匙,避免允许访问的客户端和服务端重复握手。可是PHP执行的Socket无法共享密匙,我们只能每次都是重新连接。

还可以使用socket_set_nonblock创建“非阻塞”的Socket。不过这是在打开Socket的时候不阻塞,你还是要等待完成才能写入内容。如果精确考虑打开Socket写入数据的时间,页面加载会慢约100ms。

总结起来:

Socket可以在有权限限制的PHP上运行
fscokopen是阻塞的,即使不阻塞Socket也需要等待再写入数据
SSL连接明显减慢连接,因为额外的握手和加密过程
打开连接使页面延迟100ms

二,写日志文件

如果你没有其他系统权限的时候,Sokets是非常棒的方案。这儿我们介绍一种在性能上更好的方法,那就是把所有事件以日志方式写到文件。这个日志文件可以被工作进程或者cron做"带外"处理。

基于文件方法的优点是具有最小的API对外请求。当php代码发出track 或者identify请求的时候,通过这种方法工作进程可以同时处理100个事件的请求,而不是仅一个请求。

这种方法的另外一个优点是php进程可以相对更快的记录文件,一个写操作往往只需要几毫秒。当php打开一个文件句柄的时候,用fwrite进行追加写是很简单的操作。由于纯php不具有“共享内存队列”机制,在这日志文件实际上和“共享内存队列”具有异曲同工的效果。

为了读日志文件,我利用analytics-pythonlibrary库写了一个python上传脚本。为了防止日志文件太大,脚本自动进行更名操作。可以动态的写php文件,还可以写内存中的文件句柄,在老请求创建的地方,新请求会创建一个新的日志文件。

这种方法没有太多的逻辑,只要开发者多写点cron任务,并且通过PyPI分别安装我们的python库。(这两段感觉是在给他们的python库做公告,既然python那么好,用php干嘛,bs)

方法总结(关键点):

写文件较快,系统资源开销少。
需要消耗磁盘空间,要求守护进程对文件有写权限。
必须运行工作进程处理带外的记录消息。

三:调用Curl过程

还有一个可选择的方法,我们可以通过exec 操作curl工具来发出请求。curl请求才可以做为独立进程一部分来完成,允许php代码继续执行,而不会阻塞socket连接。

这种方法的性能介于前面两种方法之间,比soket方法快,比写文件的方法花费更少的系统资源。

操作 forkd curl 方法,最简单的例子如下:

view source

print?

01
<?php
02
private
function
request(
$url
,
$payload
) {

03
 
 
04
  
$cmd

=
"curl -X POST -H'Content-Type: application/json'"
;
05
  
$cmd
.=
" -d '"
.
$payload

.
"' "

.
"'"
.
$url

.
"'"
;
06
 
 
07
  
if

(!
$this
->debug()) {
08
    
$cmd

.=
" > /dev/null 2>&1 &"
;
09
  
}

10
 
 
11
  
exec
(
$cmd
,
$output
,

$exit
);
12
  
return

$exit
==0; 
13
}
14
?>
如果运行在生产模式,我们不希望等着fork进程的消息输出。所以代码中加添了"> /dev/null 2>&1 &"让进程正确的执行 ,而把任何可能输出都丢弃掉。

同样功能的shell脚本如下:

view source

print?

1
curl -X POST -H
'Content-Type: application/json'

\
2
  
-d '{
"batch"
:[{
"secret"
:
"testsecret"
,
"userId"
:
"some_user"
,
3
"event"
:
"PHP Fork Queued Event"
,
"properties"
:null,
"timestamp"
:
4
"2013-01-30T14:34:50-08:00"
,
"context"
:{
"library"
:
"analytics-php"
},
5
"action"
:
"track"
}],
"secret"
:
"testsecret"
}'
\
6
  
'https://api.segment.io/v1/import'

> /dev/null 2>&1 &
脚本花费了大概1秒多一点的时间,占用大约4k的的常驻内存。而curl进程用了 标准SSL 300毫秒完成请求,exce调用立刻相应php程序。这使得服务页面能很快相应用户。

笔者用一台一般水平的机器试验,这种方法curl可以每秒响应100个左右https请求,而没有任何的内存开销。如果不用SSL,响应的请求会更多。

不用等待输入,Fork一个进程非常快。

curl花费了和socket同样时间响应一个请求,但是这个外带的过程。

调用curl需要仅仅普通的unix基础。

Fork发起一个简单的请求,只需要几毫秒的时间,但是大量的同步调用(forks)会导致系统变慢。

 

使用析构函数减少出栈请求

虽然不是一个异步请求的方法,但是我们可以用析构函数帮助我们进行批量API请求。

为了减少请求的数量,我们首先将他们放在内存中,然后对他们进行批处理。如果不适用运行时扩展,他们只能在一个单一的PHP脚本中运行。要做到这一点,我们首先初始化一个队列,在程序脚本运行结束时,将所有队列请求批量发送出去。

view source

print?

01
<?php
02
class
Analytics_SomeConsumer {
03
 
 
04
  
public

function
__construct() {
05
    
$this
->socket =socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
06
    
socket_set_nonblock(
$this
->socket);
07
    
socket_connect(
$this
->socket,
$this
->host,

$this
->port);
08
    
$this
->queue =
array
();
09
  
}

10
 
 
11
  
public

function
__destruct() {

12
    
$payload

=json_encode(
$this
->queue);
13
    
# ...
// wait for socket to be writeable
14
    
socket_write(
$this
->socket,
$payload
);

15
    
socket_close(
$this
->socket);
16
  
}

17
 
 
18
  
public

function
track(
$item
) {
19
    
array_push
(
$this
->queue,
$item
);
20
  
}

21
?>
队列中的对象创建后,当它被销毁时将队列进行刷新,这样保证了队列在每次请求时只刷新一次。

另外,当PHP解释器忙着来渲染页面而我们等待实际写入套接字时,我们可以以非阻塞的方式在构造函数中创建套接字,然后写入析构函数,这样可以预留更多的时间来建立连接。

抉择?

最完美的方法是用纯php实现,而不是调用其他进程,这也是响应请求保守做法。我们更趋向于开发者最方便,不会把精力分散在其他地方。

实际中,这往往是不可触及的。基于处理的问题的大小,以及系统的限制,以上每种方法都是有缺点和限制。由于简单的方法不可能满足实际中的用户状况,我们创建不同的适配器以支持不同用户的不同需求。

我们以调用curl方法做为基础,调用一个进程不会导致重大的页面性能负担,同时他还支持扩展到每属主每秒处理多请求。请求的数量通过usinglimits.conf严格限制。

高并发用户或者拥有高系统权限的用户可以实用日志文件系统。系统权限受限的用户(虚拟空间等)可以使用sockets方法。

最后,需要你去了解一下实际中你能拥有的系统限制和系统的负载情况。这些都最终决定你选择更合适的方法。

如果你对Segment.io 的php开发感兴趣?请访问资源documentation 或者 访问我们的github项目github repo

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: