Skip to content

Example Code Needs Attention #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ca751d7
New protocol / data encoding specification
rauchg Nov 11, 2010
3a79b02
Fixed syntax error
rauchg Nov 11, 2010
db03ea2
Added new evented streaming decoder
rauchg Nov 12, 2010
07e0744
Fixed encoder and added encoding tests. Now at 100% test coverage for…
rauchg Nov 12, 2010
efe074b
Started implementing the new Data API in the Client
rauchg Nov 13, 2010
67245d0
Finished decodeMessage function + tests
rauchg Nov 13, 2010
be05788
Heartbeat ping now uses correct message type
rauchg Nov 13, 2010
c97264a
Added _onData to handle data decoding
rauchg Nov 13, 2010
e5e84f3
Added encode() and decode() utility functions for tests that follow t…
rauchg Nov 13, 2010
252ff5b
Make sure Client#send applies the `j` annotation for JSON encoding
rauchg Nov 15, 2010
3f4c156
Added .swn to git ignore
rauchg Nov 15, 2010
c0d7f04
JSONP-polling tests adapted to new message encoding/decoding
rauchg Nov 15, 2010
8eecfd9
Adapted xhr-multipart tests to new message encoding/decoding
rauchg Nov 15, 2010
b98f9ce
XHR-Polling tests adapted to new message encoding/decoding
rauchg Nov 15, 2010
eaf900e
Added listen() helper to listener.js and transports.flashsocket.js tests
rauchg Nov 15, 2010
c4314f3
Add the ability to pass custom annotations to encode() tests helper
rauchg Nov 15, 2010
0c98a79
Started Changelog for 0.7
rauchg Nov 15, 2010
67996d8
Now really deprecated clientConnect/clientDisconnect
rauchg Nov 15, 2010
8b3f053
Added Realm
rauchg Nov 15, 2010
0ecb583
Added broadcastJSON method
rauchg Nov 15, 2010
3975761
Added message handler override for realm listeners
rauchg Nov 15, 2010
b3478d1
Removed unnecessary checks for attributes argument in Realm#on
rauchg Nov 15, 2010
b177531
Realms test passing
rauchg Nov 15, 2010
387199e
Added `ignoreEmptyOrigin` option to Client (pending documentation)
rauchg Nov 15, 2010
c2593b2
Added origin parameter for jsonp-polling tests get() utility
rauchg Nov 15, 2010
a31680d
More OCD-related work for 85 columns limit
rauchg Nov 15, 2010
aa9cb21
Added Origin verification for HTMLFile transport
rauchg Nov 15, 2010
928ecdd
Make sure to call verifyOrigin even if Origin header is not sent / is…
rauchg Nov 15, 2010
e729246
Added domain mismatch test
rauchg Nov 15, 2010
926868f
Test for disallowance of connection because of empty origin
rauchg Nov 15, 2010
1594fd7
Faster / more efficient WebSocket streaming parser
rauchg Nov 16, 2010
9d19ae6
Updated client
rauchg Nov 16, 2010
b57b00b
Added missing Parser#error method
rauchg Nov 16, 2010
9e69acd
Update example docs to be compatible with Node 0.2.5
dlo Nov 26, 2010
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ lib-cov
*.csv
*.dat
*.out
*.pid
*.pid.swp
*.swp
*.swo
*.swn
13 changes: 12 additions & 1 deletion History.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,15 @@ http://www.lightsphere.com/dev/articles/flash_socket_policy.html
- Flash at some point enables us to skip 843 checking altogether
- Tests compatibility
* Fixed connection timeout, noDelay and socket encoding for draft 76 (had been accidentally moved into the `else` block)
* Some stylistic fixes
* Some stylistic fixes

0.7.0 /

* [DEPRECATE] onClientConnect / onClientMessage / onClientDisconnect events. Please use .on('connection', function(conn){ conn.on('message', function(){}); conn.on('disconnect', function(){}); }); instead
* [DEPRECATE} .clientsIndex accessor. Please use .clients intead
* Improved session id generation mechanism
* Implemented new message encoding mechanism (read more about it in the README)
- Implemented message types properly
- Implemented new message encoder/decoder with annotations support
* Added `tests.js` set of testing helpers
* Added `.json()` and `.broadcastJSON()` to send and brodcast messages in JSON. For just encoding objects as json you can continue to use `.send({ your: 'object' })`, but if you wish to force the JSON encoding of other types (like Number), use `.json`
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ test-cov:
example:
node ./example/server.js

.PHONY: example
.PHONY: example
62 changes: 46 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ On the server:

server = http.createServer(function(req, res){
// your normal server code
res.writeHeader(200, {'Content-Type': 'text/html'});
res.writeBody('<h1>Hello world</h1>');
res.finish();
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Hello world</h1>');
res.end();
});

