This article describes how to inspect and load-balance WebSockets traffic using Stingray Traffic Manager, and when necessary, how to manage WebSockets and HTTP traffic that is received on the same IP address and port.
WebSockets is an emerging protocol that is used by many web developers to provide responsive and interactive applications. It's commonly used for talk and email applications, real-time games, and stock market and other monitoring applications.
By design, WebSockets is intended to resemble HTTP. It is transported over tcp/80, and the initial handshake resembles an HTTP transaction, but the underlying protocol is a simple bidirectional TCP connection.
For more information on the protocol, refer to the Wikipedia summary and RFC 6455.
Basic WebSockets Load Balancing
Basic WebSockets load balancing is straightforward. You must use the 'Generic Streaming' protocol type to ensure that Stingray will correctly handle the asynchronous nature of websockets traffic.
A WebSocket handshake message resembles an HTTP request, but you cannot use the built-in http.* TrafficScript functions to manage it because these are only available in HTTP-type virtual servers.
The libWebSockets.rts library (see below) implements analogous functions that you can use instead:
Paste the libWebSockets.txt library to your Rules catalog and reference it from your TrafficScript rule as follows:
import libWebSockets.rts as ws;
You can then use the ws.* functions to inspect and modify WebSockets handshakes. Common operations include fixing up host headers and URLs in the request, and selecting the target servers (the 'pool') based on the attributes of the request.
import libWebSockets.rts as ws; if( ws.getHeader( "Host" ) == "echo.example.com" ) { ws.setHeader( "Host", "www.example.com" ); ws.setPath( "/echo" ); pool.use( "WebSockets servers" ); }
Ensure that the rules associated with WebSockets virtual server are configured to run at the Request stage, and to run 'Once', not 'Every'. The rule should just be triggered to read and process the initial client handshake, and does not need to run against subsequent messages in the websocket connection:
Code to handle the WebSocket handshake should be configured as a Request Rule, with 'Run Once'
Stingray can SSL-decrypt TCP connections, and this operates fully with the SSL-encrypted wss:// protocol:
Note that when testing this capability, we found that Chrome refused to connect to WebSocket services with untrusted or invalid certificates, and did not issue a warning or prompt to trust the certificate. Other web browsers may operate similarly. In Chrome's case, it was necessary to access the virtual server directly (https://), save the certificate and then import it into the certificate store.
Stingray can also SSL-encrypt downstream TCP connections (enable SSL encryption in the pool containing the real websocket servers) and this operates fully with SSL-enabled origin WebSockets servers.
HTTP traffic should be handled by an HTTP-type virtual server rather than a Generic Streaming one. HTTP virtual servers can employ HTTP optimizations (keepalive handling, HTTP upgrades, Compression, Caching, HTTP Session Persistence) and can access the http.* TrafficScript functions in their rules.
If possible, you should run two public-facing virtual servers, listening on two separate IP addresses. For example, HTTP traffic should be directed to www.site.com (which resolves to the public IP for the HTTP virtual server) and WebSockets traffic should be directed to ws.site.com (resolving to the other public IP):
Configure two virtual servers, each listening on the appropriate IP address
Sometimes, this is not possible – the WebSockets code is hardwired to the main www domain, or it's not possible to obtain a second public IP address. In that case, all traffic can be directed to the WebSockets virtual server and then HTTP traffic can be demultiplexed and forwarded internally to an HTTP virtual server:
Listen on a single IP address, and split off the HTTP traffic to a second HTTP virtual server
The following TrafficScript code, attached to the 'WS Virtual Server', will detect if the request is an HTTP request (rather than a WebSockets one) and hand the request off internally to an HTTP virtual server by way of a special 'Loopback Pool':
import libWebSockets.rts as ws; if( !ws.isWS() ) pool.use( "Loopback Pool" );
The implementation described in this article was developed using the following browser-based client, load-balancing traffic to public 'echo' servers (ws://echo.websocket.org/, wss://echo.websocket.org, ws://ajf.me:8080/).
At the time of testing:
If you find this solution useful, please let us know in the comments below.
Are there plans to integrate support for websockets more directly into the HTTP virtual server?
Hi Andrew,
It''s definitely a possibility... we're waiting to see what the feedback on this approach is. I appreciate this dual-virtual-server design is a little more complex than we would like, so if it's popular, there's a good case to add this capability to a HTTP virtual server.
Consider this article to be a tentative prototype for a future product feature, and please let us know how well it works for you.
regards
Owen
Hello,
I had to modify your lib websocket, because some clients didn't wait for the confirmation of the socket upgrade, and were sending data directly on the first request. (i don't say this behavior is clean, but it happens...)
On the updateRequest function, request's data are replaced only with the headers and without the eventual data received after the headers on the same request.
Here our simple modification :
--- libWebsocket.txt 2013-06-19 12:23:24.000000000 +0200
+++ libWebsocketSky.txt 2013-06-19 12:27:50.000000000 +0200
@@ -113,6 +113,9 @@
$t = "\n";
if( endsWith( $first, "\r\n" ) ) $t = "\r\n";
connection.data.set( "ws-TERMINATOR", $t );
+
+ # Split the request to only get headers on the 1st request
+ request.endsWith($t . $t);
# Headers are prefixed by a terminator to make searches easier, and
# end with the double-terminator
Has Andrew, we are really expecting integration of websockets on HTTP virtual servers.
Best,
Patrice Damezin
Hi Patrice - looks like a very sensible change - thanks for sharing it. I've updated the library accordingly.
Best regards
Owen