基于HTTP长连接的“服务器推”技术的简易聊天室

By 深空, 2009年10月28日 19:43

  关于HTTP长连接的“服务器推”技术原理可以查看IBM的这篇文章,我简单的做了个DEMO:
  首先是首页,包含一个文本输入和一个显示聊天内容的iframe,还有一个隐藏iframe用来提交form表单:

<?php
//chat.php
header('cache-control: private');
header('Content-Type: text/html; charset=utf-8');
?>
<html>
<script type="text/javascript">
function submitChat(obj) {
    obj.submit();
    document.getElementsByName('content')[0].value = '';
}
</script>
<iframe src="./chat_content.php" height="300" width="100%"></iframe>
<iframe name="say" height="0" width="0"></iframe>
<form method="POST" target="say" action="./say.php" onsubmit="submitChat(this)">
<input type="text" size="30" name="content" /> <input type="button" value="say" onclick="submitChat(this.form)" />
</form>
</html>

  另外一个就是保存用户提交的聊天内容了,我简单的写一下文本,而且没有做什么锁定,这个只是简易版本:

<?php
$content = trim($_POST['content']);
if ($content) {
    $fp = fopen('./chat.txt', 'a');
    fwrite($fp, $content . "\n");
    fclose($fp);
    clearstatcache();
}
?>

  接下来看主要的HTTP长连接部分,也就是chat_content.php文件:

<?php
header('cache-control: private');
header('Content-Type: text/html; charset=utf-8');

//测试设置30秒超时,一般会设置比较长时间。
set_time_limit(30);

//这一行是为了搞定IE这个BT
echo str_repeat(' ', 256);

ob_flush();
flush();
$fp = new SplFileObject('./chat.txt', 'r+');
$line = 0;
$totalLine = 0;
while (!$fp->eof()) {
    $fp->current();
    $totalLine++;
    $fp->next();
}
$fp->seek($totalLine);
$i = $totalLine - 1;
while (true) {
    if (!$fp->eof()) {
        if ($content = trim($fp->current())) {
            echo '<div>';
            echo htmlspecialchars($content);
            echo "</div>";
            flush();
            $fp->next();
            $i++;
        }
    } else {
        $fp->seek($i - 1);
        $fp->next();
    }

    {
        //这里可以添加心跳检测后退出循环
    }
    usleep(1000);
}
?>

  我一行行解释一下,其实也比较容易理解:
  06. 设置一个超时时间,由于要保持HTTP长连接,这个时间肯定要比较长,可能要几个小时吧,上面提到的文章里也有说明,这种HTTP长连接只能打开两个,由于浏览器的限制。另外其实即使你设置了一个永不超时,其实上服务器部分(如Apache)的配置文件也可能对HTTP请求设置了最长等待时间,所以也可能效果会不是你想的,一般默认可能都是15分钟超时。如果有兴趣可以自己尝试修改。

  09. 这里输出了一段空白,主要是手册上已经说明了,IE浏览器在前面256个字符是不会直接输出的,所以我们先随便输出些空白,以便让后面的内容输出来,可能其他浏览器也有其他浏览器的设置,具体可以查看PHP手册的frush函数的说明。接下去11、12行就是强制把这些空白符丢给浏览器输出。

  13. ~ 20. 这里主要是为了计算文件行数,以便从这一行后面开始读内容。

  接下去的while循环就是一个死循环了,就是循环输出文件内容,每次判断是否到达文件末尾,如果有用户写入文件,则当前检测肯定不是文件末尾,就将该行读取出来输出,否则将指针往前移动一行,继续循环,每次等待1000微秒,

  39. 如果一直保持长连接,那么即使客户端断开,服务端也不一定能知道客户端已经断开,所以这里可能还需要做一些心跳记录,比如每个用户保持一个心跳flag,每格几秒更新一下最后心跳时间,当检测最后时间很久没更新后,推出这个死循环,关闭这个HTTP连接。

  OK,基本上原理就是这样了,当然这个性能不清楚,有兴趣的自己试试,欢迎交流。

VN:F [1.9.3_1094]
Rating: 7.5/10 (34 votes cast)
VN:F [1.9.3_1094]
Rating: +12 (from 14 votes)