server.listen(80);
Expand Down Expand Up @@ -179,21 +179,51 @@ Methods:

## Protocol

One of the design goals is that you should be able to implement whatever protocol you desire without `Socket.IO` getting in the way. `Socket.IO` has a minimal, unobtrusive protocol layer, consisting of two parts:
In order to make polling transports simulate the behavior of a full-duplex WebSocket, a session protocol and a message framing mechanism are required.

* Connection handshake

This is required to simulate a full duplex socket with transports such as XHR Polling or Server-sent Events (which is a "one-way socket"). The basic idea is that the first message received from the server will be a JSON object that contains a session ID used for further communications exchanged between the client and server.

The concept of session also naturally benefits a full-duplex WebSocket, in the event of an accidental disconnection and a quick reconnection. Messages that the server intends to deliver to the client are cached temporarily until reconnection.

The implementation of reconnection logic (potentially with retries) is left for the user. By default, transports that are keep-alive or open all the time (like WebSocket) have a timeout of 0 if a disconnection is detected.

* Message batching
The session protocol consists of the generation of a session id that is passed to the client when the communication starts. Subsequent connections to the server within that session send that session id in the URI along with the transport type.

### Message encoding

(message type)":"(content length)":"(data)","

(message type) is a single digit that represents one of the known message types (described below).

Messages are buffered in order to optimize resources. In the event of the server trying to send multiple messages while a client is temporarily disconnected (eg: xhr polling), the messages are stacked and then encoded in a lightweight way, and sent to the client whenever it becomes available.
(content length) is the number of characters of (data)

Despite this extra layer, the messages are delivered unaltered to the various event listeners. You can `JSON.stringify()` objects, send XML, or even plain text.
(data) is the message

0 = force disconnection
No data or annotations are sent with this message (it's thus always sent as "0:0:,")

1 = message
Data format:
(annotations)":"(message)

Annotations are meta-information associated with a message to make the Socket.IO protocol extensible. They're conceptually similar to HTTP headers. They take this format:

[key[:value][\n key[:value][\n ...]]]

For example, when you `.send('Hello world')` within the realm `'chat'`, Socket.IO really is sending:

1:18:r:chat:Hello world,

Two annotations are used by the Socket.IO client: `r` (for `realm`) and `j` (for automatic `json` encoding / decoding of the message).

2 = heartbeat
Data format:
(heartbeat numeric index)

Example:
2:1:0,
2:1:1,

3 = session id handshake
Data format:
(session id)

Example:
3:3:253,

## Credits

Expand Down Expand Up @@ -224,4 +254,4 @@ 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.
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 2 additions & 2 deletions example/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
}

function esc(msg){
return msg.replace(/</g, '&lt;').replace(/>/g, '&gt;');
return String(msg).replace(/</g, '&lt;').replace(/>/g, '&gt;');
};

var socket = new io.Socket(null, {port: 8080, rememberTransport: false});
Expand Down Expand Up @@ -58,4 +58,4 @@ <h1>Sample chat client</h1>
</style>

</body>
</html>
</html>
2 changes: 1 addition & 1 deletion example/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ io.on('connection', function(client){
client.on('disconnect', function(){
client.broadcast({ announcement: client.sessionId + ' disconnected' });
});
});
});
122 changes: 77 additions & 45 deletions lib/socket.io/client.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
var urlparse = require('url').parse
, OutgoingMessage = require('http').OutgoingMessage
, Stream = require('net').Stream
, Decoder = require('./data').Decoder
, encode = require('./data').encode
, encodeMessage = require('./data').encodeMessage
, decodeMessage = require('./data').decodeMessage
, options = require('./utils').options
, encode = require('./utils').encode
, decode = require('./utils').decode
, merge = require('./utils').merge;

var Client = module.exports = function(listener, req, res, options, head){
process.EventEmitter.call(this);
this.listener = listener;
this.options(merge({
ignoreEmptyOrigin: true,
timeout: 8000,
heartbeatInterval: 10000,
closeTimeout: 0
Expand All @@ -19,39 +22,58 @@ var Client = module.exports = function(listener, req, res, options, head){
this._heartbeats = 0;
this.connected = false;
this.upgradeHead = head;
this.decoder = new Decoder();
this.decoder.on('data', this._onMessage.bind(this));
this._onConnect(req, res);
};

require('sys').inherits(Client, process.EventEmitter);

Client.prototype.send = function(message){
if (!this._open || !(this.connection.readyState === 'open' || this.connection.readyState === 'writeOnly')){
return this._queue(message);
Client.prototype.send = function(message, anns){
anns = anns || {};
if (typeof message == 'object'){
anns['j'] = null;
message = JSON.stringify(message);
}
this._write(encode(message));
return this;
return this.write('1', encodeMessage(message, anns));
};

Client.prototype.sendJSON = function(message, anns){
anns = anns || {};
anns['j'] = null;
return this.send(JSON.stringify(message), anns);
};

Client.prototype.broadcast = function(message){
Client.prototype.write = function(type, data){
if (!this._open) return this._queue(type, data);
return this._write(encode([type, data]));
}

Client.prototype.broadcast = function(message, anns){
if (!('sessionId' in this)) return this;
this.listener.broadcast(message, this.sessionId);
this.listener.broadcast(message, this.sessionId, anns);
return this;
};

Client.prototype._onMessage = function(data){
var messages = decode(data);
if (messages === false) return this.listener.options.log('Bad message received from client ' + this.sessionId);
for (var i = 0, l = messages.length, frame; i < l; i++){
frame = messages[i].substr(0, 3);
switch (frame){
case '~h~':
return this._onHeartbeat(messages[i].substr(3));
case '~j~':
messages[i] = JSON.parse(messages[i].substr(3));
break;
}
this.emit('message', messages[i]);
this.listener._onClientMessage(messages[i], this);
Client.prototype._onData = function(data){
this.decoder.add(data);
}

Client.prototype._onMessage = function(type, data){
switch (type){
case '0':
this._onDisconnect();
break;

case '1':
var msg = decodeMessage(data);
// handle json decoding
if ('j' in msg[1]) msg[0] = JSON.parse(msg[0]);
this.emit('message', msg[0], msg[1]);
break;

case '2':
this._onHeartbeat(data);
}
};

Expand Down Expand Up @@ -82,34 +104,42 @@ Client.prototype._onConnect = function(req, res){
};

Client.prototype._payload = function(){
var payload = [];

this._writeQueue = this._writeQueue || [];
this.connections++;
this.connected = true;
this._open = true;

if (!this.handshaked){
this._generateSessionId();
payload.push(this.sessionId);
this._writeQueue.unshift(['3', this.sessionId]);
this.handshaked = true;
}

payload = payload.concat(this._writeQueue || []);
this._writeQueue = [];
// we dispatch the encoded current queue
// in the future encoding will be handled by _write, that way we can
// avoid framing for protocols with framing built-in (WebSocket)
if (this._writeQueue.length){
this._write(encode(this._writeQueue));
this._writeQueue = [];
}

// if this is the first connection we emit the `connection` ev
if (this.connections === 1)
this.listener._onClientConnect(this);

if (payload.length) this._write(encode(payload));
if (this.connections === 1) this.listener._onClientConnect(this);
if (this.options.timeout) this._heartbeat();
// send the timeout
if (this.options.timeout)
this._heartbeat();
};

Client.prototype._heartbeat = function(){
var self = this;
this._heartbeatInterval = setTimeout(function(){
self.send('~h~' + ++self._heartbeats);
self.write('2', ++self._heartbeats);
self._heartbeatTimeout = setTimeout(function(){
self._onClose();
}, self.options.timeout);
}, self.options.heartbeatInterval);
}, this.options.heartbeatInterval);
};

Client.prototype._onHeartbeat = function(h){
Expand Down Expand Up @@ -153,31 +183,33 @@ Client.prototype._onDisconnect = function(){
}
};

Client.prototype._queue = function(message){
Client.prototype._queue = function(type, data){
this._writeQueue = this._writeQueue || [];
this._writeQueue.push(message);
this._writeQueue.push([type, data]);
return this;
};

Client.prototype._generateSessionId = function(){
this.sessionId = ++this.listener._clientCount; // REFACTORME
this.sessionId = Math.random().toString().substr(2); // REFACTORME
return this;
};

Client.prototype._verifyOrigin = function(origin){
var origins = this.listener.options.origins;
if (origins.indexOf('*:*') !== -1) {

if (origins.indexOf('*:*') !== -1)
return true;
}
if (origin) {

if (origin){
try {
var parts = urlparse(origin);
return origins.indexOf(parts.host + ':' + parts.port) !== -1 ||
origins.indexOf(parts.host + ':*') !== -1 ||
origins.indexOf('*:' + parts.port) !== -1;
return origins.indexOf(parts.host + ':' + parts.port) !== -1
|| origins.indexOf(parts.host + ':*') !== -1
|| origins.indexOf('*:' + parts.port) !== -1;
} catch (ex) {}
}
return false;

return this.options.ignoreEmptyOrigin;
};

for (var i in options) Client.prototype[i] = options[i];
for (var i in options) Client.prototype[i] = options[i];
Loading