ResultWell, it took some time, but I made it! Because Indy has already a nice http server + client implementation (that is used by RO too) and because I could not find a good http implementation for synapse, I decided to make it with Indy 10. Unfortunately there was no code yet of websockets for Indy 10, so I took the official specification (protocol version 13) and started "hacking" :).
In short the result is:
- an Indy IOHandler (TIdIOHandlerWebsocket), which reads and writes websocket frames (used by both server and client)
- a Indy http + ws client (TIdHTTPWebsocketClient), which has a "TryUpgradeToWebsocket" function that can be used to try to upgrade to a websocket connection when the webserver supports it.
- a RemObjects http + websocket channel (TROIndyHTTPWebsocketChannel), which uses the above http + ws channel.
- a RemObjects http + websocket server (TROIndyHTTPWebsocketServer) (unfortunately "TROIndyHTTPServer" creates directly a "TROIdHTTPServer" without a virtual function, so I could not make a special Indy http + ws server), which support both http and websocket clients (!).
The IOHandler is not very complicated: it reads the websocket header and concatenates several frames (if it is fragmented) to a single message. And it writes the ws header and the raw data after it. One note: clients must send all data with a mask. This is needed because otherwise the http cache can be poisoned by a hacker via a special composed websocket message (all ws data is send via a http port), or at least something like this is stated in the specification :).
The Indy http client contains the logic for a special upgrade request, and the handling and checks for the response of the server. The upgrade request is a set of extra http headers, together with a SHA1 key. In theory the websocket connection can support some extensions (like gzip compression) and subprotocols (like SIP for VOIP) but this is not implemented yet.
The RemObjects channel tries to upgrade (once) to a websocket connection, but can also operate as an "old" http channel. The special addition in this channel is a lightweight background read thread. This in contrast to the RO super tcp channel, which has a dedicated read thread per connection. Because my customer has Windows RO services with connection/channel pools (multi-threaded, to support multiple concurrent client calls), I did not want to double the already high count of threads! However some kind of continuous reading of the client connections is needed because of the duplex character of websockets, like sending ping/pong frames by the server, sending/pushing data from server to a client at any time, etc.
After some research I made a single background thread, which uses the tcp "select" winsock api call, which can wait for incoming data for up to 64 connection! This thread only handles unexpected data from the server (ping + close frames and RO events), normal RO dispatching of calls is done by the caller thread itself. This is enough for my customer because of the (probably) low traffic of RO events, but additional or dedicated threads can be made of course in case of higher loads in the future.
A nice trick I have to mention is: I use dummy connect to 0.0.0.0 to stop the "select" wait in case a new connection is added or when the wait thread has to terminate. I don't know if there is a better way, but at least it works :). By the way: I could not use "WSAAsyncSelect" because Indy needs blocking sockets.
The Indy + RemObjects http server contains both the logic for (possible) upgrade requests and some extra RO specific handling. It has a seperate "execute" function for websocket contexts, otherwise the normal Indy http handling would give errors :).
QualityI already spent some fair amount of time to make it stable (like killing client + server while sending and receiving etc), so it should be stable enough to use. We are busy with a pilot project to see how this stuff in combination with Smart Mobile Studio will work (backend RO services, frontend html5 clients) and if it can be used in real-life production situations. Anyway, my customer allowed me to make it open source (hopefully it will be integrated in Indy 10 itself), so please test it, fix some bugs, add new features, etc!
For the Delphi client, you need to connect to the server by clicking for example the "Sum" button. If you have done this for both Delphi clients, you can press the "Progress" button in the web page (served by the same server!). Then you will see all 3 progress bars running at the same speed :).
Note: even Android 4.0 does not support websockets! You need Opera Mobile (and enable websockets) although my demo still did not work with it (haven't time yet to debug it). Or you need Chrome for Android (Android 4.0 and higher) and then my demo works fine. Probably I have to use long polling over http if I want to use it with older Android mobile phones... :(
Note 2: an RO event can also be send at any other time, for example when an other user logs in etc. To test this I added the button "Send event" on the server form (note: only for this demo :), normally you will make a Windows service for the server instead of a GUI app). Press the button and in both clients a popup will be shown: