与 Web 服务器通讯 - web sockets

  • XMLHttpRequest:可以从客户端发送请求到服务器端而且获得响应。一个小demo在这里
    但XHR不适合快速来回发送消息(如聊天室),而且没法将这一次调用和下一次调用联系起来(每次发起请求,服务器都要肯定请求来自何方)。html

  • 轮询:浏览器按期向服务器发送消息并接受服务器回答是否有数据更新。(考虑轮询间隔时间过长/太短的问题)html5

  • COMET(长轮询):在每次请求的时候,服务器端保持该链接在一段时间内处于打开状态,而不是在响应完成以后当即关闭。
    在打开状态的这一段时间里,有数据更新能够当即获得响应。
    上一个长链接关闭以后,浏览器当即打开一个新的长链接继续请求。java

  • COMET(流):浏览器向服务器发送一个请求,而服务器保持链接打开,而后周期性向浏览器发送数据。node

  • Server-Sent events:适合服务器端向客户端不断的数据推送,更容易实现了comet。但整个通讯彻底单向,没法知道浏览器是否响应。git

  • WebSocket:浏览器可以保持对Web服务器打开的链接,从而与服务器长时间交换数据。适合聊天室、大型多人游戏、端到端写做工具等。github

WebSocket

WebSocket 是 HTML5 一种新的协议。实现了浏览器与服务器全双工通讯,能更好的节省服务器资源和带宽并达到实时通信,它创建在 TCP 之上,同 HTTP 同样经过 TCP 来传输数据。URL 模式变为了 ws://wss://web

在 JavaScript 建立了WebSocket以后,会有一个 HTTP 请求发起链接。在取得服务器响应后,创建的链接会从 HTTP 协议交换为 WebSocket 协议。也就是说,须要支持这种协议的专门服务器才能工做。json

WebSocket API 蜻蜓点水

  • 建立一个实例对象,建立后浏览器立刻尝试链接:
// 应该传入绝对url,ws://www.example.com/server
var socket = new WebSocket(url);
  • 表示当前状态的 readyState 属性,其值永远从0开始:
    WebSocket.OPENING(0) 正在创建链接
    WebSocket.OPEN(1) 已经创建链接
    WebSocket.CLOSING(2) 正在关闭链接
    WebSocket.CLOSE(3) 已经关闭链接api

  • 事件
    open:成功创建链接
    send:向服务器发送数据,只能发送纯文本数据,要传JSON数据须要先序列化
    message:接收到服务器传来数据,数存据在evt.data,一样接收的数据也是字符串,须要手工解析
    error:发生错误
    close:链接关闭浏览器

WebSocket 机制

WebSocket 实现了浏览器与服务器全双工通讯,能更好的节省服务器资源和带宽并达到实时通信,它创建在 TCP 之上,同 HTTP 同样经过 TCP 来传输数据,可是它和 HTTP 最大不一样是:

  • WebSocket 是一种双向通讯协议,在创建链接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 同样;

  • WebSocket 须要相似 TCP 的客户端和服务器端经过握手链接,链接成功后才能相互通讯。

一旦 WebSocket 链接创建后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 链接或 服务器端断掉链接前,不须要客户端和服务端从新发起链接请求。

交互的报文

  • WebSocket 客户端请求报文:
GET ws://localhost:3000/ HTTP/1.1
Host: localhost:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: file://
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Sec-WebSocket-Key: 23li+QvHohBue1I3rm2VmA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

可见,发起请求的是传统 HTTP 协议。“Upgrade:websocket” 代表这是 WebSocket 类型请求。
“Sec-WebSocket-Key” 是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的 “Sec-WebSocket-Accept” 应答,不然客户端会抛出 “Error during WebSocket handshake” 错误,并关闭链接。

  • WebSocket 服务端响应报文:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: dpuNuwoCREWFf0z2sQ1F2u2Sb50=
Origin: file://

“Sec-WebSocket-Accept” 的值是服务端采用与客户端一致的密钥计算出来后返回客户端的。
“HTTP/1.1 101 Switching Protocols” 表示服务端接受 WebSocket 协议的客户端链接,握手成功,后续就能够进行 TCP 通信了。

WebSocket 实现

  • 客户端:
