Home | History | Annotate | Download | only in tcpserver
      1 /*
      2 Copyright 2012 Google Inc.
      3 
      4 Licensed under the Apache License, Version 2.0 (the "License");
      5 you may not use this file except in compliance with the License.
      6 You may obtain a copy of the License at
      7 
      8      http://www.apache.org/licenses/LICENSE-2.0
      9 
     10 Unless required by applicable law or agreed to in writing, software
     11 distributed under the License is distributed on an "AS IS" BASIS,
     12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 See the License for the specific language governing permissions and
     14 limitations under the License.
     15 
     16 Author: Renato Mangini (mangini (at) chromium.org)
     17 */
     18 
     19 const DEFAULT_MAX_CONNECTIONS=5;
     20 
     21 (function(exports) {
     22 
     23   // Define some local variables here.
     24   var socket = chrome.sockets.tcpServer;
     25 
     26   /**
     27    * Creates an instance of the client
     28    *
     29    * @param {String} host The remote host to connect to
     30    * @param {Number} port The port to connect to at the remote host
     31    */
     32   function TcpServer(addr, port, options) {
     33     this.addr = addr;
     34     this.port = port;
     35     this.maxConnections = typeof(options) != 'undefined'
     36         && options.maxConnections || DEFAULT_MAX_CONNECTIONS;
     37 
     38     this._onAccept = this._onAccept.bind(this);
     39     this._onAcceptError = this._onAcceptError.bind(this);
     40 
     41     // Callback functions.
     42     this.callbacks = {
     43       listen: null,    // Called when socket is connected.
     44       connect: null,    // Called when socket is connected.
     45       disconnect: null, // Called when socket is disconnected.
     46       recv: null,       // Called when client receives data from server.
     47       sent: null        // Called when client sends data to server.
     48     };
     49 
     50     // Sockets open
     51     this.openSockets=[];
     52 
     53     // server socket (one server connection, accepts and opens one socket per client)
     54     this.serverSocketId = null;
     55 
     56     log('initialized tcp server, not listening yet');
     57   }
     58 
     59 
     60   /**
     61    * Static method to return available network interfaces.
     62    *
     63    * @see https://developer.chrome.com/apps/system_network#method-getNetworkInterfaces
     64    *
     65    * @param {Function} callback The function to call with the available network
     66    * interfaces. The callback parameter is an array of
     67    * {name(string), address(string)} objects. Use the address property of the
     68    * preferred network as the addr parameter on TcpServer contructor.
     69    */
     70   TcpServer.getNetworkAddresses=function(callback) {
     71     chrome.system.network.getNetworkInterfaces(callback);
     72   }
     73 
     74   TcpServer.prototype.isConnected=function() {
     75     return this.serverSocketId > 0;
     76   }
     77 
     78   /**
     79    * Connects to the TCP socket, and creates an open socket.
     80    *
     81    * @see https://developer.chrome.com/apps/sockets_tcpServer#method-create
     82    * @param {Function} callback The function to call on connection
     83    */
     84   TcpServer.prototype.listen = function(callback) {
     85     // Register connect callback.
     86     this.callbacks.connect = callback;
     87     socket.create({}, this._onCreate.bind(this));
     88   };
     89 
     90 
     91   /**
     92    * Disconnects from the remote side
     93    *
     94    * @see https://developer.chrome.com/apps/sockets_tcpServer#method-disconnect
     95    */
     96   TcpServer.prototype.disconnect = function() {
     97     if (this.serverSocketId) {
     98       socket.onAccept.removeListener(this._onAccept);
     99       socket.onAcceptError.removeListener(this._onAcceptError);
    100       socket.close(this.serverSocketId);
    101     }
    102     for (var i=0; i<this.openSockets.length; i++) {
    103       try {
    104         this.openSockets[i].close();
    105       } catch (ex) {
    106         console.log(ex);
    107       }
    108     }
    109     this.openSockets=[];
    110     this.serverSocketId=0;
    111   };
    112 
    113   /**
    114    * The callback function used for when we attempt to have Chrome
    115    * create a socket. If the socket is successfully created
    116    * we go ahead and start listening for incoming connections.
    117    *
    118    * @private
    119    * @see https://developer.chrome.com/apps/sockets_tcpServer#method-listen
    120    * @param {Object} createInfo The socket details
    121    */
    122   TcpServer.prototype._onCreate = function(createInfo) {
    123     this.serverSocketId = createInfo.socketId;
    124     if (this.serverSocketId > 0) {
    125       socket.onAccept.addListener(this._onAccept);
    126       socket.onAcceptError.addListener(this._onAcceptError);
    127       socket.listen(this.serverSocketId, this.addr, this.port, 50,
    128         this._onListenComplete.bind(this));
    129       this.isListening = true;
    130     } else {
    131       error('Unable to create socket');
    132     }
    133   };
    134 
    135   /**
    136    * The callback function used for when we attempt to have Chrome
    137    * connect to the remote side. If a successful connection is
    138    * made then we accept it by opening it in a new socket (accept method)
    139    *
    140    * @private
    141    */
    142   TcpServer.prototype._onListenComplete = function(resultCode) {
    143     if (resultCode !==0) {
    144       error('Unable to listen to socket. Resultcode='+resultCode);
    145     }
    146   }
    147 
    148   TcpServer.prototype._onAccept = function (info) {
    149     if (info.socketId != this.serverSocketId)
    150       return;
    151 
    152     if (this.openSockets.length >= this.maxConnections) {
    153       this._onNoMoreConnectionsAvailable(info.clientSocketId);
    154       return;
    155     }
    156 
    157     var tcpConnection = new TcpConnection(info.clientSocketId);
    158     this.openSockets.push(tcpConnection);
    159 
    160     tcpConnection.requestSocketInfo(this._onSocketInfo.bind(this));
    161     log('Incoming connection handled.');
    162   }
    163 
    164   TcpServer.prototype._onAcceptError = function(info) {
    165     if (info.socketId != this.serverSocketId)
    166       return;
    167 
    168     error('Unable to accept incoming connection. Error code=' + info.resultCode);
    169   }
    170 
    171   TcpServer.prototype._onNoMoreConnectionsAvailable = function(socketId) {
    172     var msg="No more connections available. Try again later\n";
    173     _stringToArrayBuffer(msg, function(arrayBuffer) {
    174       chrome.sockets.tcp.send(socketId, arrayBuffer,
    175         function() {
    176           chrome.sockets.tcp.close(socketId);
    177         });
    178     });
    179   }
    180 
    181   TcpServer.prototype._onSocketInfo = function(tcpConnection, socketInfo) {
    182     if (this.callbacks.connect) {
    183       this.callbacks.connect(tcpConnection, socketInfo);
    184     }
    185   }
    186 
    187   /**
    188    * Holds a connection to a client
    189    *
    190    * @param {number} socketId The ID of the server<->client socket
    191    */
    192   function TcpConnection(socketId) {
    193     this.socketId = socketId;
    194     this.socketInfo = null;
    195 
    196     // Callback functions.
    197     this.callbacks = {
    198       disconnect: null, // Called when socket is disconnected.
    199       recv: null,       // Called when client receives data from server.
    200       sent: null        // Called when client sends data to server.
    201     };
    202 
    203     log('Established client connection. Listening...');
    204 
    205   };
    206 
    207   TcpConnection.prototype.setSocketInfo = function(socketInfo) {
    208     this.socketInfo = socketInfo;
    209   };
    210 
    211   TcpConnection.prototype.requestSocketInfo = function(callback) {
    212     chrome.sockets.tcp.getInfo(this.socketId,
    213       this._onSocketInfo.bind(this, callback));
    214   };
    215 
    216   /**
    217    * Add receive listeners for when a message is received
    218    *
    219    * @param {Function} callback The function to call when a message has arrived
    220    */
    221   TcpConnection.prototype.startListening = function(callback) {
    222     this.callbacks.recv = callback;
    223 
    224     // Add receive listeners.
    225     this._onReceive = this._onReceive.bind(this);
    226     this._onReceiveError = this._onReceiveError.bind(this);
    227     chrome.sockets.tcp.onReceive.addListener(this._onReceive);
    228     chrome.sockets.tcp.onReceiveError.addListener(this._onReceiveError);
    229 
    230     chrome.sockets.tcp.setPaused(this.socketId, false);
    231   };
    232 
    233   /**
    234    * Sets the callback for when a message is received
    235    *
    236    * @param {Function} callback The function to call when a message has arrived
    237    */
    238   TcpConnection.prototype.addDataReceivedListener = function(callback) {
    239     // If this is the first time a callback is set, start listening for incoming data.
    240     if (!this.callbacks.recv) {
    241       this.startListening(callback);
    242     } else {
    243       this.callbacks.recv = callback;
    244     }
    245   };
    246 
    247 
    248   /**
    249    * Sends a message down the wire to the remote side
    250    *
    251    * @see https://developer.chrome.com/apps/sockets_tcp#method-send
    252    * @param {String} msg The message to send
    253    * @param {Function} callback The function to call when the message has sent
    254    */
    255   TcpConnection.prototype.sendMessage = function(msg, callback) {
    256     _stringToArrayBuffer(msg + '\n', function(arrayBuffer) {
    257       chrome.sockets.tcp.send(this.socketId, arrayBuffer, this._onWriteComplete.bind(this));
    258     }.bind(this));
    259 
    260     // Register sent callback.
    261     this.callbacks.sent = callback;
    262   };
    263 
    264 
    265   /**
    266    * Disconnects from the remote side
    267    *
    268    * @see https://developer.chrome.com/apps/sockets_tcp#method-close
    269    */
    270   TcpConnection.prototype.close = function() {
    271     if (this.socketId) {
    272       chrome.sockets.tcp.onReceive.removeListener(this._onReceive);
    273       chrome.sockets.tcp.onReceiveError.removeListener(this._onReceiveError);
    274       chrome.sockets.tcp.close(this.socketId);
    275     }
    276   };
    277 
    278 
    279   /**
    280    * Callback function for when socket details (socketInfo) is received.
    281    * Stores the socketInfo for future reference and pass it to the
    282    * callback sent in its parameter.
    283    *
    284    * @private
    285    */
    286   TcpConnection.prototype._onSocketInfo = function(callback, socketInfo) {
    287     if (callback && typeof(callback)!='function') {
    288       throw "Illegal value for callback: "+callback;
    289     }
    290     this.socketInfo = socketInfo;
    291     callback(this, socketInfo);
    292   }
    293 
    294   /**
    295    * Callback function for when data has been read from the socket.
    296    * Converts the array buffer that is read in to a string
    297    * and sends it on for further processing by passing it to
    298    * the previously assigned callback function.
    299    *
    300    * @private
    301    * @see TcpConnection.prototype.addDataReceivedListener
    302    * @param {Object} readInfo The incoming message
    303    */
    304   TcpConnection.prototype._onReceive = function(info) {
    305     if (this.socketId != info.socketId)
    306       return;
    307 
    308     // Call received callback if there's data in the response.
    309     if (this.callbacks.recv) {
    310       log('onDataRead');
    311       // Convert ArrayBuffer to string.
    312       _arrayBufferToString(info.data, this.callbacks.recv.bind(this));
    313     }
    314   };
    315 
    316   TcpConnection.prototype._onReceiveError = function (info) {
    317     if (this.socketId != info.socketId)
    318       return;
    319     this.close();
    320   };
    321 
    322   /**
    323    * Callback for when data has been successfully
    324    * written to the socket.
    325    *
    326    * @private
    327    * @param {Object} writeInfo The outgoing message
    328    */
    329   TcpConnection.prototype._onWriteComplete = function(writeInfo) {
    330     log('onWriteComplete');
    331     // Call sent callback.
    332     if (this.callbacks.sent) {
    333       this.callbacks.sent(writeInfo);
    334     }
    335   };
    336 
    337 
    338 
    339   /**
    340    * Converts an array buffer to a string
    341    *
    342    * @private
    343    * @param {ArrayBuffer} buf The buffer to convert
    344    * @param {Function} callback The function to call when conversion is complete
    345    */
    346   function _arrayBufferToString(buf, callback) {
    347     var bb = new Blob([new Uint8Array(buf)]);
    348     var f = new FileReader();
    349     f.onload = function(e) {
    350       callback(e.target.result);
    351     };
    352     f.readAsText(bb);
    353   }
    354 
    355   /**
    356    * Converts a string to an array buffer
    357    *
    358    * @private
    359    * @param {String} str The string to convert
    360    * @param {Function} callback The function to call when conversion is complete
    361    */
    362   function _stringToArrayBuffer(str, callback) {
    363     var bb = new Blob([str]);
    364     var f = new FileReader();
    365     f.onload = function(e) {
    366         callback(e.target.result);
    367     };
    368     f.readAsArrayBuffer(bb);
    369   }
    370 
    371 
    372   /**
    373    * Wrapper function for logging
    374    */
    375   function log(msg) {
    376     console.log(msg);
    377   }
    378 
    379   /**
    380    * Wrapper function for error logging
    381    */
    382   function error(msg) {
    383     console.error(msg);
    384   }
    385 
    386   exports.TcpServer = TcpServer;
    387   exports.TcpConnection = TcpConnection;
    388 
    389 })(window);
    390