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