Version 2 of Minimalist Websocket Example

Updated 2017-12-17 17:27:15 by Bezoar

Bezoar

I looked in the wiki and the man page for a simple example that does not require a web server and could not find a good simple example to try out the tcllib websockets package. I did some testing on my own and cobbled together something that works and I hope people find it instructive. Luckily the websockets lib is native tcl code so I was able to determine that it is always necessary to specify a subprotocol even it its nonsense to get the websocket library to connect. With this example you can connect either via the commandline client or the html page with multiple instances. What ever you type will show up on all connectons. Also you will see pings on the commandline client. So below I have the server first, A web page you can load into your browser to test it out or a command line client using the websocket client side. The server runs on port 9900. The client assumes localhost but the javascript in the html page will get the correct origin if you serve the page from a web server. I got the html code from the testsuite of another websocket library and modified it to work using the file:// protocol. I would give attribution but its been a while and I cannot remember where I got it. I was also using the latest version of Tcllib. Note the html connects on load so start server before loading the html into a browser window or alternatively just refresh the browser after starting server.

Server

 #!/bin/sh
  # the next line restarts using wish \
  exec /opt/usr8.6.3/bin/tclsh8.6  "$0" ${1+"$@"}
  package require websocket

 proc handler { sock type msg } {
         switch -glob -nocase -- $type {
                 co* {
                         puts "Connected on $sock"
                 }
                 te* {
                         puts "RECEIVED: $msg"
                         foreach s $::openSockets {
                                 ::websocket::send $s $type  ">$msg"
                         }
                 }
                 cl* -
                 dis* -
                 error {
                         set idx [ lsearch $::openSockets $sock ]
                         if { $idx >= 0 } { 
                                 set ::openSockets [lreplace $::openSockets $idx $idx ]
                         }
                         catch { close $sock } 
                 }
                 default {
                         puts "$type"
                 } 
     }
 }
 proc readline { fd handler } {
         set line [ gets $fd ]
         if { [eof $fd ] } {
                 catch { close $fd } 
         }
         uplevel #0 $handler \"$line\"
 } 
 proc test { sock line } {
     ::websocket::send $sock text $line
 }
 proc closeapp { sock } {
         ::websocket::close $sock 1000 "normal close"
         after  1000 {
                 exit 0
         }
 }

 proc readAll { sock  } {
         global servSock
         set count  0
         array set data {}
         set mode request
         while { [gets $sock line ] != -1 } {
                 if { $line eq "\n" || $line eq "" } { continue; } 
            if { $count > 0 } {
                    set mode headers
            }
                 append data($mode) "$line\n"
                 incr count 
         }
         lassign $data(request) method url proto
         set header [ dict create  ]
         foreach line [split $data(headers) \n ] {
                  set idx [string first ":" $line ]
                  if { $idx == -1 } { continue; } 
                 set key [string range $line 0 $idx-1 ] 
                 set value [string trim [ string range $line $idx+1 end ] ]
                 puts "'$key' | '$value'"
                 dict set header $key $value
         }
         if { $url  eq "/log/me" } {
                 puts "got a websocket request : $url"
                   puts "header: $header" 
                 set wtest [::websocket::test $servSock $sock /log/me $header ]
                 if { $wtest } {
                         ::websocket::upgrade $sock
                         ::websocket::takeover $sock handler 1
                         puts "[::websocket::conninfo $sock type] from [::websocket::conninfo $sock sockname] to [::websocket::conninfo $sock peername]"
                         fileevent $sock readable
                         lappend ::openSockets $sock
                 } else {
                         puts "did not get a valid  web socket request: url : $url" 
                         set reply "$proto 404 Not Found\r\nContentType: text/html\r\n\r\n<html><head><head><p>Not a web server!</p><body</body></html>"
                         puts $sock $reply
                         catch { close $sock} 
                 }
         } else {
                 puts "did not get a web socket request: url : $url" 
                 set reply "$proto 404 Not Found\r\nContentType: text/html\r\n<html><head><head><p>Not a web server!</p><body</body></html>"
                 puts $sock $reply
                 catch { close $sock} 
         }
 }
 ::websocket::loglevel debug
 proc Server {startTime channel clientaddr clientport} {
     puts "Connection from $clientaddr registered"
     set now [clock seconds]
     fconfigure $channel -blocking 0 -buffering line
         fileevent $channel read [list readAll  $channel ] 
 }
 global openSockets
 set openSockets {} 
 set servSock [ socket -server [list Server [clock seconds]] 9900 ]
 ::websocket::server $servSock
 ::websocket::live $servSock /log/me  handler
 vwait forever

