<?php

Archive for the ‘php’ Category

fsockopen VS pfsockopen

前几天cyberty说起pfsockopen,我还不是很了解,大家平时用的最多的应该是fsockopen,事实上我基本没用过pfsockopen

那么这两个函数有什么区别呢?先看一下手册上的介绍:

resource fsockopen ( string target [, int port [, int &errno [, string &errstr [, float timeout]]]] )
resource pfsockopen ( string hostname [, int port [, int &errno [, string &errstr [, float timeout]]]] )

两函数的参数完全一致,从手册上能看出来的区别只是一句话It is the persistent version of fsockopen().

或许此时你想到了mysql_connect 和 mysql_pconnect,经验告诉我们mysql_pconnect并没有预期的效果。那么psocketopen会是什么效果呢?

下面我做了几组测试,每组测试时反复50次连接:
socket

给一些说明: 第一行的1~5代表测试次数的序号,fsockopen 和 pfsockopen在各种情况分别测试了5次;其他数字是执行时间(以秒为单位)。
从测试结果来看pfsockopen在同一次请求中大量反复使用的连接具有复用效果,但是在需要获取内容时表现不佳。

总体来看区别不是很大,或许需要更大量的测试数据来支持这一结论。

下面是测试代码,根据需要改动注释代码:

ini_set("display_errors", "On");
 error_reporting(E_ALL);
 set_time_limit(600);
 $t1 = microtime(true);
 $err = 0;

 echo "[code]\n";
 $func = isset($_GET['p']) ? "pfsockopen" : "fsockopen";
 for ($i = 0; $i < 50; $i++) {
	$fp = $func("www.163.com", 80);
	if(!is_resource($fp)) {
		echo "conn error\n";
		$err++;
		continue;
	}
	fwrite($fp, "GET / HTTP/1.1\r\n\r\n");
	$s = stream_get_contents($fp);
//	fclose($fp);
	echo strlen($s);
	echo "\n";
	flush();
 }
 $t2 = microtime(true);
 echo "time: ".($t2-$t1)."\n";
 echo "err: ".$err;

解决cacti大量sleep连接占用mysql资源的问题

最近发现msyql的进程数经常保持在20个并发连接,感觉非常奇怪。show processlist 之后才发现原来全都是cacti的连接,而且全部是sleep状态。因此考虑到有可能是连接数据库时使用了持久连接,或mysql_connect 的bool new_link 参数为false了。
接下来就有了查找目标,首先进入到cacti程序的根目录。

[root@localhost htdocs]# cd cacti
[root@localhost cacti]# grep -r mysql_pconnect .
./lib/adodb/drivers/adodb-mysql.inc.php:                        $this->_connectionID = @mysql_pconnect($argHostname,$argUsername,$argPassword,$this->clientFlags);
./lib/adodb/drivers/adodb-mysql.inc.php:                        $this->_connectionID = @mysql_pconnect($argHostname,$argUsername,$argPassword);

接下来再去找./lib/adodb/drivers/adodb-mysql.inc.php文件内查找mysql_pconnect找到所属的方法名。我这里找到的是_pconnect
然后再查找:

