Sockets

This section will describe how to communicate to remote sites using sockets.

Connecting Through a Socket

A socket API is provided to make connections to remote sites for URL protocols that are not supported directly. It requires the use of several XPCOM components. In addition, if a proxy is configured for a particular port, it may optionally be used when connecting.

The interface nsISocketTransportService is used to create socket connections. It can be used to create socket transports, which have both an input stream and an output stream associated with them. Each transport implements the nsISocketTransport interface which inherits stream opening functions from the interface nsITransport.

First, let's create the transport service. Since it is a service, we use getService instead of createInstance. Then, we use the createTransport method to create a new transport.

  var transportService =
    Components.classes["@mozilla.org/network/socket-transport-service;1"]
      .getService(Components.interfaces.nsISocketTransportService);
  var transport = transportService.createTransport(null,0,host,port,null);

The first two arguments are used if you are creating a special type of socket such as an SSL socket. Usually, you won't need to, so these two arguments can be null and 0 respectively. The host is the domain of the site you wish to connect to. Don't add the protocol, just use the host name such as 'www.xulplanet.com'. The port is the IP port number. For example, HTTP uses 80 and POP3 uses 110. The last argument is used if you need to use a proxy to connect. For this example, we'll assume not, so we'll just use null for this argument.

Note that the socket connection is not created yet. The connection is not established until the streams are created. First, let's create the output stream for writing to the socket.

  var outstream = transport.openOutputStream(0,0,0);
  outstream.write(outputData,outputData.length);

The first line opens the stream from the transport. The function openOutputStream has three parameters. The first can be set to a number of flags from the nsITransport interface. Use OPEN_BLOCKING if you want the socket to block execution while data is being written. In the case of the input stream, the flag will block execution until data arrives. The other flag, OPEN_UNBUFFERED, is used to open the stream without an internal buffer. If you don't specify this flag, you need to specify the buffer size as the second argument to openOuputStream. The third argument indicates the number of buffers to create. If you do use OPEN_UNBUFFERED, the last two arguments are ignored.

If that was too much too understand, just use zero for the three arguments. This will create a stream using default values, which should be suitable for most purposes.

The stream's write function may be used to write some data to the stream, which is output to the socket. The first argument is the data, which can be just a string, and the second argument is the length of the string. In JavaScript you can just get the length of the string using the length property, however you may wish to write only part of it. Call the write method each time you have data to write to the socket.

Next, we create the input stream to read data from the remote site.

  var stream = transport.openInputStream(0,0,0);
  var scriptablestream = Components.classes["@mozilla.org/scriptableinputstream;1"]
    .createInstance(Components.interfaces.nsIScriptableInputStream);
  scriptablestream.init(stream);

The openInputStream function takes the same arguments as openOutputStream. In the example above, we again use default values. The base input stream interface nsIInputStream doesn't provide any methods to read data from JavaScript, only from C++, so we must wrap the stream in a scriptable input stream which implements the nsIScriptableInputStream interface. The last two lines above create the scriptable stream and initialize it using the init method.

You can also use other stream types instead of nsIScriptableInputStream, for example, to read binary data, you might use nsIBinaryInputStream. However, from JavaScript, you will always need to wrap the base stream in some other form of stream.

In Mozilla, network reading and writing is done on a separate thread. Reading from the socket is done asynchronously, which means that the data will be read in the background as soon as it is available through the socket. The application will be notified when data is available to be read through the stream. For this we need to create a listener which will be notified when data is available.

For asynchronous reading, we also need to create an input stream pump, which is so named because it sends small chunks of data to a listener as it becomes available. The pump implements the nsIInputStreamPump interface. This interface also inherits from nsIRequest, which provides mechanisms for pausing connections and for specifing caching behavior. Normal sockets likely wouldn't use these features, but higher level services such as HTTP built over the socket interfaces will.

  var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"].
               createInstance(Components.interfaces.nsIInputStreamPump);
  pump.init(stream, -1, -1, 0, 0, false);
  pump.asyncRead(dataListener,null);

