Compare commits

..

9 Commits

Author SHA1 Message Date
James Coglan 22a3507dad Correct EventSource error handler. 2012-01-13 21:24:54 +00:00
James Coglan 3ba69e4242 Correct variable name in WebSocket example. 2012-01-12 22:32:52 +00:00
James Coglan cc3581fd0c Reorder handlers in example server. 2012-01-12 20:18:00 +00:00
James Coglan 3488318ae6 Document the EventSource API. 2012-01-12 20:12:35 +00:00
James Coglan 91eb2642ee Store output socket as an instance variable in EventSource. 2012-01-12 19:53:06 +00:00
James Coglan 20dd83257a Implement EventSource connection handler. 2012-01-12 19:48:59 +00:00
James Coglan c82b59259f Remove some text about client versions from the README. 2012-01-09 01:52:58 +00:00
James Coglan 727aba743c Correct readme on default value of WebSocket#protocol. 2011-12-21 19:45:28 +00:00
James Coglan 0aac24bece Update repository location. 2011-12-21 17:05:33 +00:00
11 changed files with 363 additions and 112 deletions
+81 -6
View File
@@ -7,6 +7,11 @@ rather makes it easy to handle WebSocket connections within an existing
[Node](http://nodejs.org/) application. It does not provide any abstraction
other than the standard [WebSocket API](http://dev.w3.org/html5/websockets/).
It also provides an abstraction for handling [EventSource](http://dev.w3.org/html5/eventsource/)
connections, which are one-way connections that allow the server to push data to
the client. They are based on streaming HTTP responses and can be easier to
access via proxies than WebSockets.
The server-side socket can process [draft-75](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75),
[draft-76](http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76),
[hybi-07](http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07)
@@ -15,7 +20,7 @@ supports both `text` and `binary` messages, and transparently handles `ping`,
`pong`, `close` and fragmented messages.
## Accepting WebSocket connections in Node
## Handling WebSocket connections in Node
You can handle WebSockets on the server side by listening for HTTP Upgrade
requests, and creating a new socket for the request. This socket object exposes
@@ -49,8 +54,7 @@ server.listen(8000);
The client supports both the plain-text `ws` protocol and the encrypted `wss`
protocol, and has exactly the same interface as a socket you would use in a web
browser. On the wire it identifies itself as hybi-13, though it's compatible
with servers speaking later versions of the protocol.
browser. On the wire it identifies itself as hybi-13.
```js
var WebSocket = require('faye-websocket'),
@@ -115,15 +119,86 @@ messages.
sends a text or binary message over the connection to the other peer.
* <b><tt>close(code, reason)</tt></b> closes the connection, sending the given
status code and reason text, both of which are optional.
* <b><tt>protocol</tt></b> is a string or `null` identifying the subprotocol the
socket is using.
* <b><tt>protocol</tt></b> is a string (which may be empty) identifying the
subprotocol the socket is using.
## Handling EventSource connections in Node
EventSource connections provide a very similar interface, although because they
only allow the server to send data to the client, there is no `onmessage` API.
EventSource allows the server to push text messages to the client, where each
message has an optional event-type and ID.
```js
var WebSocket = require('faye-websocket'),
EventSource = WebSocket.EventSource,
http = require('http');
var server = http.createServer();
server.addListener('request', function(request, response) {
if (EventSource.isEventSource(request)) {
var es = new EventSource(request, response);
console.log('open', es.url, es.lastEventId);
// Periodically send messages
var loop = setInterval(function() { es.send('Hello') }, 1000);
es.onclose = function() {
clearInterval(loop);
es = null;
};
} else {
// Normal HTTP request
response.writeHead(200, {'Content-Type': 'text/plain'});
response.write('Hello');
response.end();
}
});
server.listen(8000);
```
The `send` method takes two optional parameters, `event` and `id`. The default
event-type is `'message'` with no ID. For example, to send a `notification`
event with ID `99`:
```js
es.send('Breaking News!', {event: 'notification', id: '99'});
```
The `EventSource` object exposes the following properties:
* <b><tt>url</tt></b> is a string containing the URL the client used to create
the EventSource.
* <b><tt>lastEventId</tt></b> is a string containing the last event ID
received by the client. You can use this when the client reconnects after a
dropped connection to determine which messages need resending.
When you initialize an EventSource with ` new EventSource()`, you can pass
configuration options after the `response` parameter. Available options are:
* <b><tt>retry</tt></b> is a number that tells the client how long (in seconds)
it should wait after a dropped connection before attempting to reconnect.
* <b><tt>ping</tt></b> is a number that tells the server how often (in seconds)
to send 'ping' packets to the client to keep the connection open, to defeat
timeouts set by proxies. The client will ignore these messages.
For example, this creates a connection that pings every 15 seconds and is
retryable every 10 seconds if the connection is broken:
```js
var es = new EventSource(request, response, {ping: 15, retry: 10});
```
## License
(The MIT License)
Copyright (c) 2009-2011 James Coglan
Copyright (c) 2009-2012 James Coglan
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
+44 -15
View File
@@ -6,6 +6,46 @@ var WebSocket = require('../lib/faye/websocket'),
var port = process.argv[2] || 7000,
secure = process.argv[3] === 'ssl';
var upgradeHandler = function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ['irc', 'xmpp']);
console.log('open', ws.url, ws.version, ws.protocol);
ws.onmessage = function(event) {
ws.send(event.data);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
};
var requestHandler = function(request, response) {
if (!WebSocket.EventSource.isEventSource(request))
return staticHandler(request, response);
var es = new WebSocket.EventSource(request, response),
time = parseInt(es.lastEventId, 10) || 0;
console.log('open', es.url, es.lastEventId);
var loop = setInterval(function() {
time += 1;
es.send('Time: ' + time);
setTimeout(function() {
if (es) es.send('Update!!', {event: 'update', id: time});
}, 1000);
}, 2000);
es.send('Welcome!\n\nThis is an EventSource server.');
es.onclose = function() {
clearInterval(loop);
console.log('close', es.url);
es = null;
};
};
var staticHandler = function(request, response) {
var path = request.url;
@@ -22,20 +62,9 @@ var server = secure
key: fs.readFileSync(__dirname + '/../spec/server.key'),
cert: fs.readFileSync(__dirname + '/../spec/server.crt')
})
: http.createServer(staticHandler);
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head, ['irc', 'xmpp']);
console.log('open', ws.url, ws.version, ws.protocol);
ws.onmessage = function(event) {
ws.send(event.data);
};
ws.onclose = function(event) {
console.log('close', event.code, event.reason);
ws = null;
};
});
: http.createServer();
server.addListener('request', requestHandler);
server.addListener('upgrade', upgradeHandler);
server.listen(port);
+39
View File
@@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>EventSource test</title>
</head>
<body>
<h1>EventSource test</h1>
<ul></ul>
<script type="text/javascript">
var logger = document.getElementsByTagName('ul')[0],
socket = new EventSource('/');
var log = function(text) {
logger.innerHTML += '<li>' + text + '</li>';
};
socket.onopen = function() {
log('OPEN');
};
socket.onmessage = function(event) {
log('MESSAGE: ' + event.data);
};
socket.addEventListener('update', function(event) {
log('UPDATE(' + event.lastEventId + '): ' + event.data);
});
socket.onerror = function(event) {
log('ERROR: ' + event.message);
};
</script>
</body>
</html>
+16 -12
View File
@@ -16,22 +16,26 @@
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/', protos),
index = 0;
socket.onopen = function() {
logger.innerHTML += '<li>OPEN: ' + socket.protocol + '</li>';
var log = function(text) {
logger.innerHTML += '<li>' + text + '</li>';
};
socket.addEventListener('open', function() {
log('OPEN: ' + socket.protocol);
socket.send('Hello, world');
};
socket.onerror = function(event) {
logger.innerHTML += '<li>ERROR: ' + error.message + '</li>';
};
socket.addEventListener('message', function(event) {
logger.innerHTML += '<li>MESSAGE: ' + event.data + '</li>';
setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
});
socket.onerror = function(event) {
log('ERROR: ' + event.message);
};
socket.onmessage = function(event) {
log('MESSAGE: ' + event.data);
setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
};
socket.onclose = function(event) {
logger.innerHTML += '<li>CLOSE: ' + event.code + ', ' + event.reason + '</li>';
log('CLOSE: ' + event.code + ', ' + event.reason);
};
</script>
+92
View File
@@ -0,0 +1,92 @@
var API = require('./websocket/api'),
Event = require('./websocket/api/event');
var isSecureConnection = function(request) {
if (request.headers['x-forwarded-proto']) {
return request.headers['x-forwarded-proto'] === 'https';
} else {
return (request.connection && request.connection.authorized !== undefined) ||
(request.socket && request.socket.secure);
}
};
var EventSource = function(request, response, options) {
options = options || {};
this._request = request;
this._response = response;
this._stream = response.socket;
this._ping = options.ping || this.DEFAULT_PING;
this._retry = options.retry || this.DEFAULT_RETRY;
var scheme = isSecureConnection(request) ? 'https:' : 'http:';
this.url = scheme + '//' + request.headers.host + request.url;
this.lastEventId = request.headers['last-event-id'] || '';
this.readyState = API.OPEN;
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
var self = this;
this._pingLoop = setInterval(function() {
try { this._stream.write(':\r\n\r\n') } catch (e) {}
}, this._ping * 1000);
['close', 'end', 'error'].forEach(function(event) {
self._stream.addListener(event, function() { self.close() });
});
var handshake = 'HTTP/1.1 200 OK\r\n' +
'Content-Type: text/event-stream\r\n' +
'Cache-Control: no-cache, no-store\r\n' +
'\r\n\r\n' +
'retry: ' + Math.floor(this._retry * 1000) + '\r\n\r\n';
try {
this._stream.write(handshake, 'utf8');
} catch (e) {}
};
EventSource.isEventSource = function(request) {
var accept = (request.headers.accept || '').split(/\s*,\s*/);
return accept.indexOf('text/event-stream') >= 0;
};
var instance = {
DEFAULT_PING: 10,
DEFAULT_RETRY: 5,
send: function(message, options) {
message = message.replace(/(\r\n|\r|\n)/g, '$1data: ');
options = options || {};
var frame = '';
if (options.event) frame += 'event: ' + options.event + '\r\n';
if (options.id) frame += 'id: ' + options.id + '\r\n';
frame += 'data: ' + message + '\r\n\r\n';
try {
this._stream.write(frame, 'utf8');
} catch (e) {}
},
close: function() {
if (this.readyState === API.CLOSING || this.readyState === API.CLOSED)
return;
this.readyState = API.CLOSED;
clearInterval(this._pingLoop);
this._response.end();
var event = new Event('close');
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
};
for (var key in API) EventSource.prototype[key] = API[key];
for (var key in instance) EventSource.prototype[key] = instance[key];
module.exports = EventSource;
+8 -6
View File
@@ -10,7 +10,8 @@
var Draft75Parser = require('./websocket/draft75_parser'),
Draft76Parser = require('./websocket/draft76_parser'),
HybiParser = require('./websocket/hybi_parser'),
API = require('./websocket/api');
API = require('./websocket/api'),
Event = require('./websocket/api/event');
var getParser = function(request) {
var headers = request.headers;
@@ -35,7 +36,7 @@ var WebSocket = function(request, socket, head, supportedProtos) {
this._stream = request.socket;
var scheme = isSecureConnection(request) ? 'wss:' : 'ws:';
this.url = scheme + '//' + request.headers.host + request.url;
this.url = scheme + '//' + request.headers.host + request.url;
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
@@ -49,7 +50,7 @@ var WebSocket = function(request, socket, head, supportedProtos) {
this.readyState = API.OPEN;
this.version = this._parser.getVersion();
var event = new API.Event('open');
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
@@ -65,9 +66,10 @@ var WebSocket = function(request, socket, head, supportedProtos) {
});
};
var API = require('./websocket/api');
for (var key in API) WebSocket.prototype[key] = API[key];
WebSocket.Client = require('./websocket/client');
module.exports = WebSocket;
WebSocket.WebSocket = WebSocket;
WebSocket.Client = require('./websocket/client');
WebSocket.EventSource = require('./eventsource');
module.exports = WebSocket;
+6 -65
View File
@@ -1,18 +1,15 @@
var EventTarget = require('./api/event_target'),
Event = require('./api/event');
var API = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
protocol: null,
receive: function(data) {
if (this.readyState !== API.OPEN) return false;
var event = new API.Event('message');
var event = new Event('message');
event.initEvent('message', false, false);
event.data = data;
this.dispatchEvent(event);
@@ -38,7 +35,7 @@ var API = {
var close = function() {
this.readyState = API.CLOSED;
this._stream.end();
var event = new API.Event('close', {code: code || 1000, reason: reason || ''});
var event = new Event('close', {code: code || 1000, reason: reason || ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
};
@@ -50,66 +47,10 @@ var API = {
if (this._parser.close) this._parser.close(code, reason);
close.call(this);
}
},
addEventListener: function(eventType, listener, useCapture) {
this._listeners = this._listeners || {};
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push(listener);
},
removeEventListener: function(eventType, listener, useCapture) {
if (!this._listeners || !this._listeners[eventType]) return;
if (!listener) {
delete this._listeners[eventType];
return;
}
var list = this._listeners[eventType],
i = list.length;
while (i--) {
if (listener !== list[i]) continue;
list.splice(i,1);
}
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = API.Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
if (!this._listeners || !this._listeners[event.type]) return;
this._listeners[event.type].forEach(function(listener) {
listener(event);
}, this);
}
};
var Event = function(eventType, options) {
this.type = eventType;
for (var key in options)
this[key] = options[key];
};
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
this.type = eventType;
this.bubbles = canBubble;
this.cancelable = cancelable;
};
Event.prototype.stopPropagation = function() {};
Event.prototype.preventDefault = function() {};
Event.CAPTURING_PHASE = 1;
Event.AT_TARGET = 2;
Event.BUBBLING_PHASE = 3;
API.Event = Event;
for (var key in EventTarget) API[key] = EventTarget[key];
module.exports = API;
+21
View File
@@ -0,0 +1,21 @@
var Event = function(eventType, options) {
this.type = eventType;
for (var key in options)
this[key] = options[key];
};
Event.prototype.initEvent = function(eventType, canBubble, cancelable) {
this.type = eventType;
this.bubbles = canBubble;
this.cancelable = cancelable;
};
Event.prototype.stopPropagation = function() {};
Event.prototype.preventDefault = function() {};
Event.CAPTURING_PHASE = 1;
Event.AT_TARGET = 2;
Event.BUBBLING_PHASE = 3;
module.exports = Event;
+47
View File
@@ -0,0 +1,47 @@
var Event = require('./event');
var EventTarget = {
onopen: null,
onmessage: null,
onerror: null,
onclose: null,
addEventListener: function(eventType, listener, useCapture) {
this._listeners = this._listeners || {};
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push(listener);
},
removeEventListener: function(eventType, listener, useCapture) {
if (!this._listeners || !this._listeners[eventType]) return;
if (!listener) {
delete this._listeners[eventType];
return;
}
var list = this._listeners[eventType],
i = list.length;
while (i--) {
if (listener !== list[i]) continue;
list.splice(i,1);
}
},
dispatchEvent: function(event) {
event.target = event.currentTarget = this;
event.eventPhase = Event.AT_TARGET;
if (this['on' + event.type])
this['on' + event.type](event);
if (!this._listeners || !this._listeners[event.type]) return;
this._listeners[event.type].forEach(function(listener) {
listener(event);
}, this);
}
};
module.exports = EventTarget;
+6 -5
View File
@@ -1,8 +1,9 @@
var API = require('./api'),
net = require('net'),
var net = require('net'),
tls = require('tls');
var HybiParser = require('./hybi_parser');
var HybiParser = require('./hybi_parser'),
API = require('./api'),
Event = require('./api/event');
var Client = function(url, protocols) {
this.url = url;
@@ -54,7 +55,7 @@ Client.prototype._onData = function(data) {
if (this._handshake.isValid()) {
this.protocol = this._handshake.protocol || '';
this.readyState = API.OPEN;
var event = new API.Event('open');
var event = new Event('open');
event.initEvent('open', false, false);
this.dispatchEvent(event);
@@ -62,7 +63,7 @@ Client.prototype._onData = function(data) {
} else {
this.readyState = API.CLOSED;
var event = new API.Event('close', {code: 1006, reason: ''});
var event = new Event('close', {code: 1006, reason: ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
+3 -3
View File
@@ -1,6 +1,6 @@
{ "name" : "faye-websocket"
, "description" : "Standards-compliant WebSocket server and client"
, "homepage" : "http://github.com/jcoglan/faye-websocket-node"
, "homepage" : "http://github.com/faye/faye-websocket-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"]
@@ -9,7 +9,7 @@
, "main" : "./lib/faye/websocket"
, "devDependencies" : {"jsclass": ""}
, "bugs" : "http://github.com/jcoglan/faye-websocket-node/issues"
, "bugs" : "http://github.com/faye/faye-websocket-node/issues"
, "licenses" : [ { "type" : "MIT"
, "url" : "http://www.opensource.org/licenses/mit-license.php"
@@ -17,7 +17,7 @@
]
, "repositories" : [ { "type" : "git"
, "url" : "git://github.com/jcoglan/faye-websocket-node.git"
, "url" : "git://github.com/faye/faye-websocket-node.git"
}
]
}