4

I'm trying to implement a client-server communications between two or more plugins where each plugin is concurrently both server and client. I use nsIServerSocket for server part and websockets for client part. This is the code:

function startServer(port) {

var listener = {
onSocketAccepted: function(serverSocket, transport) {
    console.log("Accepted connection on " + transport.host + ":" + transport.port);
    var input = transport.openInputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0);//.QueryInterface(Ci.nsIAsyncInputStream);
    var output = transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0);
    var sin = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);

    try{
    sin.init(input);
    var readBytes = sin.available();
    var request = '';
    request = sin.read(readBytes);
    console.log('Received: ' + request);
    //getUrl(request);
    output.write("yes", "yes".length);
    output.flush();
    }
    finally{
    sin.close();
    input.close();
    output.close();
    }
}
}

try{
var serverSocket = Cc["@mozilla.org/network/server-socket;1"].createInstance(Ci.nsIServerSocket);
serverSocket.init(port, true, 5);
console.log("Opened socket on " + serverSocket.port);
serverSocket.asyncListen(listener);
}catch(e){
console.log(e);
}    
}

For server part, and the following for client part:

var address="ws://otherAddress:1234";// + port;
var window = Cc["@mozilla.org/appshell/appShellService;1"]
             .getService(Ci.nsIAppShellService)
             .hiddenDOMWindow;
ws = new window.WebSocket(address);

try{
ws.onmessage = function () {
};

ws.onopen = function(){
    console.log("connection opened");
    // Web Socket is connected. You can send data by send() method
    ws.send("lol ");
};

ws.onclose = function() {
    // websocket is closed. };
    console.log("websocket is closed");
}
}catch(evt){
console.log(evt.data);
}

The client code start when user click on a button....This code is partly working, because from console I see that when user click button, server receive the connection-open, but I can't receive the message......Anyone can help me? Thanks

UPDATE 1

the message that I see in console is like this:

"Received: GET / HTTP/1.1
Host: localhost:1234
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:27.0) Gecko/20100101 Firefox/27.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: resource://gre-resources
Sec-WebSocket-Key: zh/EpJRRsOAgLfPIbI1EDg==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

UPDATE 2 After nmaier and IvyLynx answers (thanks a lot!!), I modified my code inserting a full "ServerSocket" implementation (mainly because in future I will also be passing binary data). This is the code for a localhost case:

var {Cc, Ci, Cu, Cr, components} = require("chrome");

// the thread manager can be important when using asynchronous mode
var thread_manager = Cc["@mozilla.org/thread-manager;1"].getService();
var socket_service = Cc["@mozilla.org/network/socket-transportservice;1"].getService(Ci.nsISocketTransportService);

// make some constructors so we don't have to worry about this later
var socket = Cc["@mozilla.org/network/serversocket;1"].createInstance(Ci.nsIServerSocket);
// set the second argument to false if you want it to listen
// to connections beyond the computer the extension runs on
socket.init(-1, true, -1);
var output_stream_bin = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIBinaryOutputStream);
var input_stream_bin = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);

// this is so we can easily instantiate nsIInputStreamPump, which allows us to read input streams properly
var input_stream_pump_c = Cc["@mozilla.org/network/input-stream-pump;1"];
var input_stream_base, input_stream_async_c, input_stream_async, recieved_bytes, recieved_total, input_stream_pump;
var output_stream_base, output_stream_async_c, output_stream_async, generalStream;
var client, client_input_stream, client_output_stream, client_input_stream_pump;
var data_to_send = "hi hi"; // this holds what we want to send
var socket_transport = socket_service.createTransport(null, 0, "localhost", socket.port, null);

var socket_listener = {
   onSocketAccepted: function(socket, transport){

      client = transport;

      client_input_stream = client.openInputStream(0, 0, 0);
      client_output_stream = client.openOutputStream(0, 0, 0);
      client_output_stream.QueryInterface(Ci.nsIAsyncOutputStream);
      generalStream = client_output_stream;
      client_input_stream_pump[this_transport] = input_stream_pump_c.createInstance(Ci.nsIInputStreamPump);
      client_input_stream_pump[this_transport].init(client_input_stream, -1, -1, 0, 0, false);
      client_input_stream_pump[this_transport].asyncRead(socket_reader, socket);

   },
   onStopListening: function(socket, status){
   }
};

socket.asyncListen(socket_listener);

// this guy will get called when we're ready to send data
var output_stream_callback = {
   onOutputStreamReady: function(stream){          
      output_stream_bin.setOutputStream(stream);
      output_stream_bin.writeBytes(data_to_send, data_to_send.length);
      data_to_send = "";
   }
};

var socket_reader = {
   onDataAvailable: function(request, context, stream, offset, count){

      input_stream_bin.setInputStream(stream);

      if(input_stream_bin.available() > 0){
        recieved_bytes = input_stream_bin.readByteArray(count);
        recieved_total = "";

        // this loop converts bytes to characters
        // if you don't need to pass binary data around
        // you can just use nsIScriptableInputStream instead of
        // nsIBinaryInputStream and skip this
        for (var i = 0; i < recieved_bytes.length; i++){
            recieved_total += String.fromCharCode(recieved_bytes[i]);
        }
      console.log("Received " + recieved_total)
      }else{
        stream.close();
      }
   },
   onStartRequest: function(request, context){
   },
   onStopRequest: function(request, context, status){
   }
};

require("sdk/widget").Widget({
   id: "mozilla-link",
   label: "Mozilla website",
   contentURL: data.url("icon.png"),
   onClick: listTabs

});

function listTabs() {
   //console.log(client_output_stream);

   generalStream.asyncWait(output_stream_callback,0,0,thread_manager.mainThread);
};

The problem is the generalStream variable. I call asyncWait method when user click on extension icon, but I also insert the call in other methods. Each generalStream.asyncWait provocate the follow problem (where are ... in reality there are the path of the profile in wich the extension is executed):

console.error: client:
Message: TypeError: generalStream is undefined
Stack:
listTabs@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2NP
aadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/lo
ader.js -> resource://jid1-exo2npaaditkqg-at-jetpack/client/lib/main.js:742
_emitOnObject@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2N PaadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/l
oader.js -> resource://gre/modules/commonjs/sdk/deprecated/events.js:153
_emit@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2NPaadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://gre/modules/commonjs/sdk/deprecated/events.js:123 _onEvent@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2NPaadi
Tkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://gre/modules/commonjs/sdk/widget.js:278
WidgetView__onEvent@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2NPaadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://gre/modules/commonjs/sdk/widget.js:426
WC_addEventHandlers/listener/<@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2NPaadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://gre/modules/commonjs/sdk/widget.js:884
notify@resource://gre/modules/XPIProvider.jsm -> jar:file:///.../extensions/jid1-exo2NPaadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://gre/modules/commonjs/sdk/timers.js:40
mechalynx
  • 1,306
  • 1
  • 9
  • 24
hasmet
  • 758
  • 3
  • 13
  • 32
  • By "plugins" you actually mean extensions? Are these running in the same Firefox instance? – nmaier Jul 21 '14 at 14:59
  • Yes I mean extension, sorry. These are on different computers – hasmet Jul 21 '14 at 15:02
  • I've worked with this and I think I can answer it, but you don't mention whether the console messages get triggered or not when your listener tries to read the input stream. If there's an exception before they're executed, it's a different problem than just not having anything waiting in the stream to be read. – mechalynx Jul 21 '14 at 15:21
  • 2
    The "message" won't be posted, until your server socket performs a proper websocket protocol handshake (see my answer). – nmaier Jul 21 '14 at 15:33
  • @hasmet see? Now it's clear that nmaier is exactly correct about what your problem is :) – mechalynx Jul 21 '14 at 15:47
  • I've added an edit to my answer that concerns the addon-sdk and components object, it might help. – mechalynx Jul 22 '14 at 11:10

2 Answers2

4

nmaier is most probably correct on why your code doesn't work but I'll still post this answer as supplementary information

First of all, you don't need to use both WebSockets and XPCOM Sockets to create extensions that are both server and client. Either one of these technologies will suffice and work fine for this purpose. Personally, having done this myself, I'd recommend using XPCOM Sockets unless you really want to create portable code between browsers (seeing as you don't mention anything like that, I recommend dropping the WebSockets implementation and sticking to XPCOM Sockets - but WebSockets are easier to handle, so you might prefer them - it doesn't matter, just pick one). I'm mentioning this since you have both the WebSocket and the XPCOM Socket listening for connections and I think it's because you want this:

ServerSocket recieves client connection -> Only recieves from client connection

WebSocket connects to server connection -> Only sends to server connection