grep -rw _pconnect .
./lib/adodb/drivers/adodb-sqlite.inc.php:       function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-sybase.inc.php:       function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-oracle.inc.php:               function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-ado.inc.php:  function _pconnect($argHostname, $argUsername, $argPassword, $argProvider='MSDASQL')
./lib/adodb/drivers/adodb-postgres64.inc.php:  27 Nov 2000 jlim - added changes to _connect/_pconnect from ideas by "Lennie" <leen@wirehub.nl>
./lib/adodb/drivers/adodb-postgres64.inc.php:   function _pconnect($str,$user='',$pwd='',$db='')
./lib/adodb/drivers/adodb-csv.inc.php:  function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-mssql.inc.php:        function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-mysqli.inc.php:       function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-ibase.inc.php:        function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-pdo.inc.php:  function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-oci8.inc.php: function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-fbsql.inc.php:        function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-mysql.inc.php:        function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-ado5.inc.php: function _pconnect($argHostname, $argUsername, $argPassword, $argProvider='MSDASQL')
./lib/adodb/drivers/adodb-odbc.inc.php: function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-odbc_oracle.inc.php:  function _pconnect($argDSN, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/drivers/adodb-odbtp.inc.php:        function _pconnect($HostOrInterface, $UserOrDSN='', $argPassword='', $argDatabase='')
./lib/adodb/drivers/adodb-informix72.inc.php:   function _pconnect($argHostname, $argUsername, $argPassword, $argDatabasename)
./lib/adodb/adodb.inc.php:              if ($this->_pconnect($this->host, $this->user, $this->password, $this->database)) return true;

这次会找到很多结果 看上去有点无从下手。没关系,看一看文件名就知道了,这些基本上都是针对其他数据库的代码,与msyql无关。
注意,其中的./lib/adodb/adodb.inc.php,没错,这个就是我们要找的。
在该文件内查找包含_pconnect的函数名,只有一个函数:PConnect
再接下来就是查找调用PConnect函数的地方:

[root@localhost cacti]# grep -rw PConnect .
./lib/adodb/adodb-pear.inc.php:         if($persist) $ok = $obj->PConnect($dsninfo['hostspec'], $dsninfo['username'],$dsninfo['password'],$dsninfo['database']);
./lib/adodb/drivers/adodb-postgres64.inc.php:   //      $db->PConnect("host=host1 user=user1 password=secret port=4341");
./lib/adodb/drivers/adodb-postgres64.inc.php:   //      $db->PConnect('host1','user1','secret');
./lib/adodb/drivers/adodb-odbc.inc.php:                 ADOConnection::outp("For odbc PConnect(), $argDatabasename is not used. Place dsn in 1st parameter.");
./lib/adodb/adodb.inc.php:      var $autoRollback = false; // autoRollback on PConnect().
./lib/adodb/adodb.inc.php:      function PConnect($argHostname = "", $argUsername = "", $argPassword = "", $argDatabaseName = "")
./lib/adodb/adodb.inc.php:                                      $ok = $obj->PConnect($dsna['host'], $dsna['user'], $dsna['pass'], $dsna['path']);
./lib/database.php:             if ($cnn_id->PConnect($hostport,$user,$pass,$db_name)) {

OK, 这下找到我们的终极目标了:./lib/database.php
打开该文件浏览一遍代码,马上就会发现cacti默认使用的是PConnect, 至于有没有地方可以配置选择 我还不太清楚,因为没有使用长连接的需求,干脆直接改掉这里吧。把PConnect改成Connect,保存退出。
最后就是等待观察,经过十几分钟的观察一切正常了,再也看不到令人厌烦的sleep连接啦~
oh my lady gaga…
收兵

系统监控配置之cacti+rrdtool+netsnmp

一,安装rrdtool 官网: http://oss.oetiker.ch/rrdtool/

/usr/bin/wget http://oss.oetiker.ch/rrdtool/pub/rrdtool-1.4.2.tar.gz
/bin/tar xzf rrdtool-1.4.2.tar.gz
cd rrdtool-1.4.2
./configure --prefix=/home/rrdtool

提示以下warning信息:
configure: WARNING:
—————————————————————————-
* I found a copy of pkgconfig, but there is no pangocairo.pc file around.
  You may want to set the PKG_CONFIG_PATH variable to point to its
  location.
—————————————————————————-

configure: WARNING:
—————————————————————————-
* I could not find a working copy of pangocairo. Check config.log for hints on why
  this is the case. Maybe you need to set LDFLAGS and CPPFLAGS appropriately
  so that compiler and the linker can find libpango-1.0 and its header files. If
  you have not installed pangocairo, you can get it either from its original home on

     http://ftp.gnome.org/pub/GNOME/sources/pango/1.17

  You can find also find an archive copy on

     http://oss.oetiker.ch/rrdtool/pub/libs

  The last tested version of pangocairo is 1.17.

       LIBS=-lm  -lglib-2.0 
   LDFLAGS= -L/lib64   
  CPPFLAGS= -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include 

—————————————————————————-
               
checking for xmlParseFile in -lxml2… yes
checking libxml/parser.h usability… yes
checking libxml/parser.h presence… yes
checking for libxml/parser.h… yes
configure: error: Please fix the library issues listed above and try again.

信息表明缺了pango的头文件,因此要先安装pango-devel

/usr/bin/yum install pango-devel

接下来重新编译

./configure --prefix=/home/rrdtool

这次编译通过:
Config is DONE!

make && make install

做一个软连接

/bin/ln -s /home/rrdtool/bin/* /usr/local/bin/
cd ..

这样就完成了rrdtool的安装,

 

二,安装net-snmp 官网: http://www.net-snmp.org/

先下载net-snmp的最新版本5.5

/usr/bin/wget http://downloads.sourceforge.net/project/net-snmp/net-snmp/5.5/net-snmp-5.5.tar.gz?use_mirror=nchc
/bin/tar xzf net-snmp-5.5.tar.gz
cd net-snmp-5.5
./configure #一切按照默认值来编译(默认安装到/usr/loca/)
make && make install

运行以下snmpget,检查是否安装成功

/usr/loca/bin/snmpget

我这里就遇到了问题,提示信息是:
/usr/local/bin/snmpget: error while loading shared libraries: libnetsnmp.so.20: cannot open shared object file: No such file or directory
先通过ldd查看snmpget需要加载哪些库

[root@test_server net-snmp-5.5]# /usr/bin/ldd /usr/local/bin/snmpget
        libnetsnmp.so.20 => not found
        libcrypto.so.6 => /lib64/libcrypto.so.6 (0×0000003c1fc00000)
        libc.so.6 => /lib64/libc.so.6 (0×0000003c1d000000)
        libdl.so.2 => /lib64/libdl.so.2 (0×0000003c1d400000)
        libz.so.1 => /usr/lib64/libz.so.1 (0×0000003c1ec00000)
        /lib64/ld-linux-x86-64.so.2 (0×0000003c1cc00000)
可见需要加载的库文件大都放在/lib64/下的;
再查一下libnetsnmp.so.20是在什么位置

/usr/bin/whereis libnetsnmp.so.20
libnetsnmp.so: /usr/local/lib/libnetsnmp.so /usr/local/lib/libnetsnmp.so.20

ok, 至此我们就知道怎么解决问题了:

/bin/ln -s /usr/local/lib/libnetsnmp.so.20 /lib64/libnetsnmp.so.20

再执行一次snmpget

/usr/local/bin/snmpget
Created directory: /var/net-snmp
Created directory: /var/net-snmp/mib_indexes
No hostname specified.
USAGE: snmpget [OPTIONS] AGENT OID [OID]...

  Version:  5.5
  Web:      <a href="http://www.net-snmp.org/">http://www.net-snmp.org/</a>
  Email:    <a href="mailto:net-snmp-coders@lists.sourceforge.net">net-snmp-coders@lists.sourceforge.net</a>

OPTIONS:
  -h, --help            display this help message
...

一切OK

三, 安装cacti 官网: http://www.cacti.net/
值得注意的是cacti的环境需求:
RRDTool 1.0.49 or 1.2.x or greater
MySQL 4.1.x or 5.x or greater
PHP 4.3.6 or greater, 5.x greater highly recommended for advanced features
A Web Server e.g. Apache or IIS

先确认是否满足,然后:

cd ..
/usr/bin/wget http://www.cacti.net/downloads/cacti-0.8.7e.tar.gz
/bin/tar xzf cacti-0.8.7e.tar.gz
/bin/mv cacti-0.8.7e cacti

然后就可以把cacti转移到web目录下

/bin/mv cacti /data/htdocs/cacti
cd /data/htdocs/cacti
/home/mysql/bin/mysql cacti &lt; cacti.sql #导入数据库结构
/bin/chown www:www -R .
/bin/vi include/config.php

修改数据库连接信息:
$database_default = “cacti”;
$database_hostname = “localhost”;
$database_username = “root”;
$database_password = “”;
$database_port = “3306″;

增加后台任务,每两分钟搜集一次数据

*/5 * * * * /usr/local/bin/php /usr/local/apche2/htdocs/cacti/poller.php > /dev/null 2>&1

接下来就从前台访问cacti的安装目录
http://zhys9.com/cacti/install/
按照以下图片中的标红提示完成配置

1

2

3
最后通过默认账号密码: admin/admin 登录,然后系统会提示修改密码;

 

接下来的事情就只是用了 ;)

centos下nginx+php+mysql的自动安装SHELL

今天安装了一下新的测试环境,因为不想反复输入命令。。也就有了本文章。

打算找一找自动安装的脚本,一键安装自然是追求自动化的崇高追求。。gg后发现真的有不少,于是找了一个看了看,没错!是我要的。

因为安装目录、配置方面有些不同于是拿来修改一下就可以方便使用了~

修改后的脚本:lnmp-auto-install

安装目录是:
nginx: /home/nginx
php: /home/php
mysql: /home/mysql
eaccelerator: /home/eaccelerator

至于版本号就不说了吧,基本都是比较新的稳定版本了。

启动nginx:  /etc/init.d/nginx start
启动php-fpm: /home/nginx/sbin/php-fpm start
启动mysql: /etc/init.d/mysql start

感谢原作者Licess’s,以及提供镜像下载的vpser

万恶的BOM头

之前就有同事遇到类似问题,cookie无法设置成功(关掉了错误信息),或者是redirect、session失效,其实都是BOM在捣鬼。。一般产生BOM的途径大多是使用记事本改变文件的内码所致,因此在修改文件内码时不要使用记事本。

如果是打开错误信息,在setcookie的时候出现下面错误提示:

Cannot modify header information - headers already sent by ….

熟悉setcookie函数的话一看便知道是执行setcookie之前数据有输出,但是大部分情况下执行动作的PHP都是include若干个文件,一行行排查是很麻烦的事情,有两个解决办法:
一是:
ob_start();
在setcookie之前加上代码:
ob_get_clean();
这是为了获取setcookie之前页面输出的内容。页面运行后,显示输出了一个空字符串,也就是说,setcookie之前没有任何输出。

二是:
最彻底的方法,查找加载进来的文件,用十六进制编辑器可以看到包含BOM头的文件是以“ef bb bf”开头的,这个正是导致上面错误的来源,删掉、保存、重新测试,如果还有问题,再检查其他文件。。直到一切正常

BOM是什么意思?
BOM是“Byte Order Mark”的缩写,用于标记文件的编码。并不是所有的文本编辑工具都能识别BOM标记

PHP性能的血汗工厂

作为PHP性能血汗工厂的老板的你,打算如何榨干它的性能?最常用的数组操作、字符串操作需要注意的地方?给大家推荐一个网站,列举了一些常用代码不同写法的性能区别。

推荐看以下5部分:

  1. 字符串输出;echo vs. print
  2. 读循环;foreach() vs. for() vs. while(list() = each())
  3. 写循环;foreach() vs. for vs. while(list() = each())
  4. 变量类型检查;isSet() vs. empty() vs. is_array()
  5. 别名的使用;Using the &-ref-operator

http://www.phpbench.com/

ps: 每次刷新各项数据会重新计算,推荐firefox查看IE下样式有问题

foreach使用&引用赋值时要注意的问题

下文是同事整理出来的,以前也有遇到过。既然整理的很完善了就转帖过来共勉。

解决办法由几种,推荐:

unset 或者是 换用
foreach($arr as $k=>$value){//习惯用$value或$val
$arr[$k] .= ‘4′;
}

下转:

foreach 通过在 $value 之前加上 & 很容易就能修改数组的单元,如:

foreach($arr as &$value){
    
$value .= 4;
}

但这个用法也很容易造成错误,商务空间就发现有这个问题,而且也不容易找。
看例子更直截了当:

<?php
$arr = array(a,b,c);
$arr2 = array(d, e, f);
 
foreach($arr as &$value){//习惯用$value或$val
    
$value .= 4;
}
 
//都处理完毕我们在页面模版输出,首先输出$arr2
foreach($arr2 as $value){//习惯用$value或$val
    
//echo $value;
}
//然后再这样输出 $arr;
foreach($arr as $value){//习惯用$value或$val
    
echo $value, \n;
}
?>

大家看看输出的结果是否和期望的一样。这里结果是:

a4
b4
b4

结果和我期待的不一样,这个就是引用引起的问题。
在 foreach($arr as &$value) 数组遍历到最后,引用关系并没有断开,这是等同于 $value 与 $arr 的最后一个单元即 $arr[2] 引用。

再到 foreach($arr2 as $value),$value的值一直随数组单元的值变,由于引用关系没有断开,$arr[2] 的值也跟着变化。一直到$arr2遍历完,这是$value的值为f,所以$arr[2]值也为f。
这时$arr的值应为:

Array
(
[0] => a4
[1] => b4
[2] => f
)

这个和我们看到的最终输出结果又不一样。再到 foreach($arr as $value),同理,这时 $arr[2]的值也随着 $value 变化,当遍历到key为1,即$arr[1]时,$arr[2]的值也变成 $arr[1] 的值,即是

b4。然后再遍历到key为2时 $arr[2]的值又成了 $arr[2] 的值,也就是 b4 了。就是输出时的结果。

所以在 foreach 使用引用时要注意了。也可以在处理完后立即断开引用关系,后面就不会有上述情况了。

foreach($arr as &$value){
    
$value .= 4;
}
unset($value);

[Q]Comet:基于 HTTP 长连接的“服务器推”技术

以下是从developerWorks转载过来的,原文地址:http://www.ibm.com/developerworks/cn/web/wa-lo-comet/

主要是讲述基于常连接的数据交互方式,常用比如聊天,web game,等等需要交互大量即时数据的bs架构的应用。当然能实现这个功能的方法有多种,本文主要介绍的只是其中之一。

==

传统模式的 Web 系统以客户端发出请求、服务器端响应的方式工作。这种方式并不能满足很多现实应用的需求,譬如:

  • 监控系统:后台硬件热插拔、LED、温度、电压发生变化;
  • 即时通信系统:其它用户登录、发送信息;
  • 即时报价系统:后台数据库内容发生变化;

这些应用都需要服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求。“服务器推”技术在现实应用中有一些解决方案,本文将这些解决方案分为两类:一类需要在浏览器端安装插件,基于套接口传送信息,或是使用 RMI、CORBA 进行远程调用;而另一类则无须浏览器安装任何插件、基于 HTTP 长连接。

将“服务器推”应用在 Web 程序中,首先考虑的是如何在功能有限的浏览器端接收、处理信息:

  1. 客户端如何接收、处理信息,是否需要使用套接口或是使用远程调用。客户端呈现给用户的是 HTML 页面还是 Java applet 或 Flash 窗口。如果使用套接口和远程调用,怎么和 JavaScript 结合修改 HTML 的显示。
  2. 客户与服务器端通信的信息格式,采取怎样的出错处理机制。
  3. 客户端是否需要支持不同类型的浏览器如 IE、Firefox,是否需要同时支持 Windows 和 Linux 平台。

 


 

回页首

基于客户端套接口的“服务器推”技术

Flash XMLSocket

如果 Web 应用的用户接受应用只有在安装了 Flash 播放器才能正常运行, 那么使用 Flash 的 XMLSocket 也是一个可行的方案。

这种方案实现的基础是:

  1. Flash 提供了 XMLSocket 类。
  2. JavaScript 和 Flash 的紧密结合:在 JavaScript 可以直接调用 Flash 程序提供的接口。

具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。

关于如何去构建充当了 JavaScript 与 Flash XMLSocket 桥梁的 Flash 程序,以及如何在 JavaScript 里调用 Flash 提供的接口,我们可以参考 AFLAX(Asynchronous Flash and XML)项目提供的 Socket Demo 以及 SocketJS(请参见 参考资源)。

Javascript 与 Flash 的紧密结合,极大增强了客户端的处理能力。从 Flash 播放器 V7.0.19 开始,已经取消了 XMLSocket 的端口必须大于 1023 的限制。Linux 平台也支持 Flash XMLSocket 方案。但此方案的缺点在于:

  1. 客户端必须安装 Flash 播放器;
  2. 因为 XMLSocket 没有 HTTP 隧道功能,XMLSocket 类不能自动穿过防火墙;
  3. 因为是使用套接口,需要设置一个通信端口,防火墙、代理服务器也可能对非 HTTP 通道端口进行限制;

不过这种方案在一些网络聊天室,网络互动游戏中已得到广泛使用。

Java Applet 套接口

在客户端使用 Java Applet,通过 java.net.Socketjava.net.DatagramSocketjava.net.MulticastSocket 建立与服务器端的套接口连接,从而实现“服务器推”。

这种方案最大的不足在于 Java applet 在收到服务器端返回的信息后,无法通过 JavaScript 去更新 HTML 页面的内容。

 


 

回页首

基于 HTTP 长连接的“服务器推”技术

Comet 简介

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。
图 1. 传统的 Web 应用模型与基于 AJAX 的模型之比较
图 1. 传统的 Web 应用模型与基于 AJAX 的模型之比较

“服务器推”是一种很早就存在的技术,以前在实现上主要是通过客户端的套接口,或是服务器端的远程调用。因为浏览器技术的发展比较缓慢,没有为“服务器推”的实现提供很好的支持,在纯浏览器的应用中很难有一个完善的方案去实现“服务器推”并用于商业程序。最近几年,因为 AJAX 技术的普及,以及把 IFrame 嵌在“htmlfile“的 ActiveX 组件中可以解决 IE 的加载显示问题,一些受欢迎的应用如 meebo,gmail+gtalk 在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。因为这些原因,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于 HTTP 长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。目前已经出现了一些成熟的 Comet 应用以及各种开源框架;一些 Web 服务器如 Jetty 也在为支持大量并发的长连接进行了很多改进。关于 Comet 技术最新的发展状况请参考关于 Comet 的 wiki。

下面将介绍两种 Comet 应用的实现模型。

基于 AJAX 的长轮询(long-polling)方式

图 1 所示,AJAX 的出现使得 JavaScript 可以调用 XMLHttpRequest 对象发出 HTTP 请求,JavaScript 响应处理函数根据服务器返回的信息对 HTML 页面的显示进行更新。使用 AJAX 实现“服务器推”与传统的 AJAX 应用不同之处在于:

  1. 服务器端会阻塞请求直到有数据传递或超时才返回。
  2. 客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
  3. 当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。

图 2. 基于长轮询的服务器推模型
图 2. 基于长轮询的服务器推模型

一些应用及示例如 “Meebo”, “Pushlet Chat” 都采用了这种长轮询的方式。相对于“轮询”(poll),这种长轮询方式也可以称为“拉”(pull)。因为这种方案基于 AJAX,具有以下一些优点:请求异步发出;无须安装插件;IE、Mozilla FireFox 都支持 AJAX。

在这种长轮询方式下,客户端是在 XMLHttpRequest 的 readystate 为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate 为 4 时,数据传输结束,连接已经关闭。Mozilla Firefox 提供了对 Streaming AJAX 的支持, 即 readystate 为 3 时(数据仍在传输中),客户端可以读取数据,从而无须关闭连接,就能读取处理服务器端返回的信息。IE 在 readystate 为 3 时,不能读取服务器返回的数据,目前 IE 不支持基于 Streaming AJAX。

基于 Iframe 及 htmlfile 的流(streaming)方式

iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。
图 3. 基于流方式的服务器推模型
图 3. 基于流方式的服务器推模型

上节提到的 AJAX 方案是在 JavaScript 里处理 XMLHttpRequest 从服务器取回的数据,然后 Javascript 可以很方便的去控制 HTML 页面的显示。同样的思路用在 iframe 方案的客户端,iframe 服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript 函数的调用,如“<script type=”text/javascript”>js_func(“data from server ”)</script>”。服务器端将返回的数据作为客户端 JavaScript 函数的参数传递;客户端浏览器的 Javascript 引擎在收到服务器返回的 JavaScript 调用时就会去执行代码。

图 3 可以看到,每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。

使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth’s of Google’s amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。(请参见 参考资源

 


 

回页首

使用 Comet 模型开发自己的应用

上面介绍了两种基于 HTTP 长连接的“服务器推”架构,更多描述了客户端处理长连接的技术。对于一个实际的应用而言,系统的稳定性和性能是非常重要的。将 HTTP 长连接用于实际应用,很多细节需要考虑。

不要在同一客户端同时使用超过两个的 HTTP 长连接

我们使用 IE 下载文件时会有这样的体验,从同一个 Web 服务器下载文件,最多只能有两个文件同时被下载。第三个文件的下载会被阻塞,直到前面下载的文件下载完毕。这是因为 HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞。而 IE 在实现中严格遵守了这种规定。

HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web 应用带来如下现象:在客户端如果打开超过两个的 IE 窗口去访问同一个使用了长连接的 Web 服务器,第三个 IE 窗口的 HTTP 请求被前两个窗口的长连接阻塞。

所以在开发长连接的应用时, 必须注意在使用了多个 frame 的页面中,不要为每个 frame 的页面都建立一个 HTTP 长连接,这样会阻塞其它的 HTTP 请求,在设计上考虑让多个 frame 的更新共用一个长连接。

服务器端的性能和可扩展性

一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。

应用和技术的发展总是带来新的需求,从而推动新技术的发展。HTTP 1.1 与 1.0 规范有一个很大的不同:1.0 规范下服务器在处理完每个 Get/Post 请求后会关闭套接口连接; 而 1.1 规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接处于空闲状态。 Java 1.4 引入了支持异步 IO 的 java.nio 包。当连接处于空闲时,为这个连接分配的线程资源会返还到线程池,可以供新的连接使用;当原来处于空闲的连接的客户发出新的请求,会从线程池里分配一个线程资源处理这个请求。 这种技术在连接处于空闲的机率较高、并发连接数目很多的场景下对于降低服务器的资源负载非常有效。

但是 AJAX 的应用使请求的出现变得频繁,而 Comet 则会长时间占用一个连接,上述的服务器模型在新的应用背景下会变得非常低效,线程池里有限的线程数甚至可能会阻塞新的连接。Jetty 6 Web 服务器针对 AJAX、Comet 应用的特点进行了很多创新的改进,请参考文章“AJAX,Comet and Jetty”(请参见 参考资源)。

控制信息与数据信息使用不同的 HTTP 连接

使用长连接时,存在一个很常见的场景:客户端网页需要关闭,而服务器端还处在读取数据的堵塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。

所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP 连接,才能使控制请求不会被阻塞。

在实现上,如果是基于 iframe 流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被堵塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX 的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest 请求,通知服务器端关闭数据连接。

在客户和服务器之间保持“心跳”信息

在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道大家都在正常运行。在实现上:

  1. 服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
  2. 如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
  3. 当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。

Pushlet - 开源 Comet 框架

Pushlet 是一个开源的 Comet 框架,在设计上有很多值得借鉴的地方,对于开发轻量级的 Comet 应用很有参考价值。

观察者模型

Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。

客户端 JavaScript 库

pushlet 提供了基于 AJAX 的 JavaScript 库文件用于实现长轮询方式的“服务器推”;还提供了基于 iframe 的 JavaScript 库文件用于实现流方式的“服务器推”。

JavaScript 库做了很多封装工作:

  1. 定义客户端的通信状态:STATE_ERRORSTATE_ABORTSTATE_NULLSTATE_READYSTATE_JOINEDSTATE_LISTENING
  2. 保存服务器分配的会话 ID,在建立连接之后的每次请求中会附上会话 ID 表明身份;
  3. 提供了 join()leave()subscribe()unsubsribe()listen() 等 API 供页面调用;
  4. 提供了处理响应的 JavaScript 函数接口 onData()onEvent()

网页可以很方便地使用这两个 JavaScript 库文件封装的 API 与服务器进行通信。

客户端与服务器端通信信息格式

pushlet 定义了一套客户与服务器通信的信息格式,使用 XML 格式。定义了客户端发送请求的类型:joinleavesubscribeunsubscribelistenrefresh;以及响应的事件类型:datajoin_acklisten_ackrefreshheartbeaterrorabortsubscribe_ackunsubscribe_ack

服务器端事件队列管理

pushlet 在服务器端使用 Java Servlet 实现,其数据结构的设计框架仍可适用于 PHP、C 编写的后台客户端。

Pushlet 支持客户端自己选择使用流、拉(长轮询)、轮询方式。服务器端根据客户选择的方式在读取事件队列(fetchEvents)时进行不同的处理。“轮询”模式下 fetchEvents() 会马上返回。”流“和”拉“模式使用阻塞的方式读事件,如果超时,会发给客户端发送一个没有新信息收到的“heartbeat“事件,如果是“拉”模式,会把“heartbeat”与“refresh”事件一起传给客户端,通知客户端重新发出请求、建立连接。

客户服务器之间的会话管理

服务端在客户端发送 join 请求时,会为客户端分配一个会话 ID, 并传给客户端,然后客户端就通过此会话 ID 标明身份发出 subscribelisten 请求。服务器端会为每个会话维护一个订阅的主题集合、事件队列。

服务器端的事件源会把新产生的事件以多播的方式发送到每个会话(即订阅者)的事件队列里。

 


 

回页首

小结

本文介绍了如何在现有的技术基础上选择合适的方案开发一个“服务器推”的应用,最优的方案还是取决于应用需求的本身。相对于传统的 Web 应用, 目前开发 Comet 应用还是具有一定的挑战性。

“服务器推”存在广泛的应用需求,为了使 Comet 模型适用于大规模的商业应用,以及方便用户构建 Comet 应用,最近几年,无论是服务器还是浏览器都出现了很多新技术,同时也出现了很多开源的 Comet 框架、协议。需求推动技术的发展,相信 Comet 的应用会变得和 AJAX 一样普及。

[Q]什么是AMF?AMF0和AMF3

最近由于工作需求,对amf做了一些了解,此前对flash相关的技术用的太少,以至于n年前提出来的amf协议都不曾过耳。。 - -#

以下是关于amf的一篇文章。

Flash Remoting的核心技术——AMF
AMF是什么?它的优点中是什么?Flash Remoting为什么选择了使用AMF而放弃了SOAP与Flash 播放器通信呢?

Flash 5开始就可以以XML或者“变量/值”配对输出格式向服务器传送数据。虽然这些数据能通过Flash编译器自动解析或者通过开发人员自行编写的代码手动解析, 但解析的速度慢。因为在解析过程中,XML需要按节点逐层处理数据。而且使用XML和“变量/值”配对格式处理的数据类型只能是字符型,数字也不例外。而Flash Remoting却能处理复杂数据类型, 比如对象、结构、数组,甚至可以是数据集,配合DataGrid组件可以很方便地显示数据。

为了处理复杂数据类型,采用一种独有的方式使Flash与应用服务器间可以来回传送数据势在必行。于是AMF应运而生。AMF是Adobe独家开发出来的通信协议,它采用二进制压缩,序列化、反序列化、传输数据,从而为Flash 播放器与Flash Remoting网关通信提供了一种轻量级的、高效能的通信方式。如下图所示。

AMF最大的特色在于可直接将Flash内置对象,例如Object, Array, Date, XML,传回服务器端,并且在服务器端自动进行解析成适当的对象,这就减轻了开发人员繁复工作,同时也更省了开发时间。由于AMF采用二进制编码,这种方式可以高度压缩数据,因此非常适合用来传递大量的资料。数据量越大,Flash Remoting的传输效能就越高,远远超过Web Service。至于XML, LoadVars和loadVariables() ,它们使用纯文本的传输方式,效能就更不能与Flash Remoting相提并论了。

注意:Flash Remoting需要浏览器支持Binary POST,Flash 播放器在Netscape 6.x.环境下运行Flash Remoting会不起作用(Flash Remoting调用没有效果也不返回错误), Netscape 7已经纠正了这个bug 。对于早期Safari和Chimera版的苹果机也有这个问题。

同样是轻量级数据交换协议,同样是通过调用远程服务,同样是基于标准的HTTP和HTTPS协议, Flash Remoting为什么选择了使用AMF而放弃了SOAP与Flash 播放器通信呢? 有如下原因:

  1. SOAP将数据处理成XML格式,相对于二进制的AFM太冗长了;
  2. AMF能更有效序列化数据;因为AMF的初衷只是为了支持Flash ActionScript的数据类型,而SOAP却致力于提供更广泛的用途;
  3. AMF支持Flash 播放器 6只需要浏览器增加4 KB左右(压缩后)的大小,而SOAP就大多了;
  4. SOAP的一些头部文件请求在Flash 播放器 6不支持。那Flash 播放器 6为什么能访问基于SOAP的Web服务呢?原来Flash Remoting网关将SOAP请求在服务器端与转换成AFM格式,然后利用AFM与Flash 播放器通信。另外,AMF包中包含onResult事件(比如说response事件)和onStatus事件(比如说error事件),这些事件对象在Flash中可以直接使用。

AMF从Flash MX时代的AMF0发展到现在的AMF3。AMF3用作Flash Playe 9的ActionScript 3.0的默认序列化格式,而AMF0则用作旧版的ActionScript 1.0和2.0的序列化格式。 在网络传输数据方面,AMF3比AMF0更有效率。AMF3能将int和uint对象作为整数(integer)传输,并且能序列化ActionScript 3.0才支持的数据类型, 比如ByteArray,XML和Iexternalizable。
请阅读原文:http://www.riafan.com/article.asp?id=37

PHP性能的血汗工厂

作为PHP性能血汗工厂的老板的你,打算如何榨干它的性能?最常用的数组操作、字符串操作需要注意的地方?给大家推荐一个网站,列举了一些常用代码不同写法的性能区别。

推荐看以下5部分:

  1. 字符串输出;echo vs. print
  2. 读循环;foreach() vs. for() vs. while(list() = each()) 
  3. 写循环;foreach() vs. for vs. while(list() = each())
  4. 变量类型检查;isSet() vs. empty() vs. is_array()
  5. 别名的使用;Using the &-ref-operator

http://www.phpbench.com/ 

ps: 每次刷新各项数据会重新计算,推荐firefox查看IE下样式有问题