我的第一个Windows窗体程序

By 深空, 2009年10月20日 22:20

  虽然之前也用JAVA编写过窗体程序,但毕竟不是原装的,这次采用C来写Windows窗体,发现比JAVA简单一些,HOHO

#include <windows.h>
#include <windowsx.h>

int WINAPI WinMain(__in HINSTANCE hInstance, __in_opt HINSTANCE hPrevInstance, __in_opt LPSTR lpCmdLine, __in int nShowCmd) {
    MessageBox(NULL, "Hello world!", "你好世界!", MB_OK | MB_ICONASTERISK);
    return 0;
}

  采用Visual Studio 2008,创建的空项目,直接编译竟然就可以了。

我的第一个Windows窗体程序

我的第一个Windows窗体程序

VN:F [1.9.3_1094]
Rating: 7.1/10 (10 votes cast)
VN:F [1.9.3_1094]
Rating: +4 (from 10 votes)

我的新书架

By 深空, 2009年10月18日 09:59

  手头书有点小多,乱七八糟铺了一沙发,昨天去宜家买了个CD架当书架使,小一点不过貌似够用了,研究结果就是一般书架都很高,超过两米,只有几款比较矮的,另外深度都是28cm左右,其实我不太喜欢太深的书架,一方面占地方,另外一方面书陷进去很不好看。最后选了个小的CD架,这样可以放在电脑旁边,随手可以拿到咯,好了废话不多说上图:

安装中,宜家的东西都是需要动手安装的

安装中,宜家的东西都是需要动手安装的


来一张近焦的,嘿嘿

来一张近焦的,嘿嘿


只有技术书,全貌

只有技术书,全貌

VN:F [1.9.3_1094]
Rating: 7.2/10 (6 votes cast)
VN:F [1.9.3_1094]
Rating: +6 (from 6 votes)

用线性回归方法计算直线斜率

By 深空, 2009年10月13日 19:25

  最近在做设备负载预测,考虑到负载波动,需要拿出近似增长率来计算未来数天的设备负载增长状况,想想看以前的数学都没有学好,算法也没有搞好,只能求助同事和百度Google,最终还是折腾出来了。

点分布和趋近的直线

点分布和趋近的直线


  关于线性回归可以参考百度知道。其中采用最小二乘法可以比较容易的算出过往设备负载增长的斜率,具体公式如下:
最小二乘法公式

最小二乘法公式


  下面代码简单枚举历史10个点来计算该设备负载增长率:

//Y坐标值表示设备历史负载
$y = array(52.09, 52.4, 53.29, 54.22, 55.15, 55.83, 56.89, 56.98, 57.55, 57.8);

//X坐标值表示顺序天数
$x = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

//计算X和Y均值
$ax = array_sum($x)/count($x);
$ay = array_sum($y)/count($y);

//计算斜率公式中的分母(em)和分子(ez)
$em = 0;
$ez = 0;
for ($i = 0; $i < count($x); $i++) {
    //分母求和
    $em += (($x[$i] - $ax) * ($y[$i] - $ay));
    //分子求和
    $ez += pow(($x[$i] - $ax), 2);
}

//斜率0.69
echo $em/$ez;

//第十一个点预测负载值58.34
echo $em/$ez * 10 + $ay - ($em/$ez)*$ax;

  很多概念都不甚懂,反正数学是没有学好的,找来公式代一代,嘿嘿,还算可以,对于波动比较大的就比较难以预测,这个近似值还是很有参考意义的。

VN:F [1.9.3_1094]
Rating: 6.9/10 (16 votes cast)
VN:F [1.9.3_1094]
Rating: -2 (from 12 votes)

由会话重定向看到的对象销毁问题

By 深空, 2009年10月12日 15:58

  今天和同事又讨论到DB类中析构函数是否要关闭数据库连接的问题,粗略看起来貌似在DB对象销毁之前关闭数据库连接再正常不过:

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

  执行:

<?php
//class DB ..
$db = new DB;
?>

  正常打印 destruct:close
  再来看看下面的会话重定向到DB:

<?php

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

class SessionHandler {
    public static $db;

