As
marsd indicated, the socket ID returned by
socket -server is the
listening socket. It's purpose in life is to listen for client connections on the port you indicate, and to execute the command you provide when a client connects.
You can't use the listening socket for any type of direct communication. Therefore, most
fconfigure operations on it have no effect. (Pretty much all you can do is
fconfigure -sockname to get a list of the IP address, the host name, and the port number for the socket.) About the only other thing you can do with a listening socket is to
close it, if you want, which prevents further client connections (although it doesn't terminate any client connections which might already be in place).
Like
marsd said, when a client connects to your server, Tcl automatically calls the connection procedure you provided to your
socket -server command, automatically providing 3 arguments: the socket ID of the
communication socket that the server needs to use to communicate with the client, the client's IP address, and the port number on the client system. It's
this socket that you can call
fconfigure on to set up the communication settings and
fileevent to register your event handler for this particular client. Also, you use the same client socket ID for both reading data from the socket and writing data to the socket. Closing the client socket terminates communication with only that client; the listening socket and all other client sockets are unaffected.
One slight error in
marsd's description, though, is that Tcl
doesn't fork a child process to handle the client. This is unlike client-server programming in most other languages. Instead, the listening/connection activities as well as communication with all clients is performed in the same process. Tcl takes an
event-driven approach to handling everything. You set up handler code for particular events -- for example,
socket -server registers the handler (a procedure) to execute when a client connects;
fileevent registers the handler (once again, a procedure) to execute when data arrives on a channel; the
-command option registers a handler (any arbitrary Tcl code) to execute when a user clicks on a button; etc. Then, your application enter the
event loop. That's what the
vwait command is doing in
marsd's example.
vwait enters the event loop and processes events until a handler assigns a value to the indicated global variable. In the event loop, your program is just "hanging out," waiting for events to occur. When it receives an event, it checks to see if a corresponding handler has been registered. If so, it executes the handler. Once the handler finishes execution, your program returns to the event loop.
Ok, as I described above, in standard Tcl server applications, all clients are handled by the same process as the listening server. So, how can Tcl keep track of whcih client it's interacting with? The most common technique is to use the
communication socket ID to uniquely identify each client, as that is guaranteed by Tcl to be unique. That's why you saw
marsd passing the client's socket ID to all of those client interaction procedures. You can also store client-specific information, most often in one or more arrays, using the client socket ID as the index.
Whew! That's a lot of information. It turns out that there are some additional issues that can arise regarding
blocking and
buffering behavior that would take a while more to explain. The key is that an event-driven program can still block. For example, if a channel is set to blocking behavior (the default), the
gets command blocks until there is a complete line of data to read. While
gets is blocked, you aren't in your event loop so you can't process any other events that might be occurring. Therefore, particularly in a server application, you should try to prevent any possible blocking behavior, so that getting blocked on one channel doesn't freeze your entire server.
Let me show you the standard code fragment I use for channel communication. I'm going to illustrate this with a client-side program, just so we don't have to worry about the other server issues for now. This code processes line-oriented textual information. If you've got multi-line messages that you pass, you'd simply modify the
ProcessLine procedure I show below to accumulate complete messages and process them. If you have non-textual information, you're going to need to read up more on channel I/O and construct your own handlers following these techniques.
Code:
# ReadLine
#
# This is our communication socket handler, which is
# called whenever data arrives on the channel. The
# channel is set to non-blocking mode, so we write our
# handler to check if there's a complete line of data
# to read. If so, it passes it off to the ProcessLine
# procedure to process. If not, we simply re-enter the
# event loop and wait for more data.
proc ReadLine {sock} {
if {[catch {gets $sock line} len] || [eof $sock]} {
# The last gets encountered EOF or we had an
# abnormal termination.
HandleEOF $sock
} elseif {$len >= 0} {
# We read a complete line of data.
ProcessLine $sock $line
}
# We reach this point if there wasn't a complete
# line to read. Return to the event loop and wait
# for more data.
}
# HandleEOF
#
# We got disconnected or encountered some type of
# communication error. In either case, close our side
# of the socket connection, then set "done" to terminate
# our event loop.
proc HandleEOF {sock} {
global done
catch {close $sock}
set done 1
}
# ProcessLine
#
# We've read a complete line of data. Do whatever we
# want to do with it.
proc ProcessLine {sock line} {
puts "From $sock: $line"
}
# Make our client connection
set sid [socket localhost 9001]
# Set the channel to automatically flush its output
# whenever we've provided a complete line, and for
# non-blocking behavior.
fconfigure $sid -buffering line -blocking 0
# Register ReadLine as the handler to call whenever
# data arrives on this channel. We pass the socket ID
# as an argument. It's a good habit to get into to use
# the list command to build up a well-formed Tcl
# command for our handler to allow substitution of
# variable values when constructing the handler, but
# preserving argument boundaries (something that
# quoting with "" doesn't do.
fileevent $sid readable [list ReadLine $stats1]
# Enter the event loop to detect events and handle them.
vwait done
For a multi-client server, I basically do the same thing, except that I set up the communication handling for each client that connects. One important thing you should do that I notice
marsd didn't include in his code is to
catch all your
puts commands when writing to sockets. That's because it's possible for a socket to go dead (even after you've successfully written to or read from it previously -- maybe the remote user accidentally uplugged their network cable), and attempting to write to a dead socket results in a error. You don't want an error with one client crashing your entire server, so get
really paranoid when writing servers; use
catch commands everywhere. I simplify this by creating a utility procedure to write the data while catching errors. Here's an example based on an echo server that I use in the "Interprocess Communication with Tcl" course that I teach:
Code:
# A simple example of a socket-based server. Accept a
# client connection, then echo back all lines sent by
# the client until EOF or until receiving a line with
# only the string "quit". The message "exit" disconnects
# the client and terminates the echo server.
set listen [socket -server ClientConnect 9001]
proc ClientConnect {sock host port} {
fconfigure $sock -buffering line -blocking 0
fileevent $sock readable [list ReadLine $sock]
SendMessage $sock "Connected to Echo server"
}
proc ReadLine {sock} {
if {[catch {gets $sock line} len] || [eof $sock]} {
HandleEOF $sock
} elseif {$len >= 0} {
ProcessLine $sock $line
}
}
proc HandleEOF {sock} {
# In this case, simply close our client socket.
catch {close $sock}
}
proc ProcessLine {sock line} {
global forever
if {[string equal -nocase $line exit]} {
# We'll set our vwait variable to exit the event loop
SendMessage $sock "Killing Echo server"
catch {close $sock}
set forever 1
} elseif {[string equal -nocase $line quit]} {
# Acknowledge the message and disconnect the client
SendMessage $sock "Closing connection to Echo server"
catch {close $sock}
} else {
# Simply echo the line back to the client
SendMessage $sock $line
}
}
proc SendMessage {sock msg} {
if {[catch {puts $sock $msg} error]} {
puts stderr "Error writing to socket: $error"
catch {close $sock}
}
}
vwait forever
# Close our listening socket
catch {close $listen}
- Ken Jones, President, ken@avia-training.com
Avia Training and Consulting,
866-TCL-HELP (866-825-4357) US Toll free
415-643-8692 Voice
415-643-8697 Fax