Initial commit (some transports untested!)

This commit is contained in:
Guillermo Rauch
2010-03-16 17:53:52 -07:00
commit c8edad861a
19 changed files with 1021 additions and 0 deletions

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "lib/vendor/js-oo"]
path = lib/vendor/js-oo
url = git://github.com/visionmedia/js-oo.git
[submodule "test/client"]
path = test/client
url = git@github.com:RosePad/Socket.IO-client.git

214
README.md Normal file
View File

@@ -0,0 +1,214 @@
Socket.IO Server: Sockets for the rest of us
============================================
The `Socket.IO` server provides seamless supports for a variety of transports intended for realtime communication
- WebSocket (with Flash policy support)
- Server-Sent Events
- XHR Polling
- XHR Multipart Streaming
- Forever Iframe
Requirements
------------
- Node v0.1.32+
- [Socket.IO-client](http://github.com/RosePad/Socket.IO-client) to connect on the client side.
How to use
----------
`Socket.IO` is designed not to take over an entire port or Node `http.Server` instance. This means that if you choose your HTTP server to listen on the port 80, `socket.io` can intercept requests directed to it and the normal requests will still be served.
By default, the server will intercept requests that contain `socket.io` in the path / resource part of the URI. You can change this (look at the available options below).
var http = require('http'),
io = require('./socket.io/socket.io.js'),
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();
});
// socket.io, I choose you
io.listen(server);
Due to a lack of flexibility in the current Node HTTP server implementation, you'll have to patch Node before using `socket.io`.
In the node directory run:
patch -p1 < {../directory/to/socket.io-node}/patch/{node version}.patch
./configure
make
make test
sudo make install
On the client side, you should use [Socket.IO-client](https://github.com/RosePad/Socket.IO-client) to connect.
## Demo
To run the demo, go to `test` directory and run
sudo node server.js
and point your browser to http://localhost:8080. In addition to 8080, if the transport `flashsocket` is enabled, a server will be initialized to listen to requests on the port 843.
## Documentation
### Listener
io.listen(<http.Server>, [options])
Returns: a `Listener` instance
Public Properties:
- *server*
The instance of _process.http.Server_
- *options*
The passed in options combined with the defaults
- *clients*
An array of clients. Important: disconnected clients are set to null, the array is not spliced.
- *clientsIndex*
An object of clients indexed by their session ids.
Methods:
- *addListener(event, λ)*
Adds a listener for the specified event. Optionally, you can pass it as an option to `io.listen`, prefixed by `on`. For example: `onClientConnect: function(){}`
- *removeListener(event, λ)*
Remove a listener from the listener array for the specified event.
- *broadcast(message, [except])*
Broadcasts a message to all clients. There's an optional second argument which is an array of session ids or a single session id to avoid broadcasting to.
Options:
- *resource*
socket.io
The resource is what allows the `socket.io` server to identify incoming connections by `socket.io` clients. Make sure they're in sync.
- *transports*
['websocket', 'server-events', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling']
A list of the accepted transports.
- *timeout*
8000
Time it has to pass without a reconnection to consider a client disconnected. Applies to all transports.
- *log*
ƒ(){ sys.log }
The logging function. Defaults to outputting to stdout through `sys.log`
Events:
- *clientConnect(client)*
Fired when a client is connected. Receives the Client instance as parameter
- *clientMessage(message, client)*
Fired when a message from a client is received. Receives the message and Client instance as parameter
- *clientDisconnect(client)*
Fired when a client is disconnected. Receives the Client instance as parameter
Important note: `this` in the event listener refers to the `Listener` instance.
### Client
Client(listener, req, res)
Public Properties:
- *listener*
The `Listener` instance this client belongs to.
- *connected*
Whether the client is connected
- *connections*
Number of times the client connected
Methods:
- *send(message)*
Sends a message to the client
- *broadcast(message)*
Sends a message to all other clients. Equivalent to Listener::broadcast(message, client.sessionId)
## 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. It consists of two parts:
* 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 that will be used for further communication exchanged between the client and the server.
The concept of session also benefits naturally 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 the reconnection.
The implementation of reconnection logic (potentially with retries) is left for the user.
* Message batching
In order to optimize the resources, messages are batched. In the event of the server trying to send multiple messages while the client is temporarily disconnected (ie: xhr polling), and messages are stacked, or messages being stacked prior to the handshake being successful, a JSON object containing a list (array) of messages is sent to the client.
Despite this extra layer, your messages are delivered unaltered to the different event listeners. You can JSON.stringify() objects, send XML, or maybe plain text.
## Credits
Guillermo Rauch [guillermo@rosepad.com]
Special thanks to [Jonas Pfenniger](http://github.com/zimbatm) for his workaround patch to keep the HTTPConnection open after the request is successful.
## License
(The MIT License)
Copyright (c) 2009 RosePad &lt;dev@rosepad.com&gt;
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 the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
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.

8
lib/socket.io.js Normal file
View File

@@ -0,0 +1,8 @@
require.paths.unshift(__dirname + '/vendor/js-oo/lib');
require('oo');
var sys = require('sys'),
Listener = require('./socket.io/listener').Listener;
this.listen = function(server, options){
return new Listener(server, options);
};

91
lib/socket.io/client.js Normal file
View File

@@ -0,0 +1,91 @@
var sys = require('sys');
this.Client = Class({
init: function(listener, req, res){
this.listener = listener;
this.connections = 0;
this.connected = false;
this._onConnect(req, res);
},
send: function(message){
if (!this.connected || !(this.connection.readyState == 'open' || this.connection.readyState == 'writeOnly')) return this._queue(message);
sys.log('will deliver ' + message + ' to ' + this.sessionId);
this._write(JSON.stringify({messages: [message]}));
return this;
},
broadcast: function(message){
if (!('sessionId' in this)) return this;
this.listener.broadcast(message, this.sessionId);
return this;
},
_onMessage: function(data){
var messages = JSON.parse(data);
for (var i = 0, l = messages.length; i < l; i++){
this.listener._onClientMessage(messages[i], this);
}
},
_onConnect: function(req, res){
var self = this;
this.request = req;
this.response = res;
this.connection = this.request.connection;
if (this._disconnectTimeout) clearTimeout(this._disconnectTimeout);
},
_payload: function(){
var payload = [];
this.connections++;
this.connected = true;
if (!this.handshaked){
this._generateSessionId();
payload.push(JSON.stringify({
sessionid: this.sessionId
}));
this.handshaked = true;
}
payload = payload.concat(this._writeQueue || []);
this._writeQueue = [];
if (payload.length) this._write(JSON.stringify({messages: payload}));
if (this.connections == 1) this.listener._onClientConnect(this);
},
_onClose: function(){
var self = this;
this.connected = false;
this._disconnectTimeout = setTimeout(function(){
sys.log('timeout!');
self._onDisconnect();
}, this.listener.options.timeout);
},
_onDisconnect: function(){
if (!this.finalized){
this._writeQueue = [];
this.connected = false;
this.finalized = true;
if (this.handshaked) this.listener._onClientDisconnect(this);
}
},
_queue: function(message){
if (!('_writeQueue' in this)) this._writeQueue = [];
this._writeQueue.push(message);
return this;
},
_generateSessionId: function(){
if (this.sessionId) return this.listener.options.log('This client already has a session id');
this.sessionId = Math.random().toString().substr(2);
return this;
}
});

121
lib/socket.io/listener.js Normal file
View File

@@ -0,0 +1,121 @@
var url = require('url'),
sys = require('sys'),
Options = require('./util/options').Options,
Client = require('./client').Client,
Transports = {},
Listener = this.Listener = Class({
include: [process.EventEmitter.prototype, Options],
options: {
origins: '*:*',
resource: 'socket.io',
transports: ['websocket', 'server-events', 'flashsocket', 'htmlfile', 'xhr-multipart', 'xhr-polling'],
timeout: 8000,
log: function(message){
sys.log(message);
}
},
init: function(server, options){
process.EventEmitter.call(this);
this.server = server;
this.setOptions(options);
this.clients = [];
this.clientsIndex = {};
if (!(this.server instanceof process.http.Server)){
throw new Error('Please pass the result of http.createServer() to the listener');
}
var listener = (this.server._events['request'] instanceof Array)
? this.server._events['request'][0]
: this.server._events['request'];
if (listener){
var self = this;
this.server._events['request'] = function(req, res){
if (self.check(req, res)) return;
listener(req, res);
};
} else {
throw new Error('Couldn\'t find the `request` event in the HTTP server.');
}
this.options.transports.forEach(function(t){
if (!(t in Transports)){
Transports[t] = require('./transports/' + t)[t];
if (Transports[t].init) Transports[t].init(this);
}
}, this);
this.options.log('socket.io ready - accepting connections');
},
broadcast: function(message, except){
for (var i = 0, l = this.clients.length; i < l; i++){
if (this.clients[i] && (!except || [].concat(except).indexOf(this.clients[i].sessionId) == -1)){
this.clients[i].send(message);
}
}
return this;
},
check: function(req, res){
var path = url.parse(req.url).pathname, parts, cn;
if (path.indexOf('/' + this.options.resource) === 0){
parts = path.substr(1).split('/');
if (parts[2]){
cn = this._lookupClient(parts[2]);
if (cn){
cn._onConnect(req, res);
} else {
req.connection.close();
sys.log('Couldnt find client with session id "' + parts[2] + '"');
}
} else {
this._onConnection(parts[1], req, res);
}
return true;
}
return false;
},
_lookupClient: function(sid){
return this.clientsIndex[sid];
},
_onClientConnect: function(client){
if (!(client instanceof Client) || !client.sessionId){
return sys.log('Invalid client');
}
client.i = this.clients.length;
this.clients.push(client);
this.clientsIndex[client.sessionId] = client;
sys.log('Client '+ client.sessionId +' connected');
this.emit('clientConnect', client);
},
_onClientMessage: function(data, client){
this.emit('clientMessage', data, client);
},
_onClientDisconnect: function(client){
this.clientsIndex[client.sessionId] = null;
this.clients[client.i] = null;
sys.log('Client '+ client.sessionId +' disconnected');
this.emit('clientDisconnect', client);
},
// new connections (no session id)
_onConnection: function(transport, req, res){
if (this.options.transports.indexOf(transport) === -1){
req.connection.close();
return sys.log('Illegal transport "'+ transport +'"');
}
sys.log('Initializing client with transport "'+ transport +'"');
new Transports[transport](this, req, res);
}
});

View File

@@ -0,0 +1,25 @@
var websocket = require('./websocket').websocket,
tcp = require('tcp'),
listeners = [];
this.flashsocket = websocket.extend({});
this.flashsocket.init = function(listener){
listeners.push(listener);
};
tcp.createServer(function(socket){
socket.write('<?xml version="1.0"?>\n');
socket.write('<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">\n');
socket.write('<cross-domain-policy>\n');
listeners.forEach(function(l){
[].concat(l.options.origins).forEach(function(origin){
var parts = origin.split(':');
socket.write('<allow-access-from domain="' + parts[0] + '" to-ports="'+ parts[1] +'"/>\n');
});
});
socket.write('</cross-domain-policy>\n');
socket.close();
}).listen(843);

View File

@@ -0,0 +1,46 @@
var Client = require('../client').Client,
qs = require('querystring');
this['htmlfile'] = Client.extend({
_onConnect: function(req, res){
switch (req.method){
case 'GET':
var self = this;
this.__super__(req, res);
this.request.addListener('end', function(){
if (!('hijack' in self.connection)){
throw new Error('You have to patch Node! Please refer to the README');
}
self.connection.addListener('end', function(){ self._onClose(); });
self.connection.hijack();
self.connection.setTimeout(0);
});
this.response.writeHead(200, { 'Content-type': 'text/html' });
this.response.flush();
this._payload();
break;
case 'POST':
req.addListener('data', function(message){
try {
var msg = qs.parse(message);
self._onMessage(msg.data);
} catch(e){}
res.writeHead(200);
res.write('ok');
res.close();
});
break;
}
},
_write: function(message){
// not sure if this is enough escaping. looks lousy
this.response.write("<script>parent.callback('"+ message.replace(/'/, "\'") +"')</script>");
}
});

View File

@@ -0,0 +1,45 @@
var Client = require('../client').Client,
qs = require('querystring');
this['server-events'] = Client.extend({
_onConnect: function(req, res){
switch (req.method){
case 'GET':
var self = this;
this.__super__(req, res);
this.request.addListener('end', function(){
if (!('hijack' in self.connection)){
throw new Error('You have to patch Node! Please refer to the README');
}
self.connection.addListener('end', function(){ self._onClose(); });
self.connection.hijack();
self.connection.setTimeout(0);
});
this.response.writeHead(200, { 'Content-type': 'application/x-dom-event-stream' });
this.response.flush();
this._payload();
break;
case 'POST':
req.addListener('data', function(message){
try {
var msg = qs.parse(message);
self._onMessage(msg.data);
} catch(e){}
res.writeHead(200);
res.write('ok');
res.close();
});
break;
}
},
_write: function(message){
this.response.write("Event: socket.io\ndata: " + message);
}
});

View File

@@ -0,0 +1,61 @@
var Client = require('../client').Client,
url = require('url'),
sys = require('sys');
this.websocket = Client.extend({
_onConnect: function(req, res){
var self = this;
this.__super__(req, res);
if (this.request.headers['connection'] !== 'Upgrade'
|| this.request.headers['upgrade'] !== 'WebSocket'
|| !this._verifyOrigin(this.request.headers['origin'])){
this.listener.options.log('WebSocket connection invalid');
this.connection.close();
return;
}
this.request.addListener('end', function(){
if (!('hijack' in self.connection)){
throw new Error('You have to patch Node! Please refer to the README');
}
self.connection.hijack();
self.connection.setTimeout(0);
self.connection.setNoDelay(true);
self.connection.addListener('end', function(){ self._onClose(); });
self.connection.addListener('data', function(data) {
if (data[0] !== '\u0000' && data[data.length - 1] !== '\ufffd'){
self.connection.close();
} else {
self._onMessage(data.substr(1, data.length - 2));
}
});
});
// this.response.use_chunked_encoding_by_default = false;
this.response.writeHeader(101, 'Web Socket Protocol Handshake', {
'Upgrade': 'WebSocket',
'Connection': 'Upgrade',
'WebSocket-Origin': this.request.headers.origin,
'WebSocket-Location': 'ws://' + this.request.headers.host + this.request.url
});
this.response.flush();
this._payload();
},
_verifyOrigin: function(origin){
var parts = url.parse(origin);
return this.listener.options.origins.indexOf('*:*') !== -1
|| this.listener.options.origins.indexOf(parts.host + ':' + parts.port) !== -1
|| this.listener.options.origins.indexOf(parts.host + ':*') !== -1
|| this.listener.options.origins.indexOf('*:' + parts.port) !== -1;
},
_write: function(message){
this.connection.write('\u0000' + message + '\uffff');
}
});

View File

@@ -0,0 +1,51 @@
var Client = require('../client').Client,
qs = require('querystring');
this['xhr-multipart'] = Client.extend({
_onConnect: function(req, res){
var self = this;
switch (req.method){
case 'GET':
var self = this;
this.request.addListener('end', function(){
if (!('hijack' in self.connection)){
throw new Error('You have to patch Node! Please refer to the README');
}
self.connection.addListener('end', function(){ self._onClose(); });
self.connection.hijack();
self.connection.setTimeout(0);
self.response.use_chunked_encoding_by_default = false;
});
this.response.writeHead(200, {
'Content-type': 'multipart/x-mixed-replace;boundary=socketio'
});
this.response.write("--socketio\r\n");
this.response.flush();
this._payload();
break;
case 'POST':
req.addListener('data', function(message){
try {
var msg = qs.parse(message);
self._onMessage(msg.data);
} catch(e){}
res.writeHead(200);
res.write('ok');
res.close();
});
break;
}
},
_write: function(message){
this.response.write("Content-type: text/plain\r\n");
this.response.write(message);
this.response.write("\r\n--socketio\r\n");
}
});

View File

@@ -0,0 +1,44 @@
var Client = require('../client').Client,
qs = require('querystring'),
sys = require('sys');
this['xhr-polling'] = Client.extend({
options: {
duration: 20000
},
_onConnect: function(req, res){
var self = this;
switch (req.method){
case 'GET':
this.__super__(req, res);
this._closeTimeout = setTimeout(function(){
self._write('');
}, this.options.duration);
this._payload();
break;
case 'POST':
req.addListener('data', function(message){
try {
var msg = qs.parse(message);
self._onMessage(msg.data);
} catch(e){}
res.writeHead(200);
res.write('ok');
res.close();
});
break;
}
},
_write: function(message){
if (this._closeTimeout) clearTimeout(this._closeTimeout);
this.response.writeHead(200, {'Content-Type': 'text/plain', 'Content-Length': message.length});
this.response.write(message);
this.response.close();
this._onClose();
}
});

View File

@@ -0,0 +1,16 @@
// Based on Mixin.js from MooTools (MIT)
// Copyright (c) 2006-2009 Valerio Proietti, <http://mad4milk.net/>
this.flatten = function(arr){
var array = [];
for (var i = 0, l = arr.length; i < l; i++){
var item = arr[i];
if (item != null) array = array.concat(item instanceof Array ? array.flatten(item) : item);
}
return array;
};
this.include = function(arr, item){
if (arr.indexOf(item) == -1) arr.push(item);
return arr;
};

View File

@@ -0,0 +1,38 @@
// Based on Mixin.js from MooTools (MIT)
// Copyright (c) 2006-2009 Valerio Proietti, <http://mad4milk.net/>
var clone = this.clone = function(item){
var clone;
if (item instanceof Array){
clone = [];
for (var i = 0; i < item.length; i++) clone[i] = clone(item[i]);
return clone;
} else if (typeof item == 'object') {
clone = {};
for (var key in object) clone[key] = clone(object[key]);
return clone;
} else {
return item;
}
},
mergeOne = function(source, key, current){
if (current instanceof Array){
source[key] = clone(current);
} else if (typeof current == 'object'){
if (typeof source[key] == 'object') object.merge(source[key], current);
else source[key] = clone(current);
} else {
source[key] = current;
}
return source;
};
this.merge = function(source, k, v){
if (typeof k == 'string') return mergeOne(source, k, v);
for (var i = 1, l = arguments.length; i < l; i++){
var object = arguments[i];
for (var key in object) mergeOne(source, key, object[key]);
}
return source;
};

View File

@@ -0,0 +1,28 @@
// Based on Mixin.js from MooTools (MIT)
// Copyright (c) 2006-2009 Valerio Proietti, <http://mad4milk.net/>
var object = require('./object'), sys = require('sys');
this.Options = {
options: {},
setOption: function(key, value){
object.merge(this.options, key, value);
return this;
},
setOptions: function(options){
for (var key in options) this.setOption(key, options[key]);
if (this.addListener){
for (var i in this.options){
if (!(/^on[A-Z]/).test(i) || typeof this.options[i] != 'function') continue;
this.addListener(i.replace(/^on([A-Z])/, function(full, first){
return first.toLowerCase();
}), this.options[i]);
this.options[i] = null;
}
}
return this;
}
};

1
lib/vendor/js-oo vendored Submodule

Submodule lib/vendor/js-oo added at 1f94bd8979

83
patch/0.1.32.patch Normal file
View File

@@ -0,0 +1,83 @@
diff -rup node-v0.1.32-orig/src/node_http.cc node-v0.1.32/src/node_http.cc
--- node-v0.1.32-orig/src/node_http.cc 2010-03-13 13:14:00.000000000 -0800
+++ node-v0.1.32/src/node_http.cc 2010-03-13 13:23:48.000000000 -0800
@@ -57,6 +57,7 @@ HTTPConnection::Initialize (Handle<Objec
client_constructor_template->InstanceTemplate()->SetInternalFieldCount(1);
client_constructor_template->SetClassName(String::NewSymbol("Client"));
NODE_SET_PROTOTYPE_METHOD(client_constructor_template, "resetParser", ResetParser);
+ NODE_SET_PROTOTYPE_METHOD(client_constructor_template, "hijack", Hijack);
target->Set(String::NewSymbol("Client"), client_constructor_template->GetFunction());
t = FunctionTemplate::New(NewServer);
@@ -64,6 +65,7 @@ HTTPConnection::Initialize (Handle<Objec
server_constructor_template->Inherit(Connection::constructor_template);
server_constructor_template->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_PROTOTYPE_METHOD(server_constructor_template, "resetParser", ResetParser);
+ NODE_SET_PROTOTYPE_METHOD(server_constructor_template, "hijack", Hijack);
server_constructor_template->SetClassName(String::NewSymbol("ServerSideConnection"));
end_symbol = NODE_PSYMBOL("end");
@@ -101,6 +103,14 @@ Handle<Value> HTTPConnection::ResetParse
}
+Handle<Value> HTTPConnection::Hijack(const Arguments& args) {
+ HandleScope scope;
+ HTTPConnection *connection = ObjectWrap::Unwrap<HTTPConnection>(args.Holder());
+ connection->Hijack();
+ return Undefined();
+}
+
+
void
HTTPConnection::OnReceive (const void *buf, size_t len)
{
@@ -109,6 +119,11 @@ HTTPConnection::OnReceive (const void *b
assert(refs_);
size_t nparsed;
+ if (hijacked) {
+ Connection::OnReceive(buf, len);
+ return;
+ }
+
nparsed = http_parser_execute(&parser_, static_cast<const char*>(buf), len);
if (nparsed != len) {
diff -rup node-v0.1.32-orig/src/node_http.h node-v0.1.32/src/node_http.h
--- node-v0.1.32-orig/src/node_http.h 2010-03-13 13:14:00.000000000 -0800
+++ node-v0.1.32/src/node_http.h 2010-03-13 13:25:05.000000000 -0800
@@ -12,17 +12,21 @@ public:
static void Initialize (v8::Handle<v8::Object> target);
static v8::Persistent<v8::FunctionTemplate> client_constructor_template;
- static v8::Persistent<v8::FunctionTemplate> server_constructor_template;
+ static v8::Persistent<v8::FunctionTemplate> server_constructor_template;
protected:
static v8::Handle<v8::Value> NewClient (const v8::Arguments& args);
static v8::Handle<v8::Value> NewServer (const v8::Arguments& args);
static v8::Handle<v8::Value> ResetParser(const v8::Arguments& args);
+ static v8::Handle<v8::Value> Hijack(const v8::Arguments& args);
+
+ bool hijacked;
HTTPConnection (enum http_parser_type t)
: Connection()
{
type_ = t;
+ hijacked = false;
ResetParser();
}
@@ -41,6 +45,10 @@ protected:
parser_.data = this;
}
+ void Hijack() {
+ hijacked = true;
+ }
+
void OnReceive (const void *buf, size_t len);
void OnEOF ();

77
test/chat.html Normal file
View File

@@ -0,0 +1,77 @@
<!doctype html>
<html>
<head>
<title>socket.io client test</title>
<script src="/client/lib/vendor/js-oo/lib/oo.js"></script>
<script src="/client/lib/vendor/LABjs/LAB.src.js"></script>
<script src="/client/lib/io.js"></script>
<script src="/client/lib/util/util.js"></script>
<script src="/client/lib/util/object.js"></script>
<script src="/client/lib/util/array.js"></script>
<script src="/client/lib/util/options.js"></script>
<script src="/client/lib/util/events.js"></script>
<script src="/client/lib/util/json.js"></script>
<script src="/client/lib/transport.js"></script>
<script src="/client/lib/transports/xhr.js"></script>
<script src="/client/lib/transports/websocket.js"></script>
<script src="/client/lib/transports/flashsocket.js"></script>
<script src="/client/lib/transports/htmlfile.js"></script>
<script src="/client/lib/transports/server-events.js"></script>
<script src="/client/lib/transports/xhr-multipart.js"></script>
<script src="/client/lib/transports/xhr-polling.js"></script>
<script src="/client/lib/socket.js"></script>
</head>
<body>
<script>
io.path = '/client/';
function message(obj){
var el = document.createElement('p');
if ('announcement' in obj) el.innerHTML = '<em>' + obj.announcement;
else if ('message' in obj) el.innerHTML = '<b>' + obj.message[0] + ':</b> ' + obj.message[1];
document.getElementById('chat').appendChild(el);
document.getElementById('chat').scrollTop = 1000000;
}
function send(){
var val = document.getElementById('text').value;
socket.send(val);
message({ message: ['you', val] });
document.getElementById('text').value = '';
}
var socket = new io.Socket('localhost', {rememberTransport: false, transports:['xhr-polling'], port: 8080});
socket.connect();
socket.addEvent('message', function(data){
var obj = io.util.JSON.decode(data);
if ('buffer' in obj){
document.getElementById('form').style.display='block';
document.getElementById('chat').innerHTML = '';
for (var i in obj.buffer) message(obj.buffer[i]);
} else message(obj);
});
</script>
<h1>Sample chat client</h1>
<div id="chat"><p>Connecting...</p></div>
<form id="form" onsubmit="send(); return false">
<input type="text" autocomplete="off" id="text"><input type="submit" value="Send">
</form>
<style>
#chat { height: 300px; overflow: auto; width: 800px; border: 1px solid #eee; font: 13px Helvetica, Arial; }
#chat p { padding: 8px; margin: 0; }
#chat p:nth-child(odd) { background: #F6F6F6; }
#form { width: 782px; background: #333; padding: 5px 10px; display: none; }
#form input[type=text] { width: 700px; padding: 5px; background: #fff; border: 1px solid #fff; }
#form input[type=submit] { cursor: pointer; background: #999; border: none; padding: 6px 8px; -moz-border-radius: 8px; -webkit-border-radius: 8px; margin-left: 5px; text-shadow: 0 1px 0 #fff; }
#form input[type=submit]:hover { background: #A2A2A2; }
#form input[type=submit]:active { position: relative; top: 2px; }
</style>
</body>
</html>

1
test/client Submodule

Submodule test/client added at 042b4d7f9a

65
test/server.js Normal file
View File

@@ -0,0 +1,65 @@
var http = require('http'),
url = require('url'),
fs = require('fs'),
io = require('../lib/socket.io'),
sys = require('sys'),
send404 = function(res){
res.writeHead(404);
res.write('404');
res.close();
},
server = http.createServer(function(req, res){
// your normal server code
var path = url.parse(req.url).pathname;
switch (path){
case '/':
res.writeHead(200, {'Content-Type': 'text/html'});
res.write('<h1>Welcome. Try the <a href="/chat.html">chat</a> example.</h1>');
res.close();
break;
default:
if (/\.(js|html|swf)$/.test(path)){
try {
var swf = path.substr(-4) == '.swf';
res.writeHead(200, {'Content-Type': swf ? 'application/x-shockwave-flash' : ('text/' + (path.substr(-3) == '.js' ? 'javascript' : 'html'))});
res.write(fs.readFileSync(__dirname + path, swf ? 'binary' : 'utf8'), swf ? 'binary' : 'utf8');
res.close();
} catch(e){
send404(res);
}
break;
}
send404(res);
break;
}
});
server.listen(8080);
// socket.io, I choose you
// simplest chat application evar
var buffer = [], json = JSON.stringify;
io.listen(server, {
onClientConnect: function(client){
client.send(json({ buffer: buffer }));
client.broadcast(json({ announcement: client.sessionId + ' connected' }));
},
onClientDisconnect: function(client){
client.broadcast(json({ announcement: client.sessionId + ' disconnected' }));
},
onClientMessage: function(message, client){
var msg = { message: [client.sessionId, message] };
buffer.push(msg);
if (buffer.length > 15) buffer.shift();
client.broadcast(json(msg));
}
});