<div id="output"></div>
<script>
    function checkBrowser() {
      if (!window.WebSocket) {
        console.log("Not support WebSocket!");
      }
    }
      
    function openWS(e) {
      log("ws opened.", e);
      sendMessage("Conan");
    };

    function closeWS(e) {
      log("ws closed", e);
      log("was clean?" + e.wasClean + " code=" + e.code + " reason=" + e.reason);
    };

    function receiveMsg(e) {
      log("RECEIVED: " + e.data, e);
      // ws.close();
    };

    function errorOccured(e) {
      log('Error occured: ' + e.data, e);
    };

    function sendMessage(msg) {
      ws.send(msg);
      log("SEND : "+ msg);
    };

    function log(s, e) {
      var output = document.getElementById("output");
      var p = document.createElement("p");

      p.style.wordWrap = "break-word";
      p.style.padding = "10px";
      p.style.background = "#eee";
      p.textContent = "LOG : " + s;
      output.appendChild(p);
    }

    var ws, output;
    window.onload = function() {
      output = document.getElementById("output");

      ws = new WebSocket("ws://localhost:3000");
      ws.onopen = openWS;
      ws.onmessage = receiveMsg;
      ws.onclose = closeWS;
      ws.onerror = errorOccured;
    };
</script>
  • 服务器端:
    已经开源的库能够直接使用,如node-WebSocket,socket.io等。
process.title = 'node-chat';

// Port where we'll run the websocket server
var webSocketsServerPort = 3000;

// websocket and http servers
var webSocketServer = require('websocket').server;
var http = require('http');

// latest 100 messages
var history = [ ];
// list of currently connected clients (users)
var clients = [ ];

/**
 * Helper function for escaping input strings
 */
function htmlEntities(str) {
    return String(str).replace(/&/g, '&').replace(/>/g, '>').replace(/"/g, '"');
}

// Array with some colors
var colors = [ 'red', 'green', 'blue', 'magenta', 'purple', 'plum', 'orange' ];
// ... in random order
colors.sort(function(a,b) { return Math.random() > 0.5; } );

/**
 * HTTP server
 */
var server = http.createServer(function(request, response) {
    // Not important for us. We're writing WebSocket server, not HTTP server
}).listen(webSocketsServerPort);

/**
 * WebSocket server
 */
var wsServer = new webSocketServer({
    // WebSocket server is tied to a HTTP server. WebSocket request is just
    // an enhanced HTTP request. For more info http://tools.ietf.org/html/rfc6455#page-6
    httpServer: server
});

// This callback function is called every time someone tries to connect to the WebSocket server
wsServer.on('request', function(request) {
    console.log((new Date()) + ' Connection from origin ' + request.origin + '.');

    // accept connection - you should check 'request.origin' to make sure that
    // client is connecting from your website
    // (http://en.wikipedia.org/wiki/Same_origin_policy)
    var connection = request.accept(null, request.origin); 
    // we need to know client index to remove them on 'close' event
    var index = clients.push(connection) - 1;
    var userName = false;
    var userColor = false;

    console.log((new Date()) + ' Connection accepted.');

    // send back chat history
    if (history.length > 0) {
        connection.sendUTF(JSON.stringify( { type: 'history', data: history} ));
    }

    // user sent some message
    connection.on('message', function(message) {
        if (message.type === 'utf8') { // accept only text
            if (userName === false) { // first message sent by user is their name
                // remember user name
                userName = htmlEntities(message.utf8Data);
                // get random color and send it back to the user
                userColor = colors.shift();
                connection.sendUTF(JSON.stringify({ type:'color', data: userColor }));
                console.log((new Date()) + ' User is known as: ' + userName
                            + ' with ' + userColor + ' color.');

            } else { // log and broadcast the message
                console.log((new Date()) + ' Received Message from '
                            + userName + ': ' + message.utf8Data);

                // we want to keep history of all sent messages
                var obj = {
                    time: (new Date()).getTime(),
                    text: htmlEntities(message.utf8Data),
                    author: userName,
                    color: userColor
                };
                history.push(obj);
                history = history.slice(-100);

                // broadcast message to all connected clients
                var json = JSON.stringify({ type:'message', data: obj });
                for (var i=0; i < clients.length; i++) {
                    clients[i].sendUTF(json);
                }
            }
        }
    });

    // user disconnected
    connection.on('close', function(connection) {
        if (userName !== false && userColor !== false) {
            console.log((new Date()) + " Peer "
                + connection.remoteAddress + " disconnected.");
            // remove user from the list of connected clients
            clients.splice(index, 1);
            // push back user's color to be reused by another user
            colors.push(userColor);
        }
    });

});

参考

WebSocket实战

相关文章
相关标签/搜索