websocket 之测试使用
在没有websocket 之前, 比如记得06年大学老师一个作业就是让做一个聊天室, 当时使用的是 轮询的方式, 意思是客户端需要不断的询问服务器是否有新消息, 印象很深当时使用的还不是ajax , 使用的是不断刷新页面, 当时总觉得这样不太对,,, 大学老师也没给出更好的方式,,
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。 WebSocket 就是这样发明的。 WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话, 属于服务器推送技术的一种。
其特点包括: (1)建立在 TCP 协议之上,服务器端的实现比较容易。 (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽, 能通过各种 HTTP 代理服务器。 (3)数据格式比较轻量,性能开销小,通信高效。 (4)可以发送文本,也可以发送二进制数据。 (5)没有同源限制,客户端可以与任意服务器通信。 (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
websocket 协议: status Code 是 101 Switching Protocls 协议版本 13
关于性能上面的优势, 之前一直以为这样一个长连接建立在那里是不是很耗资源, 使用虚拟机测试了下,
服务器建立连接占用资源相当少, 500M 内存1000 个连接, 而客户端的压测机 1000个连接已将2G的内存吃完, 还将swap 吃完
Client:
使用软件: php, https://www.swoole.com/
swoole 默认的php扩展是使用的php7的, 如果你生产环境使用的 php5.x 请git clone 并切换到 1.9 分支下进行 phpize 等进行安装 ,
Installation
Install via pecl php7.x
pecl install swoole
Install from source
git clone https://gitee.com/swoole/swoole.git # php5.x git checkout 1.9 cd swoole-src phpize ./configure make && make install
在对应的php.ini 里添加 extension=swoole.so
php -m 查看 swoole 是否加载成功
虽然目前绝大多数浏览器已支持websocket , 但还是需要兼容一下那些老古董的, 需要写一个轮询的做法来解决:
......
因为不可用保证网络的可用性的, 所以一定要做一下websocket 的连接监控, 使用js 监控websocket , 发现断开了, 马上重新建立:
webSocket.readyState readyState属性返回实例对象的当前状态,共有四种。 CONNECTING:值为0,表示正在连接。 OPEN:值为1,表示连接成功,可以通信了。 CLOSING:值为2,表示连接正在关闭。 CLOSED:值为3,表示连接已经关闭,或者打开连接失败。 下面是一个示例。 switch (ws.readyState) { case WebSocket.CONNECTING: // do something break; case WebSocket.OPEN: // do something break; case WebSocket.CLOSING: // do something break; case WebSocket.CLOSED: // do something break; default: // this never happens break;}
服务: server_user.php
php -f server_user.php 启动服务:
<?php $server = new swoole_websocket_server("0.0.0.0", 9502); $server->on('open', function($server, $req) { echo "connection open: {$req->fd}\n"; }); $server->on('message', function($server, $frame) { $data=json_decode($frame->data,1); var_dump($data); echo "received message: {$frame->data}\n"; $server->push($data['to_member_id'], $frame->fd.'说:'.$data['msg']); }); $server->start();
以上用户ID 直接使用了 $req->fd 的值, 真正进行使用时, 这里应该维护一个在线用户列表, 即$req->fd 与 member_id 的一个对应关系表才对:
1、fd 是一个自增数字,范围是 1 ~ 1600 万,fd 超过 1600 万后会自动从 1 开始进行复用 2、$fd 是复用的,当连接关闭后 fd 会被新进入的连接复用 3、正在维持的 TCP 连接 fd 不会被复用 这是文档中说的,按照第三条他应该会有检测机制吧,而且 1600w 基本上混淆的几率极小,
测试: 用户1, 用户2都与websocket 建立连接, 用户1对用户2说 " 一边玩去 ", 用户2回答说: " 去玩? "
Javascript: 实现 用户1:
用户2:
websocketd 使用: http://websocketd.com/
思路打开了, 各种创新应用就有了, 不只是聊天室:
stat.js
var allTimeSeries = {}; var allValueLabels = {}; var descriptions = { 'Processes': { 'r': 'Number of processes waiting for run time', 'b': 'Number of processes in uninterruptible sleep' }, 'Memory': { 'swpd': 'Amount of virtual memory used', 'free': 'Amount of idle memory', 'buff': 'Amount of memory used as buffers', 'cache': 'Amount of memory used as cache' }, 'Swap': { 'si': 'Amount of memory swapped in from disk', 'so': 'Amount of memory swapped to disk' }, 'IO': { 'bi': 'Blocks received from a block device (blocks/s)', 'bo': 'Blocks sent to a block device (blocks/s)' }, 'System': { 'in': 'Number of interrupts per second, including the clock', 'cs': 'Number of context switches per second' }, 'CPU': { 'us': 'Time spent running non-kernel code (user time, including nice time)', 'sy': 'Time spent running kernel code (system time)', 'id': 'Time spent idle', 'wa': 'Time spent waiting for IO' } } function streamStats() { var ws = new ReconnectingWebSocket('ws://' + location.host + '/'); var lineCount; var colHeadings; ws.onopen = function() { console.log('connect'); lineCount = 0; }; ws.onclose = function() { console.log('disconnect'); }; ws.onmessage = function(e) { switch (lineCount++) { case 0: // ignore first line break; case 1: // column headings colHeadings = e.data.trim().split(/ +/); break; default: // subsequent lines var colValues = e.data.trim().split(/ +/); var stats = {}; for (var i = 0; i < colHeadings.length; i++) { stats[colHeadings[i]] = parseInt(colValues[i]); } receiveStats(stats); } }; } function initCharts() { Object.each(descriptions, function(sectionName, values) { var section = $('.chart.template').clone().removeClass('template').appendTo('#charts'); section.find('.title').text(sectionName); var smoothie = new SmoothieChart({ grid: { sharpLines: true, verticalSections: 5, strokeStyle: 'rgba(119,119,119,0.45)', millisPerLine: 1000 }, minValue: 0, labels: { disabled: true } }); smoothie.streamTo(section.find('canvas').get(0), 1000); var colors = chroma.brewer['Pastel2']; var index = 0; Object.each(values, function(name, valueDescription) { var color = colors[index++]; var timeSeries = new TimeSeries(); smoothie.addTimeSeries(timeSeries, { strokeStyle: color, fillStyle: chroma(color).darken().alpha(0.5).css(), lineWidth: 3 }); allTimeSeries[name] = timeSeries; var statLine = section.find('.stat.template').clone().removeClass('template').appendTo(section.find('.stats')); statLine.attr('title', valueDescription).css('color', color); statLine.find('.stat-name').text(name); allValueLabels[name] = statLine.find('.stat-value'); }); }); } function receiveStats(stats) { Object.each(stats, function(name, value) { var timeSeries = allTimeSeries[name]; if (timeSeries) { timeSeries.append(Date.now(), value); allValueLabels[name].text(value); } }); } $(function() { initCharts(); streamStats(); });
websocket重连, 这个很关键, 测试了下网络断开的情况, 重新自动建立连接正常可用:
// MIT License: // // Copyright (c) 2010-2012, Joe Walnes // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. /** * This behaves like a WebSocket in every way, except if it fails to connect, * or it gets disconnected, it will repeatedly poll until it succesfully connects * again. * * It is API compatible, so when you have: * ws = new WebSocket('ws://....'); * you can replace with: * ws = new ReconnectingWebSocket('ws://....'); * * The event stream will typically look like: * onconnecting * onopen * onmessage * onmessage * onclose // lost connection * onconnecting * onopen // sometime later... * onmessage * onmessage * etc... * * It is API compatible with the standard WebSocket API. * * Latest version: https://github.com/joewalnes/reconnecting-websocket/ * - Joe Walnes */ function ReconnectingWebSocket(url, protocols) { protocols = protocols || []; // These can be altered by calling code. this.debug = false; this.reconnectInterval = 1000; this.timeoutInterval = 2000; var self = this; var ws; var forcedClose = false; var timedOut = false; this.url = url; this.protocols = protocols; this.readyState = WebSocket.CONNECTING; this.URL = url; // Public API this.onopen = function(event) { }; this.onclose = function(event) { }; this.onconnecting = function(event) { }; this.onmessage = function(event) { }; this.onerror = function(event) { }; function connect(reconnectAttempt) { ws = new WebSocket(url, protocols); self.onconnecting(); if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'attempt-connect', url); } var localWs = ws; var timeout = setTimeout(function() { if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'connection-timeout', url); } timedOut = true; localWs.close(); timedOut = false; }, self.timeoutInterval); ws.onopen = function(event) { clearTimeout(timeout); if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'onopen', url); } self.readyState = WebSocket.OPEN; reconnectAttempt = false; self.onopen(event); }; ws.onclose = function(event) { clearTimeout(timeout); ws = null; if (forcedClose) { self.readyState = WebSocket.CLOSED; self.onclose(event); } else { self.readyState = WebSocket.CONNECTING; self.onconnecting(); if (!reconnectAttempt && !timedOut) { if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'onclose', url); } self.onclose(event); } setTimeout(function() { connect(true); }, self.reconnectInterval); } }; ws.onmessage = function(event) { if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'onmessage', url, event.data); } self.onmessage(event); }; ws.onerror = function(event) { if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'onerror', url, event); } self.onerror(event); }; } connect(url); this.send = function(data) { if (ws) { if (self.debug || ReconnectingWebSocket.debugAll) { console.debug('ReconnectingWebSocket', 'send', url, data); } return ws.send(data); } else { throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; } }; this.close = function() { if (ws) { forcedClose = true; ws.close(); } }; /** * Additional public API method to refresh the connection if still open (close, re-open). * For example, if the app suspects bad data / missed heart beats, it can try to refresh. */ this.refresh = function() { if (ws) { ws.close(); } }; } /** * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. */ ReconnectingWebSocket.debugAll = false;
参考资料:
http://www.ruanyifeng.com/blog/2017/05/websocket.html
其它解决方案:
演示参考:
https://socket.io/demos/chat/
源码参考:
https://github.com/socketio/socket.io/tree/master/examples/chat