Version 8 of WebSocket

Updated 2010-12-20 23:10:27 by agb

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
 }

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

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

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"
 }

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
 }