Skip to content

Instantly share code, notes, and snippets.

@ql-owo-lp
Created May 17, 2012 15:19
Show Gist options
  • Save ql-owo-lp/2719598 to your computer and use it in GitHub Desktop.
Save ql-owo-lp/2719598 to your computer and use it in GitHub Desktop.
A JavsScript library that help managing HTML5's WebSocket
/*!
* WebSocket45 Javascript Library v0.1
* http://code.google.com/p/util45/
*
* Copyright 2011, Kevin Wang
* E-Mail : [email protected]
* Dual licensed under the MIT or GPL Version 2 licenses.
*/
/*
* Example :
var ws = new ws45('ws://...');
or
var ws = new ws45(wsObject/htmlObject);
arg : {
worker : URL (string)
}
*/
var ws45 = (function () {
// private global
var $$ws_arr = [],
$$tcp_send_timer = [],
$$tcp_receive_timer = [],
$$send_pool = [],
$$receive_pool = [],
$$debug = true,
// limit the speed of sending progress to avoid killing the browser
$$send_rate_limit = 10,
$$send_rate_brust = 14,
NULL = 0;
// hexed
var $$crc32_table = [0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,2932959818,3654703836,1088359270,936918000,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117];
// call a function
function _invoke($_func, $_self) {
return (function ($f,$s) {
return function () {
if ($f && $s[$f]) $s[$f].apply($s, arguments.length>0 ? Array.prototype.slice.call(arguments) : [] );
}
})($_func, $_self);
}
function _getTime() {
return (new Date()).getTime();
}
function _getRandString() {
return (''+Math.random()).replace(/^0\./,'');
}
function _debug($msg) {
if ($$debug)
console.log('WS45-'+ $msg);
}
function _json_reg($key) {
return eval('\/(?:,|{)\\s*(?:\"|\')?'+ $key +'(?:\"|\')?\\s*:\\s*(\\[?(?:(?:(?:\"|\')?.*(?:\"|\')?)|-?\\d*(?:\\.\\d+))+(?:\\s*,\\s*(?:(?:\"|\')?.*(?:\"|\')?)|-?\\d*(?:\\.\\d+))*\\]?)\\s*(?:,|})\/');
}
//return retry packet
function _retry_packet($_data) {
var typ = typeof($_data), val = null;
if ($.isEmptyObject($_data))
return null;
else if (typ === 'string') {
//try to extract some useful information from the fargment
var reg = {
from : _json_reg('from'),
type : _json_reg('type'),
series : _json_reg('series'),
offset : _json_reg('offset')
}
val = {};
$.each(reg, function (k,v) {
var match = $_data.match(v);
if (!match) {
_debug("Fail to exract information from bad packet | key : "+ k +" , value : "+ v);
return null;
}
_debug("Exract information from bad packet | key : "+ k +" , value : "+ match[1]);
val[k] = eval(match[1]);
});
}
else if (typ === 'object') {
val = {
from : $_data.from,
type : $_data.type || 0,
series : $_data.series || 0,
offset : $_data.offset || 0
};
}
// quit retrying for ack packets
if ($.isEmptyObject(val) || val.type)
return null;
// change packet type
val.type = $$b.packet.RESEND;
return val;
}
// return ack packet
function _ack_series_packet($_data) {
return {
series : $_data.series || 0,
type : $$b.packet.ACK_SERIES
};
}
// validate data
function _validate_pack($_data, $chk_mode) {
switch ($chk_mode || 0) {
case $$b.validate.CRC32 :
return $$b.validate.crc32($_data);
case $$b.validate.OFF :
default :
return null;
}
}
function _validate_unpack($_data, $chk_mode) {
switch ($chk_mode || 0) {
case $$b.validate.CRC32 :
return $$b.validate.crc32($_data);
case $$b.validate.OFF :
default :
return null;
}
}
// when receive nothing, request for resend
function _receive_pool_resend_packet($_data) {
var p = $$receive_pool[$_data.series];
// receive progress may have been finished
if (!p)
return;
var resend = [];
for (var i=0; i<$_data.slice_count; i++)
if (typeof p[i] === 'undefined')
resend.push(i);
var dat = $.extend({}, $_data);
// make offset an array
dat.offset = resend;
_debug('Offset need to resend-series('+ $_data.series +')-offsets['+ resend +']');
return _retry_packet(dat);
}
// constructor
var $$b = function ($_arg,$_prot) {
// private instance
var $self = this,
$id = null,
$ws = null,
$send_buffer_size = 1024,
$send_queue = [],
// default validate mode
$chk_mode = 1,
// current web socket belong to application ? | default null
$app = null,
// default 0 stands auto_close deactiviated
$auto_close_timeout = 0,
$auto_close_timer = null,
// default keep-alive every 7 seconds
$keep_alive_interval = 7000,
$keep_alive_timer = null,
// retry after how many seconds when nothing received, default 3 seconds
$tcp_send_timeout = 3000,
$tcp_receive_timeout = 3000,
$user_id = 0,
$retry = 3,
// after how many seconds delete the send cache
$send_cache_timeout = 60,
$last_activity = null,
// temporary cache that store the last message received
$temp = '',
$worker = null;
// return readyState
function _readyState() {};
_readyState.prototype.toString=function () {return $ws.readyState};
// return protocol
function _protocol() {};
_protocol.prototype.toString=function () {return $ws.protocol};
function _bufferedAmount() {};
_bufferedAmount.prototype.toString=function () {return $ws.bufferedAmount};
function _constructor($_arg, $_prot) {
// create new instance
if ($_arg instanceof WebSocket)
$ws = $_arg;
else if (typeof $_arg === 'string') {
if (!$$b.test($_arg))
return null;
else if ($_prot)
$ws = new WebSocket($_arg, $_prot);
else
$ws = new WebSocket($_arg);
}
else {
$_arg = $_arg || {};
}
// creating new WebSocket instance failed
if (!$ws) {
_debug('Web Socket fail to create');
return null;
}
// push to array and set id
$id = $$ws_arr.push($self)-1;
// in case of null value, test before revoke the function
$ws.onopen = onopen_delegate;
$ws.onmessage = onmessage_delegate;
$ws.onclose = _invoke('onclose', $self);
$ws.onerror = _invoke('onerror', $self);
$self.url = $ws.url;
return $self;
}
// for debug only
function _send($_data) {
if ($ws.readyState != $$b.OPEN) {
_debug('Send: Connection not open, wait in the queue');
$send_queue.push($_data);
return
}
var dat = $.toJSON($_data);
_debug("Send: "+ dat);
$ws.send(dat);
}
function _receive_timeout_check($_data) {
return function () {
// timeout
var _this = $$tcp_receive_timer[$_data.series], time = _getTime();
// null/NULL
if (!_this)
return;
if (time - _this.lastActivity > $tcp_receive_timeout) {
_debug('Recieve time out! Now request resending..');
// update last activity time
_this.lastActivity = time;
$self.send(_receive_pool_resend_packet($_data),{raw:true});
if (++_this.retry < $retry)
_this.timer=setTimeout(_receive_timeout_check($_data), $tcp_receive_timeout);
}
else {
// still have time, wait for another chance
clearTimeout(_this.timer);
_this.timer=setTimeout(_receive_timeout_check($_data), $tcp_receive_timeout-time+_this.lastActivity);
}
};
}
// process when the connection is time out
function _autoClose() {
if ($auto_close_timeout < 1)
return;
var now = _getTime();
// $auto_close_timeout - (now - $last_activity)
var time_diff = $auto_close_timeout - now + $last_activity;
if (time_diff<=0) {
$self.close();
$self.onautoclose();
}
else
$auto_close_timer = setTimeout(function () { _autoClose.call($self) }, time_diff);
}
// check if a series is finished, return null or the finished packet
function _swim_receive_pool($_data) {
var slice_count=0, finished=true, p=null;
// this packet only have one piece
if ($_data.slice_count <= 1)
return $self.onmessage.call($self, {data : $_data.data});
// check if all packets have been received
if (!$$receive_pool[$_data.series]) {
// null should not be processed
if ($$receive_pool[$_data.series] === NULL)
return;
// when the first slice arrived
p = $$receive_pool[$_data.series] = [];
// create timer
if (!$$tcp_receive_timer[$_data.series])
$$tcp_receive_timer[$_data.series] = {
lastActivity : _getTime() ,
timer : setTimeout(_receive_timeout_check($_data), $tcp_receive_timeout),
retry : 0
};
}
else {
p = $$receive_pool[$_data.series];
$$tcp_receive_timer[$_data.series].lastActivity = _getTime();
};
// only data is put into the pool
p[$_data.offset] = $_data.data;
for (var i=0; i<$_data.slice_count; i++) {
if (typeof p[i] === 'undefined') {
finished = false;
break;
}
}
if (finished) {
$self.onmessage.call($self, {
data : p.join('')
});
$self.send(_ack_series_packet($_data), {raw:true});
// release resource
delete p;
delete $$receive_pool[$_data.series];
// notice : after a successful receive, the value should be null instead of undefined. In case the ws receive information that already be fetched, pool marked with null should not accept new values.
$$receive_pool[$_data.series] = NULL;
if ($$tcp_receive_timer[$_data.series]) {
if ($$tcp_receive_timer[$_data.series].timer)
clearTimeout($$tcp_receive_timer[$_data.series].timer);
delete $$tcp_receive_timer[$_data.series];
$$tcp_receive_timer[$_data.series] = NULL;
}
}
}
// on receive an ack packet
function _swim_send_pool($_data) {
var offset_arr = [], p=null;
switch ($_data.type) {
case $$b.packet.ACK_SLICE:
if ($.isArray($_data.offset))
offset_arr = $_data.offset;
else
offset_arr = [$_data.offset];
p = $$send_pool[$_data.series];
$.each(offset_arr, function (k,v) { delete p[v]; p[v]=NULL; });
_debug('Pool cleared according to ACK_slice: series('+ $_data.series +')-offset('+ $_data.offset +')');
break;
case $$b.packet.ACK_SERIES:
delete $$send_pool[$_data.series];
$$send_pool[$_data.series] = NULL;
_debug('Pool cleared according to ACK_series: series('+ $_data.series +')');
break;
case $$b.packet.RESEND:
if ($.isArray($_data.offset))
offset_arr = $_data.offset;
else
offset_arr = [$_data.offset];
_debug('Resend packet: series('+ $_data.series +')-offset('+ offset_arr +')');
// null or undefined
if (!$$send_pool[$_data.series])
return;
p = $$send_pool[$_data.series];
$.each(offset_arr, function (k,v) {
if (p[v] == NULL)
return;
// reset timeout timer and retry hit
var retry = p[v].retry = (p[v].retry || 0) +1;
// retry reach the maxmium, give up
if (retry < $retry)
$self.send(p[v], {raw:true});
else
_debug('Retry reach the maxmium, give up! Packet: series('+ $_data.series +')-offset('+ v +')');
});
break;
}
}
function onmessage_delegate($_evt) {
var dat = null, str_data = $_evt.data;
// quit processing null or empty string
if ($.isEmptyObject(str_data)) {
//_debug('Keep-alive! last activity: '+ $last_activity);
return;
}
_debug('Receive-data-length('+str_data.length+'): '+str_data);
try {
dat = $.secureEvalJSON(str_data);
}
catch (e) {
// try to concat this to the last received message
if (/^\s*{\s*(?:'|")?.+(?:'|")?\s*}\s*$/.test(str_data))
; // do nothing
//match the head of a JSON
else if (/^\s*{\s*(?:'|")?/.test(str_data))
$temp = str_data;
// match the tail of a JSON
else if (/(?:'|")?\s*}\s*$/.test(str_data)) {
// try this again..
$temp+=str_data;
_debug('More data received! Concat and try again!');
return onmessage_delegate({data: $temp});
return;
}
else
$temp += str_data;
_debug('BAD PACKET! DUMP and WAIT for following packets! '+str_data);
// the bad packets should not ask for a resend, since the following data may be just on the way. Asking for a resend can cause more bad packets and eventually turns into a dead-loop.
return;
}
// receive null data
if (dat==null) {
_debug('RECEIVE NULL! DUMP!');
return;
}
// ack packets
if (dat.type) {
_debug('ACK packet received! data.type: '+dat.type);
return _swim_send_pool(dat);
}
if (dat.data.length!=dat.data_length || (dat.chk_mode && dat.chksum != _validate_unpack(dat.data, dat.chk_mode))) {
_debug('DATA INVALID! data.length: '+ dat.data.length +'; packet: '+ str_data);
$self.send(_retry_packet(dat),{raw:true});
return;
}
_swim_receive_pool(dat);
}
function onopen_delegate($_evt) {
$last_activity = _getTime();
_keep_alive();
$self.keepAlive();
for (var i=0; i<$send_queue.length; i++)
_send($send_queue.shift());
$self.onopen.call($self, $_evt);
}
function _keep_alive() {
// neither the server nor the client should process empty string
$last_activity = _getTime();
$ws.send('');
}
/*
* slice data and add tag, $_data is required
* return data of string array
*/
function _slice($_data,$_opt) {
var buff=[],len=$_data.length,start=0;
do {
buff.push($_data.substring(start, start+$send_buffer_size));
} while ((start += $send_buffer_size) < len);
return buff;
}
// tag all data in $_buff
function _tag($_buff, $_series, $_opt) {
// make a copy of buffer
var res = [];
for (var i=0, len=$_buff.length; i<len; i++) {
res.push({
from : $user_id,
to : $user_id,
series : $_series,
type : $_opt.type || $$b.packet.NORMAL,
offset : i,
app : $app,
slice_count : len,
chk_mode : $_opt.chk_mode || $chk_mode,
chksum : _validate_pack($_buff[i], $_opt.chk_mode || $chk_mode),
data_length : $_buff[i].length,
data : $_buff[i]
});
_debug('Tagged slice('+i+'): '+ $.toJSON(res[i]));
}
return res;
}
// a customed WebSocket class
$.extend($self, {
// public instance
// url default is null
url : null ,
readyState : new _readyState() ,
protocol : new _protocol() ,
bufferedAmount : new _bufferedAmount(),
//message process mode
mode : 0,
close : function () {
if ($auto_close_timer != null) {
clearTimeout($auto_close_timer);
$auto_close_timer = null;
}
$ws.close();
return $self;
},
/*
* $_opt : {
mode : {MSG_IMMIDIATE | MSG_WAITALL},
validate : boolean(default false),
timeout : (seconds) default disabled, specific how much time it is going to take before invoking ontimeout function
ontimeout : function ($_data){}
onprogress : function ($total, $finished) {}
oncomplete : function () {}
}
*/
send : function ($_data, $_opt) {
if ($.isEmptyObject($_data))
return $self;
$last_activity = _getTime();
$_opt = $_opt || {};
if ($_opt.raw)
_send($_data);
else {
var series = _getRandString();
var dat
= $$send_pool[series]
= _tag(_slice($_data, $_opt), series, $_opt);
$.each(dat, function (k,v) {
_send(v);
});
}
return $self;
},
// send every element in the array
sendArray : function ($_data_arr, $_opt) {
if ($.isEmptyObject($_data_arr) || !$.isArray($_data_arr) || $_data_arr.length<1)
return $self;
$last_activity = _getTime();
$_opt = $_opt || {};
var send_func = null;
if ($_opt.raw)
send_func = _send;
else
send_func = function ($_data) {$self.send($_data,$_opt)};
$.each($_data_arr, function (k,v) {
send_func(v);
});
return $self;
},
// automatically close connection, set $_timeout to -1 to turn autoclose off
autoClose : function ($_timeout, $_onautoclose) {
if (!$_timeout)
;
else if ($_timeout >0)
$auto_close_timeout = $_timeout * 1000;
else {
$auto_close_timeout = 0;
if ($auto_close_timer != null) {
clearTimeout($auto_close_timer);
$auto_close_timer = null;
}
return $self;
}
if ($_onautoclose)
$self.onautoclose = $_onautoclose;
if ($auto_close_timeout) {
_debug('AutoClose: Shut down keep alive to avert conflict');
$self.keepAlive(-1);
$auto_close_timer = setTimeout(function () { _autoClose.call($self) },
($last_activity!=null) ? ($auto_close_timeout - _getTime()+$last_activity) : $auto_close_timeout);
}
return $self;
},
// when autoclose is done
onautoclose : function () {},
// keep websocket alive
keepAlive : function ($_interval) {
if (!$_interval)
;
else if ($_interval > 0)
$keep_alive_interval = $_interval * 1000;
else {
$keep_alive_interval = 0;
if ($keep_alive_timer != null) {
clearTimeout($keep_alive_timer);
$keep_alive_timer = null;
}
return $self;
}
if ($keep_alive_interval) {
_debug('KeepAlive: Shut down auto close to avert conflict');
// shut down auto close
$self.autoClose(-1);
$keep_alive_timer = setInterval(function () {_keep_alive()},
($last_activity!=null) ? ($keep_alive_interval - _getTime()+$last_activity) : $keep_alive_interval);
}
return $self;
},
// how many times to retry
retry : 3,
ontimeout : function () {},
// delete all resource
dispose : function () {
$self.close();
delete $$ws_arr[$id];
$$ws_arr.length--;
delete $ws;
return $self;
},
// default socket
onopen : function () {},
onmessage : function () {},
onclose : function () {},
onerror : function () {}
});
// return constructor by default
return _constructor($_arg, $_prot) ;
};
return $.extend($$b, {
// public global static
// readonly
CONNECTING : 0 ,
OPEN : 1 ,
CLOSING : 2 ,
CLOSED : 3 ,
// test whether a object is a ws instance or ws url is valid
test : function ($_arg) {
if ($_arg instanceof $$b)
return ($_arg != null);
// whether the ws/wss protocol url is valid
else if (typeof $_arg === "string")
return /^wss?:\/\/(?:[\w-]+\.)+[\w-]+(?:\:\d+)?(?:\/[\w-\s.\/\?%&=]*)?$/.test($_arg);
else
return false;
} ,
// close instance
close : function ($_arg) {
if (typeof $_arg === 'undefined')
//close all websockets
for (target in $ws_arr)
if (target instanceof $$b)
target.close();
else if ($_arg instanceof $$b)
$_arg.close();
},
packet : {
ACK_SERIES : 2,
ACK_SLICE : 3,
RESEND : 1,
NORMAL : 0
},
validate : {
// mode
OFF : 0,
CRC32 : 1,
// calculate CRC32 check sum
crc32 : function ($str, $crc_start) {
var crc = ($crc_start || 0) ^ (-1);
for (var i = 0, len = $str.length; i < len; i++ )
crc = (crc >>> 8) ^ $$crc32_table[(crc ^ $str.charCodeAt(i)) & 0xFF];
return crc ^ (-1);
}
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment