Version 10 of WebSocket

Updated 2011-05-01 02:06:56 by AMG

WebSockets are a nice alternative to XMLHTTPRequest for bi-directional communication between a web browser and a server application. They need a browser, e.g. Chrome, that supports the WebSocket API and a server that supports the WebSocket protocol.

agb Dec. 2010. Chrome now supports an updated version of the websocket protocol so the wibble example previously here no longer works (to see it have a look in this page's history) . The changes to get the current version working are non-trivial.

jbr 2010-12-20 - Here is code that will allow wibble to handshake with the new spec. The version of Chrome that I have (8.0.552.231) has the new handshake but sends the old data framing. I can send data from the client, but, I haven't gotten it to accept data messages from the server. Wibble.tcl needs to be patched to add a way for it to release the socket that will be the websocket channel without responding and closing it:

agb 2010-12-21 - I made a small change to ::wibble::ws-handle to check that chan read actually reads a byte. With this change I have successful, bi-directional messages over the web socket with chrome 8.0.552.224. Thanks for updating wibble.

 # Abort processing on this client.
 proc wibble::abortclient {} {
    return -code 7
 }

AMG: I take it that this command is to be called by a zone handler in order to get Wibble to terminate the coroutine without closing the socket. Correct?

Also, see my comments on Wibble wish list ([L1 ]) for an alternative, less invasive approach.

agb: Set keepalive 0 at the top of ::wibble::process and then change the exceptional return handling like this:

     } on 7 outcome {
        set keepalive 1
     } finally {
        if { !$keepalive } {
            catch {chan close $socket}
        }
     }

AMG: Some time after you wrote the above, I have changed Wibble to have customizable cleanup handlers. With the latest version of Wibble, instead of modifying the finally block, change the initialization of the cleanup list (top of [process]) to the following:

    set cleanup {
        {chan close $file}
        {if {!$keepalive} {chan close $socket}}
        {dict unset ::wibble::icc::feeds $coro}
    }

agb: Add this to the zone handlers:

  wibble::handle /ws websocket handler ws-demo

This is your server side callback:

 proc ::ws-demo { event sock { data {} } } {
    switch $event {
        connect {}
        message {
            puts "WS-Demo: $event $sock $data"
        }
    }

  ::wibble::ws-send $sock "Hello"
 }

AMG: Are connect and message the only two events that can happen?

agb: Utility to help the server send data frames, doesn't work yet!!

 proc ::wibble::ws-send { sock message } {
    # New data framing?
    #puts -nonewline $sock [binary format cc 4 [string length $message]]$message

    # Old data framing?
    puts  -nonewline $sock "\x00"
    puts  -nonewline $sock $message
    puts  -nonewline $sock "\xFF"

    flush $sock
 }

Handler to accept data from browser. Uses old data framing.

 proc ::wibble::ws-handle { handler sock } {
    if { [chan eof $sock] } {
        puts "Closed $sock"
        close $sock
    } else {
        set code [read $sock 1]
        if {[binary scan $code c code]} {        ; # Do I need this? I think so.
          switch $code {
            0 {
                set message {}

                while { [set c [read $sock 1]] != "\xFF" } {
                    append message $c
                }
                $handler message $sock $message
            }
            default {
                puts "Bad Blocking: $c"
            }
          }
       }
    }
 }

The Zone Handler

 package require md5

 proc ::wibble::websocket { state } {
    set upgrade    {}
    set connection {}
    dict with state request header {}

    if { $connection ne "Upgrade" || $upgrade ne "WebSocket" } {
        return
    }

    set sock [dict get $state request socket]

    puts "WebSocket Connect: $sock"

    set key1 [regsub -all {[^0-9]} ${sec-websocket-key1} {}]
    set spc1 [string length [regsub -all {[^ ]}   ${sec-websocket-key1} {}]]
    set key2 [regsub -all {[^0-9]}   ${sec-websocket-key2} {}]
    set spc2 [string length [regsub -all {[^ ]} ${sec-websocket-key2} {}]]

    set key3 [read $sock 8]

    set handler [dict get $state options handler]
    chan event $sock readable [list ::wibble::ws-handle $handler $sock]

    set key1 [expr $key1/$spc1]
    set key2 [expr $key2/$spc2]

    set challenge [binary format II $key1 $key2]$key3
    set response  [md5 $challenge]

    puts $sock "HTTP/1.1 101 WebSocket Protocol Handshake"
    puts $sock "Connection: Upgrade"
    puts $sock "Upgrade: WebSocket"
    puts $sock "Sec-WebSocket-Origin: http://localhost:8080"                ; # This shouldn't be hard coded!!
    puts $sock "Sec-WebSocket-Location: ws://localhost:8080/ws/demo"
    puts $sock ""

    chan configure $sock -translation binary
    puts $sock $response
    chan flush $sock

    $handler connect $sock  ; # There should be an option to pass a session Id here.

    abortclient
 }

AMG: Thanks for the code, guys. I will need to ponder some more before integrating this into Wibble, but I do think I want this feature. However, I think it would benefit from tighter integration. As far as I can tell, it leverages Wibble for establishing the connection but then takes over all I/O. This concept is quite similar to something JCW shared with me the other day, namely an implementation of Server-Sent Events [L2 ] [L3 ]. Whatever I do, I would like it to support both protocols, or at least their common requirements.

If you're wondering why I haven't integrated all this sooner, it's because AJAX was my priority. It may be terribly clumsy compared to WebSockets and Server-Sent Events, but it also has the most browser support.