Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations gkittelson on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

TCL client/server sockets 1

Status
Not open for further replies.

johnlopez2000

Programmer
Aug 2, 2002
90
US
Colleagues,

just need a little help with sockets.

Trying to get a client/server connection via 2 TCL sessions and I think I am missing something:

#------------------------------------
#server
set host 127.0.0.1
set port 5001

set socket [socket -server $host $port]
fconfigure $socket -blocking 1 -translation auto

gets $socket bytes

set msg [read $socket $bytes]

puts $msg

#------------------------------
#client
set host 127.0.0.1
set port 5001

set socket [socket $host $port]
fconfigure $socket -blocking 1 -translation auto

set msg [list HELLO 1.0 $tcl_version]
puts $socket [string length $msg]
puts $socket $msg
flush $socket

close $socket


#--------------------------------------

When the server does the GETS, I get the following error:

% gets $socket bytes
channel "sock376" wasn't opened for reading

I assume that I am doing something wrong with the "fconfigure" command, assuming that this sets up read/write operations with the port.

Also, how does one:
a) cause GETS to wait for an incomming connection?
b) do you loop to GET multiple incoming connections?
c) if one wants bi-directional messaging, does one need run a "client" and "server" thread on each side?

Thanks very much for your help.

PS: Example code that works would be a big bonus!


John Lopez
Enterprise PDM Architect
lopez.john@goodrich.com
 
Here's some example code.
Please remember to check the wiki for these problems as well.


Code:
#!/usr/bin/tclsh

#echo server
set messagecnt 0

proc putOut {handle msg} {
global messagecnt
      fconfigure $handle -buffering line -blocking 0 -translation auto
      incr messagecnt
      puts $handle "Read $msg: number messages read this session = $messagecnt"
      catch {flush $handle}
}

proc readIn {handle} {

       while {1} {
              if {[gets $handle line] > 0} {
                  if {[string compare $line "quit"] == 0} {
                      puts $handle "Quitting..have a nice day.."
                      catch {close $handle}
                      return 
                  } else {
                      putOut $handle $line
                      return 
                  }
              }
       }
}

proc handleConnect {sock addr port} {
  puts "Echo Server responding to client: $addr on port $port"
  puts $sock "Ready..."
  flush $sock
        fconfigure $sock -blocking 1 -buffering line -translation auto
        fileevent $sock readable [list readIn $sock]
}


#main()

set mysock [socket -server handleConnect 12000]
vwait forever

The tcl socket -server command returns a list consisting
of fd, remote address and remote port. The handler uses
this info to communicate. The server starts and dups/
forks child processes to handle connects.

Fileevent is used with both readable and writable
options to trigger procedures or commands for either of these child socket conditions.

vwait is used to cause the server to remain active
perpetually. You could also use tcl extensions to
fork() the server to place it in the bg.

Your use of fconfigure in the above example isn't
really necessary IMO.

Hope this helps.
 
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
 
Ken & Marsd,

thanks for all the help.

John

John Lopez
Enterprise PDM Architect
lopez.john@goodrich.com
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top