Created
May 17, 2012 15:19
-
-
Save ql-owo-lp/2719598 to your computer and use it in GitHub Desktop.
A JavsScript library that help managing HTML5's WebSocket
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* 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