Instead, you can just have an nsIServerSocket that both reads and sends data to another server socket. Also, as nmaier says, blocking streams are a bad idea, unless you really need them and in this case, you don't. It'll probably help things work better too, if you use asynchronous mode.

Below is a sample implementation of what you want using nsIServerSocket, which I'm including because understanding how this works took me a lot of pain and time. If you don't want the power of native-application level tcp sockets (in other words, you don't need to read binary streams or do complex handling or write your own communication protocol), WebSockets are probably adequate for you and preferrable, as they'll handle a simple messaging system fine. Simply put, if the below is not your cup of tea, just stick to WebSockets until they're not capable of serving your needs.

Note: the following code makes no attempt at namespacing and promptly thrashes the global namespace - it isn't intended to be production code, so normally, you'd want all of these variables and objects wrapped up into your extension's namespacing object.

Here is the sample implementation with nsIServerSocket and binary streams:

Preparation code

// these aliases will shorten the code
var {
    utils: Cu,
    interfaces: Ci,
    classes: Cc,
    results: Cr,
    stack: Cs,
    manager: Cm,
    Exception: Ce,
    Constructor: CC,
} = Components;

// get related services
// the thread manager can be important when using asynchronous mode
var thread_manager = Cc["@mozilla.org/thread-manager;1"].getService();
var socket_service = Cc["@mozilla.org/network/socket-transport-service;1"].getService(Ci.nsISocketTransportService);

// make some constructors so we don't have to worry about this later
var socket_c = CC("@mozilla.org/network/server-socket;1", "nsIServerSocket", "init");
var output_stream_bin_c = CC("@mozilla.org/binaryoutputstream;1", "nsIBinaryOutputStream", "setOutputStream");
var input_stream_bin_c = CC("@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream");

// this is so we can easily instantiate nsIInputStreamPump, which allows us to read
// input streams properly
var input_stream_pump_c = Cc["@mozilla.org/network/input-stream-pump;1"];

// normally all these would be placed in a global object. they're declared here
// so we can instantiate them later, but this is just a sample, not production code!
var input_stream_base, input_stream_async_c, input_stream_async, input_stream_bin, recieved_bytes, recieved_total, input_stream_pump;
var output_stream_base, output_stream_async_c, output_stream_async, output_stream_bin;
var client, client_input_stream, client_output_stream, client_input_stream_pump;
var data_to_send = ""; // this holds what we want to send

Make a socket

// this socket will only listen on localhost
// set the second argument to false if you want it to listen
// to connections beyond the computer the extension runs on
var socket = new socket_c(-1, true, -1);

var socket_transport = socket_service.createTransport(null, 0, "localhost", socket.port, null);

Define callbacks and listeners

// this guy will get called when we're ready to send data
var output_stream_callback = {
    onOutputStreamReady: function(stream){          
        output_stream_bin = new output_stream_bin_c(stream);
        output_stream_bin.writeBytes(data_to_send, data_to_send.length);
        data_to_send = "";
    }
};

var socket_reader = {

    onDataAvailable: function(request, context, stream, offset, count){

        input_stream_bin = new input_stream_bin_c(stream);

        if(input_stream_bin.available() > 0){
            recieved_bytes = input_stream_bin.readByteArray(count);

            recieved_total = ""; // this holds the stuff we get

            // this loop converts bytes to characters
            // if you don't need to pass binary data around
            // you can just use nsIScriptableInputStream instead of
            // nsIBinaryInputStream and skip this
            for (var i = 0; i < recieved_bytes.length; i++){
                recieved_total += String.fromCharCode(recieved_bytes[i]);
             }

        }else{
            stream.close();
            // Nothing there, closing stream.
        }
    },
    onStartRequest: function(request, context){

    },
    onStopRequest: function(request, context, status){

    }
};

var socket_listener = {
    onSocketAccepted: function(socket, transport){

        client = transport;

        client_input_stream = client.openInputStream(0, 0, 0);
        client_output_stream = client.openOutputStream(0, 0, 0);
        client_output_stream.QueryInterface(Ci.nsIAsyncOutputStream);

        client_input_stream_pump[this_transport] = input_stream_pump_c.createInstance(Ci.nsIInputStreamPump);
        client_input_stream_pump[this_transport].init(client_input_stream, -1, -1, 0, 0, false);
        client_input_stream_pump[this_transport].asyncRead(socket_reader, socket);

    },
    onStopListening: function(socket, status){

    }
};

Start listening on the socket

socket.asyncListen(socket_listener);

When you want to send data

(edit- this part should be placed in a function, to be called only after a connection is made and only when you want to send data)

var stream = client_output_stream; // what stream you want to send the data to

// this is the only place where the thread_manager is required
stream.asyncWait(output_stream_callback,0,0,thread_manager.mainThread);

This implementation is fully asynchronous, so it should never have a performance impact or cause problems unless there's too much data (I guess, I don't really think there would be a problem with performance before your connection caps out) or something goes wrong (such as calling .asyncWait from the output stream callback).

Your input is in recieved_total and what you want to send is placed in data_to_send before you call .asyncWait on the connected client's output stream. Remember, this is a sample implementation, so if you want to use this, you'll need to change it so that it uses namespacing and you'll need to add handling functions for whatever data you want to get or send. Also, if you expect more than 1 client connection, you'll need to handle that separately as well (by an object array or something).

If you need information on the components used, the MDN is obviously the place to go to for that. However, nsIInputStreamPump for some reason doesn't have a page. For that you'll need to go to its idl implementation in the source (nsIInputStreamPump.idl). The mozilla lxr is also a great place to check out test implementations of sockets in javascript, as there are some .js files in there that are used to test the implementations.

edit -

for the addon-sdk, this might work better:

try replacing var {Cc, Ci, Cu, Cr, components} = require("chrome"); with var Components = require("chrome"); and then add

var {
    utils: Cu,
    interfaces: Ci,
    classes: Cc,
    results: Cr,
    stack: Cs,
    manager: Cm,
    Exception: Ce,
    Constructor: CC,
} = Components;

as was in the original code. Also, add a console.log(Components); after the require line so you can see if you're actually getting the components object.

mechalynx
  • 1,306
  • 1
  • 9
  • 24
  • Hello IvyLynx.....thank you very much for the response.....I'm using your code, but I have a doubt for the last part of code, because I always receive an error on stream variable (that is always undefined) – hasmet Jul 22 '14 at 08:20
  • Yeah I should have been more careful when I wrote that (the above is a simplified version of code I wrote for a project, so some stuff was thrown out and I use a different model there for things). `stream` will be `undefined` because if you just put the last part in sequence, it will try to use `client_output_stream` which is also `undefined` until a socket connection opens. The last part should be placed within a function of your making, that is called _only_ after you've already made a connection and only when you want to send data. – mechalynx Jul 22 '14 at 08:30
  • Yeah I've insert the last part (when I want send data) in a method because I imagined that the client_output_stream variable is defined only when the connection is completed. I debugging the code and I see that when I call the function (pressing a button) the connection is active....do you have any advice? – hasmet Jul 22 '14 at 08:42
  • @hasmet What does the console give you? Does it explicitly state that `stream` is `undefined`? Add something like `console.log(client_output_stream);` to the send data method before `var stream = client_output_stream` to see whether the method actually sees `client_output_stream` properly. Normally, it should give you a stream object in the console. If not, try defining `stream` from the `onSocketAccepted` method and then check if `stream` is defined from within the send data method. Have a conditional to output a console message if it _isn't_ defined so that you know what's happening. – mechalynx Jul 22 '14 at 08:47
  • I've tried to insert a console log on client_output_stream but the result is always "undefined". Additionally, I've shift the definition of stream in onSocketAccepted but the result is the same. The output of console is in the follow message – hasmet Jul 22 '14 at 09:01
  • * Call to xpconnect wrapped JSObject produced this error: * [Exception... "'[JavaScript Error: "stream is undefined" {file:"resource://gre/modules/XPIProvider.jsm -> jar:file:.../jid1exo2NPaadiTkqg@jetpack.xpi!/bootstrap.js -> resource://gre/modules/commonjs/toolkit/loader.js -> resource://ji d1-exo2npaaditkqg-at-jetpack/project/lib/main.js" line: 260}]' when calling method: [nsIObserver::observe]" nsresult: "0x80570021(NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS)" location: "native frame :: :: ::line 0" data:yes] – hasmet Jul 22 '14 at 09:01
  • 1
    Ooooh something is breaking because you're using the addon-sdk it seems. I have _zero_ experience with the addon-sdk unfortunately, so I have no idea why this is happening. However, since you're on the addon-sdk there's probably some .jsm module that can do all this for you a lot more easily. I think I've actually seen something like this. I'll check and be back. – mechalynx Jul 22 '14 at 09:15
  • check this out: [MDN link](https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/XUL_Migration_Guide#Using_XPCOM). Perhaps if you replace the first variable definition in the preparation code with `var {Cc, Ci} = require("chrome");` it can work properly. Of course, in `{Cc, Ci}` you'd have to add all the other abbreviations, but I'd suggest you read the article to find out exactly what you need to do. If you gain access to `Components` you can still use the same preparation code I think. – mechalynx Jul 22 '14 at 09:28
  • There's more information on what modules the addon-sdk provides [here](https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Module_structure_of_the_SDK). Make sure to check out [this](https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules) as well. Some code modules there can be very useful in making things easier. I'm especially interested in how something like `XPCOMUtils.jsm` could help you by providing access to `Components` possibly. – mechalynx Jul 22 '14 at 09:31
  • In reality I've already modified your code inserting your suggested modifications....I think that the best solution is to update my question inserting the new versione of code....wait 2 minutes – hasmet Jul 22 '14 at 09:38
  • I saw it, just didn't have time to review your code yet :P – mechalynx Jul 22 '14 at 10:39
2

nsISocketServer implements a simple TCP/bind server, but does not implement the websocket protocol.

  • You either need to implement the websocket protocol yourself in the server socket (incl. HTTP/1.1 Upgrade)
  • or use raw TCP sockets (nsISocketTransports via nsISocketTransportService).

Given that raw TCP sockets are usually a mess to deal with and that you'll need to implement some simple exchange protocol on top of that anyway, I'd guess the first option of implementing the websocket protocol in the server socket would be easier (at the very least, you get the client implementation for free).

PS: Blocking mode is a bad idea as it blocks the UI thread for potentially long periods of time.

PS: Apparantly, somebody implemented the websocket protocol already in coffee script and somebody else (from the Add-on SDK team) implemented it in (what appears to be some form of :p) Javascript as well (although the latter is pretty much not self-contained and hard to read/gasp).

Edit I got curious and wrote a stand-alone JS code module WebSocket server, that seems to mostly work. :p

Community
  • 1
  • 1
nmaier
  • 32,336
  • 5
  • 63
  • 78
  • thank you very much nmaier.....I'm trying to understand the second implementation....in case of problems (I hope not :) ) I will update this page – hasmet Jul 21 '14 at 15:36
  • @hasmet might want to accept the answer if it solves your problem :P – mechalynx Jul 21 '14 at 16:10
  • 1
    Added link to the WebSocket Server implementation I just wrote. Should work in a JS code module, although I wrote it in a Browser Scratchpad, actually... – nmaier Jul 22 '14 at 00:40
  • Finally I prefer the nsISocketServer implementation because in future maybe I will use binary data and I've updated my question with my new problem – hasmet Jul 22 '14 at 10:39
  • Yeah, don't change your questions like that. It would be better to post a follow-up question if you go a new question. Also WebSockets (and my WebSocketServer implementation for that matter) allow passing binary data, e.g `ws.send(new Uint8Array((1<<16) + 1));`. – nmaier Jul 22 '14 at 11:03
  • One question: is it possible use your code to realize a full asynchronous implementation of client-server model? – hasmet Jul 22 '14 at 11:56
  • It implements websocket (server-side), using async network I/O, so I'd say: yes, as client-server and async as it can get! – nmaier Jul 22 '14 at 12:03
  • Sorry but I've some problems with import part of your code. Substantially I've insert WebSocketServer.jsm in my project into a "modules" folder. I've inserted the row "resource external_components modules/" in chrome.manifest file but the extension crash at line Cu.import("resource://external_components/WebSocketServer.jsm"); with error "Component returned failure code: 0x80070057(NS_ERROR_ILLEGAL_VA LUE) [nsIXPCComponents_Utils.import] fileName = undefined" do you have any advice? – hasmet Jul 22 '14 at 13:34
  • Make sure you got the latest version (`git pull`). I had some encoding errors in the original version (my keyboard behaves strangely as of late :p). Otherwise: No idea. It works in FX31 and Nightly. It won't work on old version, though (IIRC 29 and earlier) but could be made to work by removing the ECMA-6 stuff. Please lets not discuss it here, but over at github if you have an issue with it. – nmaier Jul 22 '14 at 19:32
  • Ok finally I find a method to import the jsm file, but I have an additionall problem at row 569, because the skd is saying that the wsi.socket is undefined.....any ideas? Thanks – hasmet Jul 23 '14 at 09:43