    function open() {
    }

    function close() {
    }

    function read() {
    }

    function write($id, $data) {
        echo "session:write<br />";
        $sql = "REPLACE INTO session VALUES('$id', '$data')";
        self::$db->query($sql);
    }

    function destroy() {
    }

    function gc() {
    }
}

SessionHandler::$db = new DB;
session_set_save_handler(array('SessionHandler', 'open'), array('SessionHandler', 'close'), array('SessionHandler', 'read'), array('SessionHandler', 'write'), array('SessionHandler', 'destroy'), array('SessionHandler', 'gc'));

session_start();
$_SESSION['user'] = '深空';

?>

  这次析构正常,只不过会话写入出错了,析构在会话写入之前被执行了:

destruct:close
session:write

Warning: mysql_query(): 2 is not a valid MySQL-Link resource on line 12

  厄,见鬼了吧,PHP竟然在脚本执行结束后先执行析构函数,然后才处理会话。

  我再稍微修改一下 SessionHandler:

<?php

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

class SessionHandler {
    function open() {
    }

    function close() {
    }

    function read() {
    }

    function write($id, $data) {
        global $db;
        echo "session:write<br />";
        $sql = "REPLACE INTO session VALUES('$id', '$data')";
        $db->query($sql);
    }

    function destroy() {
    }

    function gc() {
    }
}

$db = new DB();
session_set_save_handler(array('SessionHandler', 'open'), array('SessionHandler', 'close'), array('SessionHandler', 'read'), array('SessionHandler', 'write'), array('SessionHandler', 'destroy'), array('SessionHandler', 'gc'));

session_start();
$_SESSION['user'] = '深空';

?>

  输出如下:

destruct:close
session:write

Fatal error: Call to a member function query() on a non-object on line 35

  这次报错让我感到意外,根据提示可以认定35行的$db变量不是个对象,后来想想也确实应该是这样,因为__destruct方法已经执行了,那么其实对象在write之前已经被销毁了,所以找不到$db变量已经是空的,不足为奇。我又测试了一些字符串和数组变量,发现write执行的时候,这些变量还没有被销毁,我又做了如下修改:

<?php

class DB {
    public $conn;

    public function __construct() {
        $this->conn = mysql_connect('localhost', 'root', '');
        mysql_select_db('test', $this->conn);
    }

    public function query($sql) {
        return mysql_query($sql, $this->conn);
    }

    public function __destruct() {
        echo "destruct:close<br />";
        mysql_close($this->conn);
    }
}

class SessionHandler {
    function open() {
    }

    function close() {
    }

    function read() {
    }

    function write($id, $data) {
        global $db;
        echo "session:write<br />";
        $sql = "REPLACE INTO session VALUES('$id', '$data')";
        $db['session']->query($sql);
    }

    function destroy() {
    }

    function gc() {
    }
}

$db['session'] = new DB();
session_set_save_handler(array('SessionHandler', 'open'), array('SessionHandler', 'close'), array('SessionHandler', 'read'), array('SessionHandler', 'write'), array('SessionHandler', 'destroy'), array('SessionHandler', 'gc'));

session_start();
$_SESSION['user'] = '深空';

?>

  输出:

destruct:close
session:write

Warning: mysql_query(): 2 is not a valid MySQL-Link resource on line 12

  呵呵,这次$db没有被销毁,因为它是个数组变量,后来和同事分析觉得可能是这些变量和对象的存储方式不一样,销毁的顺序也不一样导致的。不过__destruct仍然是在write前执行,基本上可以判断会话是在很后面才执行的。要解决这个问题,只能去掉DB的__destruct方法。

  我没有去深究PHP内部对变量的处理和回收方式,只是在重定向会话处理方法的时候这个问题才显露出来,是个比较有趣的问题,发出来和大家分享我的发现主要是想让大家在碰到这个问题的时候不会太浪费时间在调试上。

  希望明真相的高手不吝指点。

VN:F [1.9.3_1094]
Rating: 7.1/10 (13 votes cast)
VN:F [1.9.3_1094]
Rating: +1 (from 7 votes)

京ICP备05002071号 ©2003-2010 深空