First, we create the input stream pump component. The second line initializes the pump with the input stream. Pass the nsIInputStream, not the scriptable stream, as the first argument to the init method. The next few arguments specify various things such as the position in the stream to start and buffer sizes. Usually, the default values as used in the example above are sufficient. The last argument, if true, will cause the stream to automatically close once all data has been read. In this case, we use false to close it manually.

The asyncRead function is used to assign a listener, here called dataListener, to the pump. The listener will be called when data is available and when the connection is closed. The second argument can be any object and is passed to the listener. It isn't used by the pump, so you can use it for own purposes.

The listener is expected to implement the nsIStreamListener interface. This interface has only a single method onDataAvailable which is called whenever additional data is available through the socket. This interface inherits from the nsIRequestObserver interface which has two additional methods, onStartRequest and onStopRequest, which are called at the beginning and end of the input respectively. That means that the listener will need to implement three methods.

  var dataListener = {
    onStartRequest: function(request, context){},
    onStopRequest: function(request, context, status){
      instream.close();
      outstream.close();
    },
    onDataAvailable: function(request, context, inputStream, offset, count){
      contentRead += scriptablestream.read(count);
    },
  };

We don't need to do anything special on the start of the request. However, at this point, the socket has successfully connected. The request argument is the pump as an nsIRequest, and the context argument is the value passed as the second argument to asyncRead earlier. In the example, this was null.

The onStopRequest function will be called when there is no more input. This won't generally be called automatically unless you cancel the request, or an error occurs. It will also be called if the server closes the connection, as HTTP requests generally do once data has been transfered. In the example above, we just make sure the streams are closed in the onStopRequest method. The first two arguments to onStopRequest are as with the onStartRequest method. The third argument is a status code. If this is non-zero, the connection stopped because an error occured.

The onDataAvailable method takes five arguments, the first two of which are the same as the other two functions. The inputStream is the nsIInputStream being read from. The offset is the position in the stream and the count is the number of bytes of data that are now available.

Within the onDataAvailable method, we read the number of bytes corresponding to the count argument from the stream. We need to read from the scriptable stream, which was created earlier, not from the real stream. Note that it's also possible to create the scriptable stream inside this method. The scriptable stream's read method reads a number of bytes and returns them as string. In this example, we just add the data to a variable contentRead. In reality, you would do something more complex with the data.

The code above doesn't do any error checking. You should really add a try-catch block, or more than one, to catch errors that occur. Such errors include invalid hosts, blocked ports, and network failures.

Here is a complete example. For simplicity, an HTTP request is made, which is fairly simple to implement. The readAllFromSocket method connects to www.mozilla.org via port 80 and makes an HTTP 1.0 request for the index page. A listener is called with the page content which is then printed to the console.

function getPageContent()
{
  var listener = {
    finished : function(data){
      dump(data);
    }
  }
  readAllFromSocket("www.mozilla.org",80,"GET / HTTP/1.0\n\n",listener);
}
function readAllFromSocket(host,port,outputData,listener)
{
  try {
    var transportService =
      Components.classes["@mozilla.org/network/socket-transport-service;1"]
        .getService(Components.interfaces.nsISocketTransportService);
    var transport = transportService.createTransport(null,0,host,port,null);
    var outstream = transport.openOutputStream(0,0,0);
    outstream.write(outputData,outputData.length);
    var stream = transport.openInputStream(0,0,0);
    var instream = Components.classes["@mozilla.org/scriptableinputstream;1"]
      .createInstance(Components.interfaces.nsIScriptableInputStream);
    instream.init(stream);
    var dataListener = {
      data : "",
      onStartRequest: function(request, context){},
      onStopRequest: function(request, context, status){
        instream.close();
        outstream.close();
        listener.finished(this.data);
      },
      onDataAvailable: function(request, context, inputStream, offset, count){
        this.data += instream.read(count);
      },
    };
    var pump = Components.
      classes["@mozilla.org/network/input-stream-pump;1"].
        createInstance(Components.interfaces.nsIInputStreamPump);
    pump.init(stream, -1, -1, 0, 0, false);
    pump.asyncRead(dataListener,null);
  } catch (ex){
    return ex;
  }
  return null;
}
Add a note User Contributed Notes
No comments available

Copyright © 1999 - 2005 XULPlanet.com