├── flash ├── jsSocket.swf ├── jsSocket2.swf ├── Makefile ├── jsSocket.as └── JsSocket.hx ├── README ├── examples ├── echo.rb └── stocks.rb └── js ├── jsonStringify.js └── jsSocket.js /flash/jsSocket.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/jssocket/master/flash/jsSocket.swf -------------------------------------------------------------------------------- /flash/jsSocket2.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/jssocket/master/flash/jsSocket2.swf -------------------------------------------------------------------------------- /flash/Makefile: -------------------------------------------------------------------------------- 1 | default: jsSocket.swf jsSocket2.swf 2 | 3 | jsSocket.swf: jsSocket.as 4 | mtasc jsSocket.as -version 8 -main -mx -header 800:600:31 -swf jsSocket.swf 5 | 6 | jsSocket2.swf: JsSocket.hx 7 | haxe -swf jsSocket2.swf -swf-version 9 -main JsSocket -swf-header 800:600:31 8 | -------------------------------------------------------------------------------- /flash/jsSocket.as: -------------------------------------------------------------------------------- 1 | import flash.external.ExternalInterface; 2 | 3 | class jsSocket { 4 | private var sock:XMLSocket; 5 | private var id:String; 6 | 7 | private function calljs(type, data) { 8 | ExternalInterface.call('jsSocket.callback', this.id, type, data); 9 | } 10 | 11 | public function jsSocket(id) { 12 | this.id = id; 13 | 14 | ExternalInterface.addCallback('open', this, open); 15 | ExternalInterface.addCallback('send', this, send); 16 | ExternalInterface.addCallback('close', this, close); 17 | 18 | this.calljs('onLoaded', true); 19 | } 20 | 21 | public function open(host, port) { 22 | System.security.loadPolicyFile('xmlsocket://' + host + ':' + port); 23 | sock = new XMLSocket(); 24 | 25 | var self = this; 26 | sock.onConnect = function(s) { self.calljs('onOpen', s); } 27 | sock.onData = function(d) { self.calljs('onData', d); } 28 | sock.onClose = function( ) { self.calljs('onClose'); } 29 | 30 | return sock.connect(host, port); 31 | } 32 | 33 | public function send(data) { 34 | if (data != 'null') // calling send() from js with no arguments sets data == 'null' 35 | return sock.send(data); 36 | } 37 | 38 | public function close() { 39 | sock.close(); 40 | sock.onClose(); 41 | } 42 | 43 | static function main(mc) { 44 | _root.jsSocket = new jsSocket(_root.id); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /flash/JsSocket.hx: -------------------------------------------------------------------------------- 1 | class JsSocket { 2 | static var socket:flash.net.Socket; 3 | static var id:String; 4 | static var buffer:String = ""; 5 | static var sizedReads:Bool = false; 6 | static var packetSize:Int = -1; 7 | static var slash:EReg = ~/\\/g; 8 | 9 | private static function calljs(type, data:Dynamic) { 10 | flash.external.ExternalInterface.call('jsSocket.callback', id, type, data); 11 | } 12 | 13 | private static function debug(data:Dynamic) { 14 | flash.external.ExternalInterface.call('console.log', data); 15 | } 16 | 17 | static function main() { 18 | id = flash.Lib.current.loaderInfo.parameters.id; 19 | sizedReads = flash.Lib.current.loaderInfo.parameters.sizedReads; 20 | 21 | flash.external.ExternalInterface.addCallback("open", open); 22 | flash.external.ExternalInterface.addCallback("send", send); 23 | flash.external.ExternalInterface.addCallback("close", close); 24 | 25 | calljs('onLoaded', true); 26 | } 27 | 28 | static function open(host, port) { 29 | flash.system.Security.loadPolicyFile('xmlsocket://' + host + ':' + port); 30 | socket = new flash.net.Socket(); 31 | 32 | socket.addEventListener(flash.events.Event.CONNECT, function(s){ 33 | calljs('onOpen', true); 34 | }); 35 | 36 | socket.addEventListener(flash.events.Event.CLOSE, function(e){ 37 | calljs('onClose', null); 38 | }); 39 | 40 | socket.addEventListener(flash.events.IOErrorEvent.IO_ERROR, function(e){ 41 | calljs('onClose', e.text); 42 | }); 43 | 44 | socket.addEventListener(flash.events.SecurityErrorEvent.SECURITY_ERROR, function(e){ 45 | calljs('onClose', e.text); 46 | }); 47 | 48 | socket.addEventListener(flash.events.ProgressEvent.SOCKET_DATA, function(d){ 49 | if (sizedReads) { 50 | while (socket.bytesAvailable > 0) { 51 | // figure out the packet size 52 | if (packetSize > -1) { 53 | // we have it already 54 | } else if (socket.bytesAvailable >= 2) { 55 | // read the packet size 56 | packetSize = socket.readShort(); 57 | } else { 58 | // lets wait 59 | break; 60 | } 61 | 62 | // read the packet, if possible 63 | if (socket.bytesAvailable >= packetSize) { 64 | calljs('onData', slash.replace(socket.readUTFBytes(packetSize), '\\\\')); 65 | packetSize = -1; 66 | } else { 67 | break; 68 | } 69 | } 70 | } else { 71 | var size = socket.bytesAvailable; 72 | var data = new flash.utils.ByteArray(); 73 | socket.readBytes(data); 74 | 75 | buffer += data.toString(); 76 | 77 | if (buffer.indexOf("\x00") > -1) { 78 | var packets = buffer.split("\x00"); 79 | while (packets.length > 1) { 80 | calljs('onData', packets.shift()); 81 | } 82 | buffer = packets.shift(); 83 | } 84 | } 85 | }); 86 | 87 | return socket.connect(host, Std.parseInt(port)); 88 | } 89 | 90 | static function send(data:String) { 91 | if (socket.connected && data.length > 0) { 92 | var t = new flash.utils.Timer(0, 1); 93 | t.addEventListener(flash.events.TimerEvent.TIMER, function(d){ 94 | socket.writeUTFBytes(data); 95 | socket.writeByte(0); 96 | socket.flush(); 97 | }); 98 | t.start(); 99 | 100 | return true; 101 | } else 102 | return false; 103 | } 104 | 105 | static function close() { 106 | if (socket.connected) 107 | socket.close(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | jsSocket: generic javascript socket API 2 | (c) 2008 Aman Gupta (tmm1) 3 | 4 | http://github.com/tmm1/jsSocket 5 | 6 | 7 | WHAT IS IT 8 | 9 | jsSocket uses Flash's XMLSocket to expose a simple socket API to javascript 10 | 11 | 12 | REQUIREMENTS 13 | 14 | jsonStringify (or json2) and jsSocket.swf 15 | mtasc (http://mtasc.org) or haxe (http://haxe.org) to re-compile the swfs from source 16 | 17 | 18 | FEATURES 19 | 20 | * multiple sockets on the same page 21 | * keepalive to keep connections open 22 | * automatic reconnect 23 | * status callback for custom connecting/disconnected/reconnecting UI 24 | * customizable logger for debugging 25 | 26 | 27 | WHY FLASH 28 | 29 | * high market penetration 30 | (according to a meebo study, more people had flash installed than javascript enabled) 31 | * single TCP connection (instead of one ajax connection per outgoing packet) 32 | * lower bandwidth usage and latency than long-polling 33 | 34 | 35 | CAVEATS 36 | 37 | * extra initial TCP connection required for security policy 38 | * packets must be delimited by null bytes 39 | * html and binary content must be base64 encoded 40 | 41 | 42 | USAGE 43 | 44 | - Specify the path to jsSocket.swf 45 | 46 | jsSocket.swf = '/flash/jsSocket.swf' 47 | 48 | 49 | - Connect to site.com on port 1234 50 | 51 | var socket = jsSocket() 52 | socket.connect('site.com', 1234) 53 | socket.send('hello world') 54 | 55 | 56 | - Connect to document.location.hostname on port 443 57 | 58 | var socket = jsSocket({ port: 443 }) 59 | 60 | 61 | - Install socket with default port and connect manually after setting data callback 62 | 63 | var socket = jsSocket({ port: 443, autoconnect: false }) 64 | socket.onData = function(data){ alert(data) } 65 | socket.connect() 66 | 67 | 68 | - Hook into connected/disconnected/data events 69 | 70 | var socket = jsSocket({ 71 | onOpen: function() { alert('connected') }, 72 | onData: function(data){ alert('got data: ' + data) }, 73 | onClose: function() { alert('disconnected') } 74 | }) 75 | 76 | 77 | - Disable keepalive pings and auto-reconnect 78 | 79 | var socket = jsSocket({ keepalive: false, autoreconnect: false }) 80 | socket.connect('site.com', 1234) 81 | 82 | 83 | - Send custom keepalive packets every minute 84 | 85 | var socket = jsSocket() 86 | socket.keepalive = function(){ socket.send('ping') } 87 | 88 | 89 | - Track the status of a socket connection 90 | 91 | var socket = jsSocket({ port: 443 }) 92 | socket.onStatus = function(type, val){ 93 | switch(type){ 94 | case 'connecting': // connecting to the server 95 | break 96 | 97 | case 'connected': // connected 98 | break 99 | 100 | case 'disconnected': // disconnected 101 | break 102 | 103 | case 'waiting': // waiting to reconnect in val seconds 104 | break 105 | 106 | case 'failed': // attempted max reconnects 107 | break 108 | } 109 | } 110 | 111 | 112 | - Debug jsSocket 113 | 114 | var socket = jsSocket({ debug: true }) 115 | socket.logger = console.log // log to firebug 116 | socket.logger = function(arg){ // log to a div 117 | if(!$('#logger')) 118 | $('
').attr('id', 'logger').appendTo('body') 119 | 120 | $('#logger').append( 121 | $('

').text(arg.toString()) 122 | ) 123 | } 124 | 125 | 126 | OTHER RESOURCES 127 | 128 | Alex MacCaw's Juggernaut: http://juggernaut.rubyforge.org 129 | jssockets (uses Flex/Flash9's RemotingSocket): http://code.google.com/p/jssockets 130 | XMLSocket bridge: http://www.devpro.it/xmlsocket/ 131 | FlashSocket: http://ionelmc.wordpress.com/2008/11/29/flash-socket-bridge-with-haxe/ 132 | socketBridge: http://matthaynes.net/blog/2008/07/17/socketbridge-flash-javascript-socket-bridge/ 133 | 134 | 135 | LICENSE 136 | 137 | Licensed under the Ruby License (http://www.ruby-lang.org/en/LICENSE.txt). 138 | -------------------------------------------------------------------------------- /examples/echo.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'eventmachine' 3 | require 'thin' 4 | require 'haml' 5 | 6 | __DIR__ = File.dirname File.expand_path(__FILE__) 7 | 8 | EM.run{ 9 | 10 | class FlashServer < EM::Connection 11 | def self.start host, port 12 | puts ">> FlashServer started on #{host}:#{port}" 13 | EM.start_server host, port, self 14 | end 15 | 16 | def post_init 17 | @buf = BufferedTokenizer.new("\0") 18 | @ip = Socket.unpack_sockaddr_in(get_peername).last rescue '0.0.0.0' 19 | puts ">> FlashServer got connection from #{@ip}" 20 | end 21 | 22 | def unbind 23 | @timer.cancel if @timer 24 | puts ">> FlashServer got disconnect from #{@ip}" 25 | end 26 | 27 | def receive_data data 28 | if data.strip == "" 29 | send %[ 30 | 31 | 32 | 33 | 34 | 35 | ] 36 | close_connection_after_writing 37 | return 38 | end 39 | 40 | @buf.extract(data).each do |packet| 41 | puts ">> FlashServer got packet from #{@ip}: #{packet}" 42 | send "you said: '#{packet}' from #{@ip} at #{Time.now}" 43 | end 44 | end 45 | 46 | def send data 47 | send_data "#{data}\0" 48 | end 49 | end 50 | 51 | class StaticApp 52 | def self.call env 53 | [ 54 | 200, 55 | {'Content-Type' => 'text/html'}, 56 | @page ||= Haml::Engine.new(%[ 57 | %html 58 | %head 59 | %title jsSocket example: #{File.basename __FILE__} 60 | %style{ :type => 'text/css'} 61 | :sass 62 | body 63 | margin: 1.5em 64 | font-size: 14pt 65 | font-family: monospace sans-serif 66 | #history 67 | height: 200px 68 | width: 90% 69 | overflow-y: scroll 70 | p 71 | margin: 0 72 | padding: 0 73 | form#input 74 | input 75 | width: 90% 76 | %body 77 | %h1 jsSocket example: #{File.basename __FILE__} 78 | 79 | #history 80 | %form#input 81 | %input{ :type => 'text' }/ 82 | 83 | %script{ :type => 'text/javascript', :src => 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js' }= '' 84 | %script{ :type => 'text/javascript', :src => '/js/jsonStringify.js' }= '' 85 | %script{ :type => 'text/javascript', :src => '/js/jsSocket.js' }= '' 86 | 87 | :javascript 88 | $socket = jsSocket({ port: 1234, 89 | debug: true, 90 | logger: console.log, 91 | onData: function(data){ 92 | $('#history').append( 93 | $('

').text(data) 94 | ).each(function(){ 95 | this.scrollTop = this.scrollHeight 96 | }) 97 | } 98 | }) 99 | $('form#input').submit(function(){ 100 | $socket.send($(this).find('input').val()) 101 | $(this).find('input').val('') 102 | return false 103 | }) 104 | ].gsub(/^ /,'')).render 105 | ] 106 | end 107 | end 108 | 109 | map = Rack::URLMap.new '/' => StaticApp, 110 | '/js' => Rack::File.new(__DIR__ + '/../js'), 111 | '/flash' => Rack::File.new(__DIR__ + '/../flash') 112 | 113 | http = Thin::Server.start 'localhost', 1233, map 114 | flash = FlashServer.start 'localhost', 1234 115 | 116 | } 117 | -------------------------------------------------------------------------------- /js/jsonStringify.js: -------------------------------------------------------------------------------- 1 | /* 2 | JSONstring v 1.01 3 | copyright 2006 Thomas Frank 4 | (small sanitizer added to the toObject-method, May 2008) 5 | 6 | This EULA grants you the following rights: 7 | 8 | Installation and Use. You may install and use an unlimited number of copies of the SOFTWARE PRODUCT. 9 | 10 | Reproduction and Distribution. You may reproduce and distribute an unlimited number of copies of the SOFTWARE PRODUCT either in whole or in part; each copy should include all copyright and trademark notices, and shall be accompanied by a copy of this EULA. Copies of the SOFTWARE PRODUCT may be distributed as a standalone product or included with your own product. 11 | 12 | Commercial Use. You may sell for profit and freely distribute scripts and/or compiled scripts that were created with the SOFTWARE PRODUCT. 13 | 14 | Based on Steve Yen's implementation: 15 | http://trimpath.com/project/wiki/JsonLibrary 16 | 17 | Sanitizer regExp: 18 | Andrea Giammarchi 2007 19 | 20 | */ 21 | 22 | JSONstring={ 23 | compactOutput:false, 24 | includeProtos:false, 25 | includeFunctions: false, 26 | detectCirculars:true, 27 | restoreCirculars:true, 28 | make:function(arg,restore) { 29 | this.restore=restore; 30 | this.mem=[];this.pathMem=[]; 31 | return this.toJsonStringArray(arg).join(''); 32 | }, 33 | toObject:function(x){ 34 | if(!this.cleaner){ 35 | try{this.cleaner=new RegExp('^("(\\\\.|[^"\\\\\\n\\r])*?"|[,:{}\\[\\]0-9.\\-+Eaeflnr-u \\n\\r\\t])+?$')} 36 | catch(a){this.cleaner=/^(true|false|null|\[.*\]|\{.*\}|".*"|\d+|\d+\.\d+)$/} 37 | }; 38 | if(!this.cleaner.test(x)){return {}}; 39 | eval("this.myObj="+x); 40 | if(!this.restoreCirculars || !alert){return this.myObj}; 41 | if(this.includeFunctions){ 42 | var x=this.myObj; 43 | for(var i in x){if(typeof x[i]=="string" && !x[i].indexOf("JSONincludedFunc:")){ 44 | x[i]=x[i].substring(17); 45 | eval("x[i]="+x[i]) 46 | }} 47 | }; 48 | this.restoreCode=[]; 49 | this.make(this.myObj,true); 50 | var r=this.restoreCode.join(";")+";"; 51 | eval('r=r.replace(/\\W([0-9]{1,})(\\W)/g,"[$1]$2").replace(/\\.\\;/g,";")'); 52 | eval(r); 53 | return this.myObj 54 | }, 55 | toJsonStringArray:function(arg, out) { 56 | if(!out){this.path=[]}; 57 | out = out || []; 58 | var u; // undefined 59 | switch (typeof arg) { 60 | case 'object': 61 | this.lastObj=arg; 62 | if(this.detectCirculars){ 63 | var m=this.mem; var n=this.pathMem; 64 | for(var i=0;i 0) 77 | out.push(',\n'); 78 | this.toJsonStringArray(arg[i], out); 79 | this.path.pop(); 80 | } 81 | out.push(']'); 82 | return out; 83 | } else if (typeof arg.toString != 'undefined') { 84 | out.push('{'); 85 | var first = true; 86 | for (var i in arg) { 87 | if(!this.includeProtos && arg[i]===arg.constructor.prototype[i]){continue}; 88 | this.path.push(i); 89 | var curr = out.length; 90 | if (!first) 91 | out.push(this.compactOutput?',':',\n'); 92 | this.toJsonStringArray(i, out); 93 | out.push(':'); 94 | this.toJsonStringArray(arg[i], out); 95 | if (out[out.length - 1] == u) 96 | out.splice(curr, out.length - curr); 97 | else 98 | first = false; 99 | this.path.pop(); 100 | } 101 | out.push('}'); 102 | return out; 103 | } 104 | return out; 105 | } 106 | out.push('null'); 107 | return out; 108 | case 'unknown': 109 | case 'undefined': 110 | case 'function': 111 | if(!this.includeFunctions){out.push(u);return out}; 112 | arg="JSONincludedFunc:"+arg; 113 | out.push('"'); 114 | var a=['\n','\\n','\r','\\r','"','\\"']; 115 | arg+=""; for(var i=0;i<6;i+=2){arg=arg.split(a[i]).join(a[i+1])}; 116 | out.push(arg); 117 | out.push('"'); 118 | return out; 119 | case 'string': 120 | if(this.restore && arg.indexOf("JSONcircRef:")==0){ 121 | this.restoreCode.push('this.myObj.'+this.path.join(".")+"="+arg.split("JSONcircRef:").join("this.myObj.")); 122 | }; 123 | out.push('"'); 124 | var a=['\n','\\n','\r','\\r','"','\\"']; 125 | arg+=""; for(var i=0;i<6;i+=2){arg=arg.split(a[i]).join(a[i+1])}; 126 | out.push(arg); 127 | out.push('"'); 128 | return out; 129 | default: 130 | out.push(String(arg)); 131 | return out; 132 | } 133 | } 134 | }; -------------------------------------------------------------------------------- /examples/stocks.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'eventmachine' 3 | require 'thin' 4 | require 'haml' 5 | require 'json' 6 | 7 | __DIR__ = File.dirname File.expand_path(__FILE__) 8 | 9 | EM.run{ 10 | 11 | class FlashServer < EM::Connection 12 | def self.start host, port 13 | puts ">> FlashServer started on #{host}:#{port}" 14 | EM.start_server host, port, self 15 | end 16 | 17 | def post_init 18 | @buf = BufferedTokenizer.new("\0") 19 | @ip = Socket.unpack_sockaddr_in(get_peername).last rescue '0.0.0.0' 20 | puts ">> FlashServer got connection from #{@ip}" 21 | end 22 | 23 | def unbind 24 | @timer.cancel if @timer 25 | puts ">> FlashServer got disconnect from #{@ip}" 26 | end 27 | 28 | def receive_data data 29 | if data.strip == "" 30 | send %[ 31 | 32 | 33 | 34 | 35 | 36 | ] 37 | close_connection_after_writing 38 | return 39 | end 40 | 41 | @buf.extract(data).each do |packet| 42 | puts ">> FlashServer got packet from #{@ip}: #{packet}" 43 | packet = JSON.parse(packet) # XXX: error handling goes here 44 | 45 | if stock = packet['stock'] 46 | # add stock to watch list 47 | (@stocks ||= []) << stock 48 | 49 | @timer ||= EM::PeriodicTimer.new(1) do 50 | # lookup (i.e. generate) stock prices and send to client 51 | @stocks.each do |s| 52 | send({ :time => Time.now.to_s, 53 | :stock => s, 54 | :price => rand(10_000)/100.0 }.to_json) 55 | end 56 | end 57 | end 58 | 59 | end 60 | end 61 | 62 | def send data 63 | send_data "#{data}\0" 64 | end 65 | end 66 | 67 | class StaticApp 68 | def self.call env 69 | [ 70 | 200, 71 | {'Content-Type' => 'text/html'}, 72 | @page ||= Haml::Engine.new(%[ 73 | %html 74 | %head 75 | %title jsSocket example: #{File.basename __FILE__} 76 | %style{ :type => 'text/css'} 77 | :sass 78 | body 79 | margin: 1.5em 80 | font-size: 14pt 81 | font-family: monospace sans-serif 82 | #history 83 | height: 200px 84 | width: 90% 85 | overflow-y: scroll 86 | p 87 | margin: 0 88 | padding: 0 89 | form#input 90 | input 91 | width: 60px 92 | %body 93 | %h1 jsSocket example: #{File.basename __FILE__} 94 | 95 | #history 96 | %form#input 97 | watch this stock: 98 | %input{ :type => 'text' }/ 99 | 100 | %script{ :type => 'text/javascript', :src => 'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js' }= '' 101 | %script{ :type => 'text/javascript', :src => '/js/jsonStringify.js' }= '' 102 | %script{ :type => 'text/javascript', :src => '/js/jsSocket.js' }= '' 103 | 104 | :javascript 105 | $socket = jsSocket({ port: 1234, 106 | debug: true, 107 | logger: console.log, 108 | onData: function(data){ 109 | $('#history').append( 110 | $('

').text(data) 111 | ).each(function(){ 112 | this.scrollTop = this.scrollHeight 113 | }) 114 | } 115 | }) 116 | $('form#input').submit(function(){ 117 | $socket.send({ stock: $(this).find('input').val() }) 118 | $(this).find('input').val('') 119 | return false 120 | }) 121 | ].gsub(/^ /,'')).render 122 | ] 123 | end 124 | end 125 | 126 | map = Rack::URLMap.new '/' => StaticApp, 127 | '/js' => Rack::File.new(__DIR__ + '/../js'), 128 | '/flash' => Rack::File.new(__DIR__ + '/../flash') 129 | 130 | http = Thin::Server.start 'localhost', 1233, map 131 | flash = FlashServer.start 'localhost', 1234 132 | 133 | } 134 | -------------------------------------------------------------------------------- /js/jsSocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jsSocket: generic javascript socket API 3 | * (c) 2008 Aman Gupta (tmm1) 4 | * 5 | * http://github.com/tmm1/jsSocket 6 | */ 7 | 8 | function jsSocket(args){ 9 | if ( this instanceof arguments.callee ) { 10 | if ( typeof this.init == "function" ) 11 | this.init.apply( this, (args && args.callee) ? args : arguments ); 12 | } else 13 | return new arguments.callee( arguments ); 14 | } 15 | 16 | // location of jsSocket.swf 17 | jsSocket.swf = "/flash/jsSocket.swf" 18 | 19 | // list of sockets 20 | jsSocket.sockets = {} 21 | 22 | // flash entry point for callbacks 23 | jsSocket.callback = function(id, type, data){ 24 | var sock = jsSocket.sockets[id] 25 | sock.callback.apply(sock, [type, data]) 26 | } 27 | 28 | jsSocket.prototype = { 29 | num: null, // numeric id 30 | id: null, // dom id 31 | 32 | wrapper: null, // dom container for flash object 33 | sock: null, // flash object 34 | 35 | host: null, // host == null means window.location.hostname 36 | port: null, // port to connect to (443 is recommended to get through firewalls) 37 | 38 | // toggle packet reading mode between null terminated or size prefixed 39 | // null terminated is the default to be bacwards compatible with flash8/AS2s XMLSocket 40 | // flash9/AS3 supports size prefixed mode which allows sending raw data without base64 41 | sizedReads: false, 42 | 43 | init: function(opts) { 44 | var self = this 45 | 46 | // update options 47 | for (key in opts) 48 | self[key] = opts[key] 49 | 50 | // don't autoconnect unless port is defined 51 | if (!self.port) 52 | self.autoconnect = false 53 | 54 | // assign unique id 55 | if (!self.num) { 56 | if (!jsSocket.id) 57 | jsSocket.id = 1 58 | 59 | self.num = jsSocket.id++ 60 | self.id = "jsSocket_" + self.num 61 | } 62 | 63 | // register with flash callback handler 64 | jsSocket.sockets[self.id] = self 65 | 66 | // install flash socket 67 | window.onload = function() { 68 | var flashvars = "id=" + self.id + "&sizedReads=" + self.sizedReads 69 | var wrapper = document.createElement('div') 70 | wrapper.id = 'jsSocketWrapper_' + self.num 71 | wrapper.style.position = 'absolute' 72 | wrapper.innerHTML = '' 73 | document.body.appendChild(wrapper) 74 | } 75 | 76 | window.onbeforeunload = function() { 77 | self.close() 78 | } 79 | }, 80 | 81 | 82 | loaded: false, // socket loaded into dom? 83 | connected: false, // socket connected to remote host? 84 | debug: false, // debugging enabled? 85 | 86 | autoconnect: true, // connect when flash loads 87 | autoreconnect: true, // reconnect on disconnect 88 | 89 | // send ping every minute 90 | keepalive: function(){ this.send({ type: 'ping' }) }, 91 | keepalive_timer: null, 92 | 93 | // reconnect logic (called if autoreconnect == true) 94 | reconnect: function(){ 95 | this.log('reconnecting') 96 | 97 | if (this.reconnect_interval) { 98 | clearInterval(this.reconnect_interval) 99 | } 100 | 101 | this.reconnect_countdown = this.reconnect_countdown * 2 102 | 103 | if (this.reconnect_countdown > 48) { 104 | this.log('reconnect failed, giving up') 105 | this.onStatus('failed') 106 | return 107 | } else { 108 | this.log('will reconnect in ' + this.reconnect_countdown) 109 | this.onStatus('waiting', this.reconnect_countdown) 110 | } 111 | 112 | var secs = 0, self = this 113 | 114 | this.reconnect_interval = setInterval(function(){ 115 | var remain = self.reconnect_countdown - ++secs 116 | if (remain == 0) { 117 | self.log('reconnecting now..') 118 | clearInterval(self.reconnect_interval) 119 | 120 | self.autoconnect = true 121 | self.remove() 122 | self.init() 123 | } else { 124 | self.log('reconnecting in '+remain) 125 | self.onStatus('waiting', remain) 126 | } 127 | }, 1000); 128 | }, 129 | reconnect_interval: null, 130 | reconnect_countdown: 3, 131 | 132 | // wrappers for flash functions 133 | 134 | // open/connect the socket 135 | // happens automatically if autoconnect is true 136 | open: function(host, port) { 137 | if (host) this.host = host 138 | if (port) this.port = port 139 | 140 | this.host = this.host || window.location.hostname 141 | if (!this.port) 142 | this.log('error: no port specified') 143 | 144 | this.onStatus('connecting') 145 | 146 | return this.sock.open(this.host, this.port); 147 | }, 148 | connect: function(){ this.open.apply(this, arguments) }, 149 | 150 | // send/write data to the socket 151 | // if argument is an object, it will be json-ified 152 | send: function(data) { 153 | if (typeof data == "object") { 154 | if ('JSONstring' in window) 155 | data = JSONstring.make(data) 156 | else if ('JSON' in window) 157 | data = JSON.stringify(data) 158 | } 159 | 160 | return this.sock.send(data); 161 | }, 162 | write: function(){ this.send.apply(this, arguments) }, 163 | 164 | // close/disconnect the socket 165 | close: function() { 166 | this.autoreconnect = true 167 | if (this.loaded && this.connected) 168 | this.sock.close() 169 | }, 170 | disconnect: function(){ this.close.apply(this) }, 171 | 172 | // uninstall the socket 173 | remove: function() { 174 | delete jsSocket.sockets[this.id] 175 | if (this.loaded && this.connected) 176 | this.sock.close() 177 | var wrapper = document.getElementById('jsSocketWrapper_'+this.num) 178 | wrapper.parentNode.removeChild(wrapper) 179 | }, 180 | 181 | // debugging 182 | 183 | log: function(){ 184 | if (!this.debug) return; 185 | 186 | arguments.slice = Array.prototype.slice 187 | var args = arguments.slice(0) 188 | 189 | if (this.logger) 190 | this.logger.apply(null, [[this.id].concat(args)]) 191 | }, 192 | 193 | // flash callback 194 | 195 | callback: function(type, data) { 196 | var self = this 197 | 198 | setTimeout(function(){ // wrap in setTimeout(.., 0) to free up flash's ExternalInterface 199 | switch(type){ 200 | case 'onLoaded': 201 | self.log('loaded') 202 | self.loaded = true 203 | self.sock = document.getElementById(self.id) 204 | 205 | if (self.autoconnect) 206 | self.connect() 207 | 208 | break 209 | 210 | case 'onOpen': 211 | if (data == true) { 212 | self.log('connected') 213 | self.connected = true 214 | 215 | if (self.keepalive) 216 | self.keepalive_timer = setInterval(function(){ 217 | self.keepalive.apply(self) 218 | }, 1*60*1000) 219 | 220 | self.reconnect_countdown = 3 221 | if (self.reconnect_interval) 222 | clearInterval(self.reconnect_interval) 223 | 224 | self.onStatus('connected') 225 | 226 | } else { 227 | self.log('connect failed') 228 | if (self.autoreconnect) 229 | self.reconnect() 230 | } 231 | 232 | break 233 | 234 | case 'onClose': 235 | self.connected = false 236 | self.log('disconnected') 237 | self.onStatus('disconnected') 238 | 239 | if (self.keepalive && self.keepalive_timer) { 240 | clearInterval(self.keepalive_timer) 241 | self.keepalive_timer = null 242 | } 243 | 244 | if (self.autoreconnect) 245 | self.reconnect() 246 | 247 | break 248 | 249 | case 'onData': 250 | self.log('got data: ', data) 251 | } 252 | 253 | self[type](data) 254 | }, 0) 255 | }, 256 | 257 | // callback hooks 258 | 259 | onLoaded: function(){}, 260 | onOpen: function(){}, 261 | onClose: function(){}, 262 | onStatus: function(){}, 263 | onData: function(){} 264 | } 265 | --------------------------------------------------------------------------------