Friday, January 4, 2013

Hosting socket.io WebSocket apps in IIS using iisnode

In this post I explain how to configure a socket.io node.js application to use of WebSockets when hosting it in IIS 8 using iisnode. This complements a recent post in which I showed how to host node.js WebSocket applications in IIS on Windows using iisnode and faye-websocket module.

The complete code of the sample for self-hosting and IIS hosting of socket.io and faye-websocket in IIS using iisnode is available at https://github.com/tjanczuk/dante.

From Zero to Divine in  Seven Seconds

You need Windows 8 or Windows 2012 machine with IIS 8 and iisnode installed.

Clone the Dante sample, install dependencies, and set up IIS virtual directory pointing to the code of the sample:

git clone https://github.com/tanczuk/dante.git
npm install
dante\setup.bat

Then navigate to the socket.io sample at http://localhost/dante/server-socketio.js. You should see Dante’s Divine Comedy streamed to you over websockets from a socket.io application hosted in IIS 8 using iisnode, one stanza every 2 seconds:

image

You can see four HTTP requests being made. The first one targets the node.js application server-socketio.js and returns the HTML page with client side JavaScript. The page requests the socket.io.js client library from the server, which is the second HTTP call. Next, the client side JavaScript on the page performs the socket.io handshake (3rd HTTP requests). Finally, a WebSocket connection is established and the Dante’s Divine Commedy is streamed from the server as discrete WebSocket messages over that single connection.

Under the hood

Hosting socket.io node.js apps in IIS using iisnode requires some extra steps compared to self-hosting such apps. Unlike in a typical self-hosting case, node.js apps hosted in a IIS virtual directory own only a subset of the URL space, and socket.io must be adequately configured to that effect. In addition, IIS must be told which requests constitute socket.io traffic and must be handled by iisnode as opposed to other built-in IIS handlers (e.g. static file handlers).

This explanation assumes the node.js application is hosted in a IIS virtual directory named ‘dante’ as opposed to the root of an IIS website. The latter case is simpler to configure, with only required changes being the changes in web.config described below.

Below are the key components of the configuration. Full source code of the sample is at https://github.com/tjanczuk/dante.

Web.config

There are three aspects that must be configured in web.config: handler registration, URL rewrite rules, and IIS wesocket module configuraiton.

First, you must inform IIS that the server-socket.io.js file is a node.js application and must be handled by iisnode. Without this, IIS would try to serve the file as a client side JavaScript using the static file handler:

<handlers>
<add name="iisnode-socketio" path="server-socketio.js" verb="*" modules="iisnode" />
</handlers>

Then, the URL rewrite module must be informed that all HTTP requests that start with the ‘socket.io’ segment constitute node.js traffic and should be redirected to the server-socketio.js as the entry point of the node.js application. Without this, IIS would attempt to map these requests to other handlers, and most likely respond with a failure code:

<rewrite>
<rules>
<rule name="LogFile" patternSyntax="ECMAScript">
<match url="socket.io"/>
<action type="Rewrite" url="server-socketio.js"/>
</rule>
</rules>
</rewrite>

Lastly, the built-in WebSocket module that IIS 8 ships with must be turned off, since otherwise it would conflict with the WebSocket implementation provided by socket.io on top of the raw HTTP upgrade mechanism node.js and iisnode support:

<webSocket enabled="false" />

The complete web.config is at https://github.com/tjanczuk/dante/blob/master/web.config.

The server

The server code must configure socket.io to inform it that the node.js application owns just a subset of the URL space as a result of being hosted in IIS virtual directory. This means that socket.io traffic that the server normally listens to on the /socket.io path is going to arrive at /dante/socket.io:

io.configure(function() {
io.set('transports', [ 'websocket' ]);
if (process.env.IISNODE_VERSION) {
io.set('resource', '/dante/socket.io');
}
});

Notice this configuration change in socket.io is only made when the application is hosted in IIS; the same code base of the sample can also be self-hosted, in which case the configuration is left unmodified.

The full code of the server is at https://github.com/tjanczuk/dante/blob/master/server-socketio.js.

The client

The client code must contain configuration change corresponding to the server, otherwise socket.io client library would by default assume the socket.io traffic should be sent to the /socket.io path on the server:

var address = window.location.protocol + '//' + window.location.host;
var details = {
resource: (window.location.pathname.split('/').slice(0, -1).join('/') + '/socket.io').substring(1)
};

var client = io.connect(address, details);

Notice that this client code works correctly both when the server is self-hosted or hosted in a IIS virtual directory. This is because the socket.io configuration sets the URL paths relative to the pathname of the original request that rendered the HTML page. In the self-hosted case, the original page is rendered from http://localhost:8888/, and consequently socket.io’s resource property is set to ‘socket.io’. In the IIS hosted case, the original request is rendered from http://localhost/dante/server-socketio.js, and as a result the socket.io resource property will be set to ‘dante/socket.io’. This allows the client code to be agnostic to how the server is hosted.

The full code of the client is at https://github.com/tjanczuk/dante/blob/master/index-socketio.html.

Enjoy!

4 comments:

  1. I am using socket io with iisnode. I got dante example to work. I created example with clients posting to the server and server brodcasting back. My next step was to create signalR hub equivalent and it seemed that the namespace example from: http://socket.io/#how-to-use (Restricting yourself to a namespace) would do just that.

    Basically the client would need to connect to:
    io.connect('http://localhost/chat')
    or io.connect('http://localhost/news');

    In my caseI also run my app not in the root.

    So, it would be:

    io.connect('http://localhost/myapp/chat')
    or io.connect('http://localhost/myapp/news');

    How would I go about it. How would I need to change the url rewrite rules etc...

    Could you post an example that works in iisnode that matches an example on http://socket.io?

    ReplyDelete
  2. This works great if the "nodeProcessCountPerApplication" setting in iisnode is set to 1 (or 0 if you have a single core processor). As soon as I set iisnode to have more than 1 process per application, this example fails off and on (I get warn: client not handshaken client should reconnect). I'm guessing it's because it can't communicate between node processes. Any ideas to a solution to this?

    ReplyDelete
    Replies
    1. This is a general problem of scaling out socket.io applications that is not related to iisnode specifically. When scaled out, instances of a socket.io application must use some form of durable storage to share state since the default in-memory storage does not support scale-out. Check out https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO for how to configure a socket.io with Redis backend. Alternatively, if you are hosting your app in Windows Azure Web Sites, you should consider ServiceBus extension for socket.io: https://github.com/WindowsAzure/socket.io-servicebus

      Delete
    2. Thanks for that. I see why now. I've run into another issue with IIS Node. If I run this example with all transports enabled on IE8, it waits about 10 seconds before I start getting text and it misses the first 5 lines of text. If I run it without IIS node (just using node.exe) it works perfectly and I immediately get text starting with the first line. Do you know why that is and how I can fix it? I need my app to support at least IE8. Thanks.

      Delete

My Photo
My name is Tomasz Janczuk. I am currently working on my own venture - Mobile Chapters (http://mobilechapters.com). Formerly at Microsoft (12 years), focusing on node.js, JavaScript, Windows Azure, and .NET Framework.