Command Line Client

 #!/bin/sh
  # the next line restarts using wish \
  exec /opt/usr8.6.3/bin/tclsh8.6  "$0" ${1+"$@"}
 package require websocket
 ::websocket::loglevel info
 proc handler { sock type msg } {
         switch -glob -nocase -- $type {
                 co* {
                         puts "Connected on $sock"
                 }
                 te* {
                         puts "RECEIVED: $msg"
                 }
                 cl* -
                 dis* -
                 error {
                         puts " $type"
                         catch { close $sock }
                         exit 0
                 }
                 default {
                         puts "$type"
                 } 

     }
         puts  -nonewline stdout ">"
         flush stdout
 }
 proc readline { fd handler } {
         set line [ gets $fd ]
         if { $line eq "q" } { exit 0  ; # harsh but this is an example after all } 
         if { [eof $fd ] } {
                 catch { close $fd } 
         }
         uplevel #0 $handler \"$line\"
 } 
 proc test { sock line } {
     ::websocket::send $sock text $line
 }
 proc closeapp { sock } {
         ::websocket::close $sock 1000 "normal close"
         after  1000 {
                 exit 0
         }
 } 
 fconfigure stdin -blocking 0 -buffering line
 set sock [::websocket::open http://localhost:9900/log/me handler -protocol s ]
 fileevent stdin read [list readline stdin [list test $sock ] ] 
 puts "[::websocket::conninfo $sock type] from [::websocket::conninfo $sock sockname] to [::websocket::conninfo $sock peername]"
 puts  -nonewline stdout ">"
 flush stdout
 vwait forever

Browser Client

a page you can load up in your browser without using a server use the file:/// syntax .

 <!DOCTYPE html>
 <html lang="en">
 <head>
 <title>Chat Example</title>
 <script type="text/javascript">
 window.onload = function () {
     var conn;
     var msg = document.getElementById("msg");
     var log = document.getElementById("log");

     function appendLog(item) {
         var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
         log.appendChild(item);
         if (doScroll) {
             log.scrollTop = log.scrollHeight - log.clientHeight;
         }
     }

     document.getElementById("form").onsubmit = function () {
         if (!conn) {
             return false;
         }
         if (!msg.value) {
             return false;
         }
         conn.send(msg.value);
         msg.value = "";
         return false;
     };

 if (window["WebSocket"]) {
         if ( document.location && document.location.host &&document.location.host !== "" ) {
             conn = new WebSocket("ws://" + document.location.host + ":9900/log/me", "s");
         } else {
             conn = new WebSocket("ws://localhost:9900/log/me","s");
                   }

         conn.onclose = function (evt) {
             var item = document.createElement("div");
             item.innerHTML = "<b>Connection closed.</b>";
             appendLog(item);
         };
         conn.onmessage = function (evt) {
             var messages = evt.data.split('\n');
             for (var i = 0; i < messages.length; i++) {
                 var item = document.createElement("div");
                 item.innerText = messages[i];
                 appendLog(item);
             }
         };
     } else {
         var item = document.createElement("div");
         item.innerHTML = "<b>Your browser does not support WebSockets.</b>";
         appendLog(item);
     }
 };
 </script>
 <style type="text/css">
 html {
     overflow: hidden;
 }

 body {
     overflow: hidden;
     padding: 0;
     margin: 0;
     width: 100%;
     height: 100%;
     background: gray;
 }

 #log {
     background: white;
     margin: 0;
     padding: 0.5em 0.5em 0.5em 0.5em;
     position: absolute;
     top: 0.5em;
     left: 0.5em;
     right: 0.5em;
     bottom: 3em;
     overflow: auto;
 }

 #form {
     padding: 0 0.5em 0 0.5em;
     margin: 0;
     position: absolute;
     bottom: 1em;
     left: 0px;
     width: 100%;
     overflow: hidden;
 }

 </style>
 </head>
 <body>
 <div id="log"></div>
 <form id="form">
     <input type="submit" value="Send" />
     <input type="text" id="msg" size="64"/>
 </form>
 </body>
 </html>