公司有一个内部管理系统,需要经常员工对员工实时消息接收,之前是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