公司有一个内部管理系统,需要经常员工对员工实时消息接收,之前是ajax轮循环,因为只是内部使用人员少也并没有出现什么问题。但是心里不爽啊!
测试环境
Windows 10
PHP5.3+
JS
Chrome 50+
PHP
start_ws.php
require('class_ws.php');
$ws = new Ws('127.0.0.1', '8080', 10);
$ws->function['add'] = 'user_add_callback';
$ws->function['send'] = 'send_callback';
$ws->function['close'] = 'close_callback';
$ws->start_server();
//回调函数们
function user_add_callback($ws) {
$data = count($ws->accept);
send_to_all($data, 'num', $ws);
}
function close_callback($ws) {
$data = count($ws->accept);
send_to_all($data, 'num', $ws);
}
function send_callback($data, $index, $ws) {
$data = json_encode(array(
'text' => $data,
'user' => $index,
));
send_to_all($data, 'text', $ws);
}
function send_to_all($data, $type, $ws){
$res = array(
'msg' => $data,
'type' => $type,
);
$res = json_encode($res);
$res = $ws->frame($res);
foreach ($ws->accept as $key => $value) {
socket_write($value, $res, strlen($res));
}
}
class_ws.php
<?php
class Ws{
private $host = '127.0.0.1';
private $port = 8080;
private $maxuser = 10;
public $accept = array(); //连接的客户端
private $cycle = array(); //循环连接池
private $isHand = array();
/*
接受三个回调函数,分别在新用户连接、有消息到达、用户断开时触发
function add、function send、function close
*/
public $function = array();
//Constructor
function __construct($host, $port, $max) {
$this->host = $host;
$this->port = $port;
$this->maxuser = $max;
}
//挂起socket
public function start_server() {
$this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//允许使用本地地址
socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE);
socket_bind($this->socket, $this->host, $this->port);
//最多10个人连接,超过的客户端连接会返回WSAECONNREFUSED错误
socket_listen($this->socket, $this->maxuser);
while(TRUE) {
$this->cycle = $this->accept;
$this->cycle[] = $this->socket;
//阻塞用,有新连接时才会结束
socket_select($this->cycle, $write, $except, null);
foreach ($this->cycle as $k => $v) {
if($v === $this->socket) {
if (($accept = socket_accept($v)) < 0) {
continue;
}
//如果请求来自监听端口那个套接字,则创建一个新的套接字用于通信
$this->add_accept($accept);
continue;
}
$index = array_search($v, $this->accept);
if ($index === NULL) {
continue;
}
if (!@socket_recv($v, $data, 1024, 0) || !$data) {//没消息的socket就跳过
$this->close($v);
continue;
}
if (!$this->isHand[$index]) {
$this->upgrade($v, $data, $index);
if(!empty($this->function['add'])) {
call_user_func_array($this->function['add'], array($this));
}
continue;
}
$data = $this->decode($data);
if(!empty($this->function['send'])) {
call_user_func_array($this->function['send'], array($data, $index, $this));
}
}
sleep(1);
}
}
//增加一个初次连接的用户
private function add_accept($accept) {
$this->accept[] = $accept;
$index = array_keys($this->accept);
$index = end($index);
$this->isHand[$index] = FALSE;
}
//关闭一个连接
private function close($accept) {
$index = array_search($accept, $this->accept);
socket_close($accept);
unset($this->accept[$index]);
unset($this->isHand[$index]);
if(!empty($this->function['close'])) {
call_user_func_array($this->function['close'], array($this));
}
}
//响应升级协议
private function upgrade($accept, $data, $index) {
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$data,$match)) {
$key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $key . "\r\n\r\n"; //必须以两个回车结尾
socket_write($accept, $upgrade, strlen($upgrade));
$this->isHand[$index] = TRUE;
}
}
//体力活
public function frame($s){
$a = str_split($s, 125);
if (count($a) == 1){
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o){
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}
//体力活
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;
}
}
// END ws class
/* End of file class_ws.php */
/* Location: ./server/class_ws.php */
JS
(function(){
var $ = function(id){return document.getElementById(id) || null;}
var wsServer = 'ws://127.0.0.1:8080';
var ws = new WebSocket(wsServer);
var isConnect = false;
ws.onopen = function (evt) { onOpen(evt) };
ws.onclose = function (evt) { onClose(evt) };
ws.onmessage = function (evt) { onMessage(evt) };
ws.onerror = function (evt) { onError(evt) };
function onOpen(evt) {
console.log("连接服务器成功");
isConnect = true;
}
function onClose(evt) {
//console.log("Disconnected");
}
function onMessage(evt) {
var data = JSON.parse(evt.data);
switch (data.type) {
case 'text':
addMsg(data.msg);
break;
case 'num' :
updataUserNum(data.msg);
break;
}
console.log('Retrieved data from server: ' + evt.data);
}
function onError(evt) {
//console.log('Error occured: ' + evt.data);
}
function sendMsg() {
if(isConnect){
ws.send($('input').value);
$('input').value = '';
}
}
function addMsg(msg) {
msg = JSON.parse(msg);
var text = '用户' + msg.user + '说:\n' + msg.text + '\n';
$('message').value += text;
$('message').scrollTop = $('message').scrollHeight;
}
function updataUserNum(msg) {
$('userNum').innerText = msg;
}
$('sub').addEventListener('click',sendMsg,false);
})();
最重要的一步:命令行要运行 D:\xampp\php>php e:web/blog/server/start_ws.php
然后浏览器测试握手,成功咯!
参考博客:http://www.jnecw.com/p/1523#comment-35125