WebSocket
The WebSocket Protocol
1. Introduction
1.1. Background
HTTP was not initially meant to be used for bidirectional communication.
A simpler solution would be to use a single TCP connection for traffic in both directions. This is what the WebSocket Protocol provides.
1.2. Protocol Overview
The WebSocket Protocol attempts to address the goals of existing bidirectional HTTP technologies in the context of the existing HTTP infrastructure.
- HTTP Request message
Method SP Request-URI SP HTTP-Version CRLF *(( general-header | request-header | entity-header ) CRLF) CRLF [ message-body ]request-header =
Accept | Accept-Charset | Accept-Encoding | Accept-Language | Authorization | Expect | From | Host | If-Match
HTTP-Version SP Status-Code SP Reason-Phrase CRLF *(( general-header | response-header | entity-header ) CRLF) CRLF [ message-body ]Status-Code =
"100" ; Continue | "101" ; Switching Protocols | "200" ; OK | "201" ; Created | "202" ; Accepted | "203" ; Non-Authoritative Information | "204" ; No Content | "205" ; Reset Content | "206" ; Partial Content | "300" ; Multiple Choices | "301" ; Moved Permanently | "302" ; Found | "303" ; See Other | "304" ; Not Modified | "305" ; Use Proxy | "307" ; Temporary Redirect | "400" ; Bad Request | "401" ; Unauthorized | "402" ; Payment Required | "403" ; Forbidden | "404" ; Not Found | "405" ; Method Not Allowed | "406" ; Not Acceptable | "407" ; Proxy Authentication Required | "408" ; Request Time-out | "409" ; Conflict | "410" ; Gone | "411" ; Length Required | "412" ; Precondition Failed | "413" ; Request Entity Too Large | "414" ; Request-URI Too Large | "415" ; Unsupported Media Type | "416" ; Requested range not satisfiable | "417" ; Expectation Failed | "500" ; Internal Server Error | "501" ; Not Implemented | "502" ; Bad Gateway | "503" ; Service Unavailable | "504" ; Gateway Time-out | "505" ; HTTP Version not supported | extension-coderesponse-header =
Accept-Ranges | Age | ETag | Location | Proxy-Authenticate | Retry-After | Server | Vary | WWW-Authenticategeneral-header =
Cache-Control | Connection | Date | Pragma | Trailer | Transfer-Encoding | Upgrade | Via | Warning
The protocol has two parts: a handshake and the data transfer.
3. WebSocket URIs
- ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
- wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
4. Opening Handshake
4.1. Client Requirements
Once a connection to the server has been established, the client MUST send an opening handshake to the server. The handshake consists of an HTTP Upgrade request, along with a list of required and optional header fields.
GET uri HTTP/1.1 Host: Upgrade:websocket Connection: Upgrade Sec-WebSocket-Key:nonce Origin:http://www.example.com Sec-WebSocket-Version:13 Sec-WebSocket-Protocol: Sec-WebSocket-Extensions:
- Origin The Origin request header indicates where a fetch originates from. It doesn't include any path information, but only the server name. Two objects have the same origin only when the scheme, host, and port all match.
Once the client's opening handshake has been sent, the client MUST wait for a response from the server before sending any further data. The client MUST validate the server's response.
4.2. Server-Side Requirements
4.2.1. Reading the Client's Opening Handshake
4.2.2. Sending the Server's Opening Handshake
- If the connection is happening on an HTTPS (HTTP-over-TLS) port, perform a TLS handshake over the connection. If this fails, then close the connection; otherwise, all further communication for the connection (including the server's handshake) MUST run through the encrypted tunnel
- The server can perform additional client authentication For example, by returning a 401 status code with the corresponding WWW-Authenticate header field.
- The server MAY redirect the client using a 3xx status code
- If the server chooses to accept the incoming connection, it MUST reply with a valid HTTP response indicating the following.
HTTP/1.1 101 Switching Protocols Upgrade:websocket Connection: Upgrade Sec-WebSocket-Acceptsecret-value Sec-WebSocket-Protocol: Sec-WebSocket-Extensions:
- Sec-WebSocket-Accept The value of this header field is constructed by concatenating the Sec-WebSocket-Key header field in the client's handshake with "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" then generates the SHA-1 hash as the secret value.
WebSocket: LIGHTWEIGHT CLIENT-SERVER COMMUNICATIONS
byAndrew Lombardi
CHAPTER 1 Quick Start
source of the examples
WebSocket gives you a bidirectional, full-duplex communications channel that operates over HTTP through a single socket.
Getting Node and npm
- Installing on OS X
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew -v brew install node node -v npm -v
sudo apt-get update sudo apt-get install nodejs sudo apt install npmNPM is the package manager for the Node JavaScript platform. It puts modules in place so that node can find them, and manages dependency conflicts intelligently. It is a package repository service that hosts published JavaScript modules.
npm install is a command that lets you download packages from their repository.
The npm cli puts all the downloaded modules in a node_modules directory where you ran npm install. Node.js has very detailed documentation on how modules find other modules which includes finding a node_modules directory.
Hello, World! Example
The ws library can take a lot of the headache out of writing a WebSocket server (or client) by offering a simple, clean API for your Node.js application. Install it using npm:
npm install ws
- server server.js:
var WebSocketServer = require('ws').Server; var wss = new WebSocketServer({port: 8181}); wss.on('connection', function(ws) { console.log('client connected'); ws.on('message', function(message) { console.log(message); }); });run the server so it’s listening for the client you’re about to code:
node server.js
<!DOCTYPE html> <html lang="en"> <head> <title>WebSocket Echo Demo</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"> <script> var ws = new WebSocket("ws://localhost:8181"); ws.onopen = function(e) { console.log('Connection to server opened'); } function sendMessage() { ws.send($('#message').val()); } </script> </head> <body lang="en"> <div class="vertical-center"> <div class="container"> <p> </p> <form role="form" id="chat_form" onsubmit="sendMessage(); return false;"> <div class="form-group"> <input class="form-control" type="text" name="message" id="message" placeholder="Type text to echo in here" value="" autofocus/> </div> <button type="button" id="send" class="btn btn-primary" onclick="sendMessage();">Send!</button> </form> </div> </div> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js> </body> </html>
Why WebSocket?
WebSocket gives you the ability to use an upgraded HTTP request, and send data in a message-based way, similar to UDP and with all
the reliability of TCP. You can also layer another protocol on top of WebSocket, and provide it in a secure way over TLS.
CHAPTER 2 WebSocket API
WebSocket is an event-driven, full-duplex asynchronous communications channel for your web applications.
While WebSocket uses HTTP as the initial transport mechanism, the communication doesn’t end after a response is received by the client., as long as the connection stays open, the client and server can freely send messages asynchronously without polling for anything new.
Initializing
Create a WebSocket object called ws that you can use to listen for events:
var ws = new WebSocket( url );url can be a string started with "ws://" or "wss://" (if using TLS).
Stock Example UI
<!DOCTYPE html> <html lang="en"> <head> <title>Stock Chart over WebSocket</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <style type="text/css"> html, body { height: 100%; } </style> <script> $(function() { var ws = new WebSocket("ws://localhost:8181"); var stock_request = { "stocks": ["AAPL", "MSFT", "AMZN", "GOOG", "YHOO"] }; var stocks = { "AAPL": 0, "MSFT": 0, "AMZN": 0, "GOOG": 0, "YHOO": 0 } $('#AAPL span').toggleClass('label-success'); ws.onopen = function(e) { console.log('Connection to server opened'); ws.send(JSON.stringify(stock_request)); } var changeStockEntry = function(symbol, originalValue, newValue) { var valElem = $('#' + symbol + ' span'); valElem.html(newValue.toFixed(2)); if(newValue < originalValue) { valElem.addClass('label-danger'); valElem.removeClass('label-success'); } else if(newValue > originalValue) { valElem.addClass('label-success'); valElem.removeClass('label-danger'); } } ws.onmessage = function(e) { var stocksData = JSON.parse(e.data); for(var symbol in stocksData) { if(stocksData.hasOwnProperty(symbol)) { changeStockEntry(symbol, stocksData[symbol], stocks[symbol]); stocks[symbol] = stocksData[symbol]; } } } ws.onclose = function(e) { console.log("Connection closed"); for(var symbol in stocks) { if(stocks.hasOwnProperty(symbol)) { stocks[symbol] = 0; } } } function disconnect() { ws.close(); } }); </script> </head> <body lang="en"> <div class="vertical-center"> <div class="container"> <h1>Stock Chart over WebSocket</h1> <table class="table" id="stockTable"> <thead> <tr> <th>Symbol</th> <th>Price</th> </tr> </thead> <tbody id="stockRows"> <tr> <td><h3>AAPL</h3></td> <td id="AAPL"><h3><span class="label label-default">95.00</span></h3></td> </tr> <tr> <td><h3>MSFT</h3></td> <td id="MSFT"><h3><span class="label label-default">50.00</span></h3></td> </tr> <tr> <td><h3>AMZN</h3></td> <td id="AMZN"><h3><span class="label label-default">300.00</span></h3></td> </tr> <tr> <td><h3>GOOG</h3></td> <td id="GOOG"><h3><span class="label label-default">550.00</span></h3></td> </tr> <tr> <td><h3>YHOO</h3></td> <td id="YHOO"><h3><span class="label label-default">35.00</span></h3></td> </tr> </tbody> </table> </div> </div> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> </body></html>
WebSocket Events
The API for WebSocket is based around events.
Listen to these events using addEventListener() or by assigning an event listener to the oneventname property of this interface.
The WebSocket object provides properties related to events:
- WebSocket.onclose An event listener to be called when the connection is closed.
ws.onclose = function(e) { console.log(e.reason + " " + e.code); for(var symbol in stocks) { if(stocks.hasOwnProperty(symbol)) { stocks[symbol] = 0; } } }; ws.close(1000, 'WebSocket connection closed')
var stock_request = {"stocks": ["AAPL", "MSFT", "AMZN", "GOOG", "YHOO"]}; var stocks = {"AAPL": 0,"MSFT": 0,"AMZN": 0,"GOOG": 0,"YHOO":0}; // WebSocket connection established ws.onopen = function(e) { console.log("Connection established"); ws.send(JSON.stringify(stock_request)); };After the open event is fired, it is ready to send and receive messages from your client/server application
ws.onerror = function(e) { console.log("WebSocket failure, error", e); handleErrors(e); };
// UI update function var changeStockEntry = function (symbol, originalValue, newValue) { var valElem = $('#' + symbol + ' span'); valElem.html(newValue.toFixed(2)); if(newValue < originalValue) { valElem.addClass('label-danger'); valElem.removeClass('label-success'); } else if(newValue > originalValue) { valElem.addClass('label-success'); valElem.removeClass('label-danger'); } }; // WebSocket message handler ws.onmessage = function(e) { var stocksData = JSON.parse(e.data); for(var symbol in stocksData) { if(stocksData.hasOwnProperty(symbol)) { changeStockEntry(symbol, stocks[symbol], stocksData[symbol]); stocks[symbol] = stocksData[symbol]; } } };
WebSocket Methods
The creators of WebSocket kept its methods pretty simple—there are only two: send() and close() .
- Send You need to ensure that the connection is open and ready to receive messages before you send the message.
- Close After this method is called, no more data can be sent or received from this connection. Optionally, you can pass a numeric code and a human-readable reason through the close() method.
// Close the WebSocket connection with reason. ws.close(1000, "Goodbye, World!");
WebSocket Attributes
When the event for open is fired, the WebSocket object can have several possible attributes that can be read.
- readyState The value of readyState:
- WebSocket.CONNECTING The connection is not yet open.
- WebSocket.OPEN The connection is open and ready to communicate.
- WebSocket.CLOSING The connection is in the process of closing.
- WebSocket.CLOSED The connection is closed or couldn’t be opened.
- bufferedAmount The amount of data buffered for sending. Use of the bufferedAmount attribute can be useful for ensuring that all data is sent before closing a connection.
- protocol The optional protocol argument used in the constructor for WebSocket,
Stock Example Server
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8181 }); var stocks = { "AAPL":95.0, "MSFT":50.0, "AMZN":300.0, "GOOG":550.0, "YHOO":35.0 } function randomInterval(min, max) { return Math.floor(Math.random()*(max-min+1)+min); } var stockUpdater; var randomStockUpdater = function() { for (var symbol in stocks) { if(stocks.hasOwnProperty(symbol)) { var randomizedChange = randomInterval(-150, 150); var floatChange = randomizedChange / 100; stocks[symbol] += floatChange; } } // get a random period between 0.5s ~ 2.5s var randomMSTime = randomInterval(500, 2500); stockUpdater = setTimeout( function() { randomStockUpdater(); }, randomMSTime ) } // start the timer which calls itself repeatedly randomStockUpdater(); wss.on('connection', function(ws) { var clientStockUpdater; var sendStockUpdates = function(ws) { if(ws.readyState == 1) { var stocksObj = {}; for(var i=0; i < clientStocks.length; i++) { symbol = clientStocks[i]; stocksObj[symbol] = stocks[symbol]; } ws.send(JSON.stringify(stocksObj)); } } clientStockUpdater = setInterval( function() { sendStockUpdates(ws); } , 1000); var clientStocks = []; ws.on('message', function(message) { var stock_request = JSON.parse(message); clientStocks = stock_request['stocks']; sendStockUpdates(ws); }); ws.on('close', function() { if(typeof clientStockUpdater !== 'undefined') { clearInterval(clientStockUpdater); } }); } );
- require('ws') require() is not part of the standard JavaScript API. But in Node.js, it's a built-in function with a special purpose: to load modules. Modules are a way to split an application into separate files instead of having all of your application in one file. This concept is also present in other languages with minor differences in syntax and behavior, like C's include, Python's import, and so on. One big difference between Node.js modules and browser JavaScript is how one script's code is accessed from another script's code:
- In browser JavaScript, scripts are added via the <script> element. When they execute, they all have direct access to the global scope, a "shared space" among all scripts. Any script can freely define/modify/remove/call anything on the global scope.
- In Node.js, each module has its own scope. A module cannot directly access things defined in another module unless it chooses to expose them. To expose things from a module, they must be assigned to exports or module.exports. For a module to access another module's exports or module.exports, it must use require().
- WebSocket.Server This class represents a WebSocket server. It extends the EventEmitter to provide the server APIs.
- new WebSocket.Server(options[, callback]) options:
- host String. The hostname where to bind the server.
- port Number. The port where to bind the server.
- verifyClient Function. A function which can be used to validate incoming connections. If verifyClient is not set then the handshake is automatically accepted. If it is provided with a single argument then that is: info:
- origin String. The value in the Origin header indicated by the client.
- req http.IncomingMessage. The client HTTP GET request.
- secure Boolean. true if req.connection.authorized or req.connection.encrypted is set.
- info Object. Same as above.
- cb Function} A callback that must be called by the user upon inspection of the info fields. Arguments in this callback are:
- result Boolean. Whether or not to accept the handshake.
- code Number. When result is false this field determines the HTTP error status code to be sent to the client.
- name String. When result is false this field determines the HTTP reason phrase.
- headers Object. When result is false this field determines additional HTTP headers to be sent to the client. For example, { 'Retry-After': 120 }.
CHAPTER 3 Bidirectional Chat
You’ll use npm to install a Node module "uuid" for generating a UUID:
npm install uuid
server.js
var WebSocket = require('ws'); var WebSocketServer = WebSocket.Server, wss = new WebSocketServer({port: 8181, verifyClient: function(info, callback) { console.log(info.req); if(info.origin === 'file://') { callback(true); return; } callback(false); } }); var uuid = require('uuid/v4'); var clients = []; wss.on('connection', function(ws) { var client_uuid = uuid(); clients.push({"id": client_uuid, "ws": ws}); console.log('client [%s] connected', client_uuid); ws.on('message', function(message) { for (var i=0; i < clients.length; i++) { var clientSocket = clients[i].ws; if(clientSocket.readyState === WebSocket.OPEN) { console.log('client [%s]: %s', clients[i].id, message); clientSocket.send(JSON.stringify({ "id": client_uuid, "message": message })); } } }); ws.on('close', function() { for(var i=0; i<clients.length; i++) { if(clients[i].id == client_uuid) { console.log('client [%s] disconnected', client_uuid); clients.splice(i, 1); } } }); });The server that will accept connections from WebSocket clients, and will rebroadcast received messages to all connected clients.
Client.html
<!DOCTYPE html> <html lang="en"> <head> <title>Bi-directional WebSocket Chat Demo</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script> var ws = new WebSocket("ws://localhost:8181"); ws.onopen = function(e) { console.log('Connection to server opened'); } function appendLog(message) { var messages = document.getElementById('messages'); var messageElem = document.createElement("li"); messageElem.innerHTML = "<h2><span class=\"label label-success\">*</span> " + message + "</h2>"; messages.appendChild(messageElem); } ws.onmessage = function(e) { var data = JSON.parse(e.data); appendLog(data.message); console.log("ID: [%s] = %s", data.id, data.message); } ws.onclose = function(e) { appendLog("Connection closed"); console.log("Connection closed"); } function sendMessage() { if(ws.readyState === WebSocket.OPEN) { ws.send($('#message').val()); } $('#message').val(''); $('#message').focus(); } function disconnect() { ws.close(); } </script> </head> <body lang="en"> <div class="vertical-center"> <div class="container"> <ul id="messages" class="list-unstyled"> </ul> <hr /> <form role="form" id="chat_form" onsubmit="sendMessage(); return false;"> <div class="form-group"> <input class="form-control" type="text" name="message" id="message" placeholder="Type text to echo in here" value="" autofocus/> </div> <button type="button" id="send" class="btn btn-primary" onclick="sendMessage();">Send Message</button> <!-- <input type="button" value="Disconnect" onclick="disconnect();" /> --> </form> </div> </div> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script> </body> </html>The content of an HTML element can be accessed by the innerHTML property of that element.
CHAPTER 4 STOMP over WebSocket
CHAPTER 5 WebSocket Compatibility
CHAPTER 6 WebSocket Security
TLS and WebSocket
Generating a Self-Signed Certificate
- CA
- Generate the root private key
openssl genrsa -aes256 -out private/cakey.pem 4096
openssl req -new -x509 -key private/cakey.pem -out cacert.pem -days 3650 -set_serial 0
- Generate the CO server key
openssl genrsa -aes256 -out co_key.pem 2048
openssl req -new -key co_key.pem -out co.csrRemove the the pass-phrase from the key:
cp co_key.pem co_key.passed.pem openssl rsa -in co_key.passed.pem -out co_key.pem
openssl ca -in co.csr -out co.pem
- Generate the Web client key
openssl genrsa -aes256 -out web_key.pem 2048
openssl req -new -key web_key.pem -out web.csr
openssl ca -in web.csr -out web.pemFiles *.csr are used for sending to a Certificate Authority for a validated certificate.
Creating a very simple HTTPS server
var fs = require("fs"), https = require("https"); var privateKey = fs.readFileSync('/home/jerry/ca/requests/co_key.pem').toString(); var certificate = fs.readFileSync('/home/jerry/ca/requests/co.pem').toString(); var options = { key: privateKey, cert: certificate }; https.createServer(options, function(req,res) { res.writeHead(200); res.end("Hello Secure World\n"); } ).listen(3000);Browse https://192.168.122.1:3000/ to get the message "Hello Secure World" returned.
Setting up WebSocket over TLS: Example
An example of using the https module to allow the bidirectional WebSocket communication to happen over TLS and listen on port 8080:
var fs = require('fs'); var https = require('https'); var http = require('http'); var WebSocket = require('ws'); // you'll probably load configuration from config var cfg = { ssl: true, host: '0.0.0.0', port: 8080, ssl_key: '/home/jerry/ca/private/co_key.pem', ssl_cert: '/home/jerry/ca/certs/co.pem' }; var WebSocketServer = WebSocket.Server; var app = null; fs.readFile('./index.html', function(err, html) { if(err) { throw err; } app = https.createServer( { key: fs.readFileSync( cfg.ssl_key ), cert: fs.readFileSync( cfg.ssl_cert ) }, function(request, response) { response.writeHeader(200, {"Content-Type": "text/html"}); response.write(html); response.end(); }).listen(8081, "0.0.0.0"); var wss = new WebSocketServer( { server: app } ); wss.on( 'connection', function ( wsConnect ) { wsConnect.on( 'message', function ( message ) { console.log( message ); }); }); })
- require('fs') The Node.js file system module allows you to work with the file system on your computer. To include the File System module, use the require('fs') method
You can use the following to test the WebSocket over SSL/TLS:
var ws = new WebSocket("wss://localhost:8080");But, the self-signed certificate can't pass the security enforced by the Web browser.
You need to add the self-signed root certificate in the Web browser.
- ERR_CERT_AUTHORITY_INVALID To fix it, you need to add your CA's certificate on the client. For Chrome, Settings --> Privacy and security --> Manage certificates (chrome://settings/certificates), on the "Authorities" tab, import your self-signed root certificate.
- ERR_CERT_COMMON_NAME_INVALID
Origin-Based Security Model
Origin validation is server-side verification.
This is used to check if the request's Origin is expected by the server.
留言