From a request by GPS on Tclers' chat, came this simple server as a socket demo. Data from each client is simply relayed to the other, like on a telephone bridge.
Extending to more than 2 parties is left as an exercise.
array set socks {} proc makebridge {p0 p1} { set l0 [socket -server [list connect 0] $p0] set l1 [socket -server [list connect 1] $p1] } proc connect {p sock addr port} { puts "Client on port $p: $sock from $addr:$port" global socks set socks($p) $sock fconfigure $sock -buffering none fconfigure $sock -blocking no fileevent $sock readable [list bridge $p] } proc close_bridge {} { global socks close $socks(0) close $socks(1) array set socks {} } proc bridge {p} { set q [expr {!$p}] global socks if {[eof $socks($p)]} { puts "Close port $p" close_bridge } elseif {[catch {set data [read $socks($p)]}]} { puts "Error on port $p: $::errorInfo" close_bridge } else { puts -nonewline $socks($q) $data } } makebridge 2000 2001 vwait forever
To help others with the "exercise" (because the example above may lead into a wrong direction): There is no need to offer the same service on several ports. (Or as an exercise for the author of the above code: What happens if two clients connect to port 2000?)
Simplifying makebridge: proc makebridge {p} { socket -server connect $p } makebridge 2001
Connect keeps track of all connections by adding them to socks (which is a simple list now).
set socks {} proc connect {sock addr port} { puts "Client connection: $addr:$port" global socks lappend socks $sock fconfigure $sock -buffering none -blocking false fileevent $sock readable [list bridge $sock] }
Bridge now forwards the message to all clients except the sender. I like to emphasize that we're working on channels, I'm using a similar program myself to bridge serial lines over a network. (Left as an exercise ...)
Plus a little helper from RS:
proc lremove {l v} { set result [lsearch -all -inline -not -exact $l $v] return $result } proc bridge {channel} { global socks if {[eof $channel]} { puts "Close channel $channel" close $channel set socks [lremove $socks $channel] } else { set data [read $channel] foreach sock $socks { if {$sock != $channel} { puts -nonewline $sock $data } } } } vwait forever
- Effe
aspect - I wrote the original snippet some time ago, and am glad to see the improvements here (and a little embarrassed at using two ports in the first example) .. here's another one. I like lremove a lot, along with the Additional list functions scattered about the wiki .. but it feels a bit out of place here, as we're really using $socks as a (very simple) set. What's the easiest way to do sets in Tcl? The keys of an array form a set! As a bonus we get to print some more useful information on disconnect:
array set socks {} proc connect {sock addr port} { puts "Client connection: $addr:$port" global socks array set socks($sock) $addr:$port fconfigure $sock -buffering none -blocking false fileevent $sock readable [list bridge $sock] } proc bridge {channel} { global socks if {[eof $channel]} { puts "Disconnect: $socks($channel)" close $channel unset socks($channel) } else { set data [read $channel] foreach sock $socks { if {$sock != $channel} { puts -nonewline $sock $data } } } } vwait forever
Now this can be made into a chat server with only a few lines changed ...