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

image.png


关于性能上面的优势, 之前一直以为这样一个长连接建立在那里是不是很耗资源, 使用虚拟机测试了下, 

服务器建立连接占用资源相当少, 500M 内存1000 个连接, 而客户端的压测机 1000个连接已将2G的内存吃完, 还将swap 吃完

Client: 

image.png


使用软件: php,  https://www.swoole.com/

swoole 默认的php扩展是使用的php7的, 如果你生产环境使用的 php5.x  请git clone 并切换到 1.9 分支下进行 phpize 等进行安装 , 

Installation

  1. Install via pecl  php7.x

    pecl install swoole
  2. 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回答说: " 去玩? "

image.png


Javascript: 实现 用户1: 

image.png


用户2: 

image.png

websocketd 使用:   http://websocketd.com/

思路打开了, 各种创新应用就有了, 不只是聊天室:

image.png



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/


演示参考:

https://socket.io/demos/chat/

源码参考: 

https://github.com/socketio/socket.io/tree/master/examples/chat