For this task, we will use the non-core websocket
module to create a pure WebSocket server that will receive and respond to WebSocket requests from the browser.
We'll create a new folder for our project which will hold two files: server.js
and client.html. server.js
. They provide the server-side websocket functionality and serve up the client.html
file. For the server-side WebSocket functionality, we also need to install the websocket
module:
npm install websocket
For more information on the websocket
module, see https://www.github.com/Worlize/WebSocket-Node.
A WebSocket is an HTTP upgrade. As such, WebSocket servers run on top of HTTP servers. So we'll require the http
and websocket
servers, plus we'll also load our client.html
file (which we'll be creating soon) and the url
module:
var http = require('http'), var WSServer = require('websocket').server; var url = require('url'), var clientHtml = require('fs').readFileSync('client.html'),
Now let's create the HTTP server, and supply it to a new WebSocket server:
var plainHttpServer = http.createServer(function (request, response) { response.writeHead(200, {'Content-type' : 'text/html'}); response.end(clientHtml); }).listen(8080); var webSocketServer = new WSServer({httpServer: plainHttpServer}); var accept = [ 'localhost', '127.0.0.1' ];
We bind our HTTP server to port 8080 because binding to ports lower than 1000 takes root access. This means our script would have to be executed with root privileges which is a bad idea. SeeChapter 10, Taking It Live, for more information and how to bind to the HTTP port (80) safely.
We've also created a new array, called accept
. We use this inside the WebSocket server to restrict which originating sites can connect. For our example, we only allow connections from localhost or 127.0.0.1. If we were hosting live we would include any domains pointing to our server in the accept
array.
Now that we have our webSocketServer
instance, we can listen to its request
event and respond accordingly:
webSocketServer.on('request', function (request) { request.origin = request.origin || '*'; //no origin? Then use * as wildcard. if (accept.indexOf(url.parse(request.origin).hostname) === -1) { request.reject(); console.log('disallowed ' + request.origin); return; } var websocket = request.accept(null, request.origin); websocket.on('message', function (msg) { console.log('Recieved "' + msg.utf8Data + '" from ' + request.origin); if (msg.utf8Data === 'Hello') { websocket.send('WebSockets!'), } }); websocket.on('close', function (code, desc) { console.log('Disconnect: ' + code + ' - ' + desc); }); });
In our request
event callback, we conditionally accept the request, then listen for the message
and close
events, responding with WebSockets! if the message from the client is Hello
.
Now for the client, we'll place the following HTML structure:
<html> <head> </head> <body> <input id=msg><button id=send>Send</button> <div id=output></div> <script> //client side JavaScript will go here </script> </body> </html>
And the content of our script
tags should look as follows:
<script> (function () { var ws = new WebSocket("ws://localhost:8080"), output = document.getElementById('output'), send = document.getElementById('send'), function logStr(eventStr, msg) { return '<div>' + eventStr + ': ' + msg + '</div>'; } send.addEventListener('click', function () var msg = document.getElementById('msg').value; ws.send(msg); output.innerHTML += logStr('Sent', msg); }); ws.onmessage = function (e) { output.innerHTML += logStr('Recieved', e.data); }; ws.onclose = function (e) { output.innerHTML += logStr('Disconnected', e.code + '-' + e.type); }; ws.onerror = function (e) { output.innerHTML += logStr('Error', e.data); }; }()); </script>
If we initialize our server with node server.js
, then direct our (WebSocket-compliant) browser to http://localhost:8080
, type Hello
in the textbox, and click on Send. The terminal console will output:
Recieved "Hello" from http://localhost:8080
And our browser will show that Hello was sent and WebSockets! was received, as shown in the following screenshot:
We could use our textbox to send any string we like to our server, but only Hello will gain a response.
In server.js
, when we require the websocket
module's server
method, we load a constructor function into WSServer
, (which is why we capitalized the first letter). We initialize WSServer
using new
and pass in our plainHttpServer
which transforms it into a WebSocket-enabled server.
The HTTP server will still serve normal HTTP requests, but when it receives a WebSocket connection handshake the webSocketServer
kicks into action to establish a persistent connection to the client.
As soon as the client.html
file is loaded in the browser (served by the HTTP server in server.js)
and the inline script is executed, the WebSocket upgrade request is made to the server.
When the server receives this WebSocket upgrade request, webSocketServer
emits a request
event, which we scrutinize with our accept
array before deciding whether we will respond.
Our accept
array holds a whitelist of hosts we'll allow to interface with our WebSocket server. We gain some security by only allowing known sources to use our WebSocket server.
Inside the webSocketServer request
event, request.origin
is parsed with url.parse
to retrieve the host name section of the origin
URL. If the host name isn't found in our accept
whitelist we call request.reject
.
If our originating host passes, we create a websocket
variable from request.accept
. The first parameter of request.accept
allows us to define a custom sub-protocol. We could create an array of websockets using multiple request.accepts
with different sub-protocols which represent different behaviors. When initiating our client, we would pass an additional argument containing that sub-protocol (for example, new WebSocket("ws://localhost:8080", 'myCustomProtocol'))
. However, we pass in null
, such functionality isn't required for our purposes. The second parameter allows us to inform request.accept
of the host we wish to allow (there is also a third that can be used for passing cookies).
For each message received from the client, WebSocket
emits a message
event. This is where we log the received data to the console
and check whether the incoming message is Hello
. If it is, we use the WebSocket.send
method to respond to the client with WebSockets!.
Finally, we listen for the close
event, to inform console
that the connection has been terminated.
WebSockets have so much potential for efficient, low latency real-time web apps, but compatibility can be a challenge. Let's look at some other uses for WebSockets, plus a tip on getting WebSockets to work in Firefox.
Firefox versions 6 to 11 do support WebSockets. However, they use a vendor prefix so our client.html
will not work on these Firefox versions.
To fix this, we simply prepend the following to the script in our client.html
file:
window.WebSocket = window.WebSocket || window.MozWebSocket;
If the WebSocket
API does not exist, we try MozWebSocket
.
The websocket
module also allows us to create a WebSocket client. We may wish to interface Node with a pre-existing WebSocket server, which is primarily for browser clients (if not, we are better off creating a simple TCP server. See Chapter 8, Integrating Network Paradigms).
So let's implement the same functionality in client.html
using Node. We'll create a new file in the same directory, calling it client.js:
var WSClient = require('websocket').client; new WSClient() .on('connect', function (connection) { var msg = 'Hello'; connection.send(msg); console.log('Sent: ' + msg); connection.on('message', function (msg) { console.log("Received: " + msg.utf8Data); }).on('close', function (code, desc) { console.log('Disconnected: ' + code + ' - ' + desc); }).on('error', function (error) { console.log("Error: " + error.toString()); }); }) .on('connectFailed', function (error) { console.log('Connect Error: ' + error.toString()); }) .connect('ws://localhost:8080/', null, 'http://localhost:8080'),
For brevity, we've simply hardcoded our msg
variable, though we could have used process.stdin
or process.argv
to input a custom message. We initialize a new client with the websocket
module's client
method. Then we immediately begin to listen for the connect
and connectFailed
events.
After the two on
methods, we chain the connect
method. The first parameter is our WebSocket server, the second is the protocol (remember, we have a null
protocol for request.accept
in our recipe), and the third defines the request.origin
value.
Origin protection is designed to prevent an attack that only works from browsers. So although we can manufacture origins outside of the browser, it doesn't pose the same threat. The biggest threat is from a JavaScript injection attack into a high traffic site, that could cause a large amount of unauthorized connections from an unintended origin resulting in Denial of Service. See Chapter 7, Implementing Security, Encryption, and Authentication.
18.225.10.116