Klienten und Server mit TCL (1)

cs.rit.edu

Klienten und Server mit TCL (1)

Klienten und Server mit TCL

Axel-Tobias Schreiner, Universität Osnabrück

Im letzten Heft hat Dieter Glauß die Tool Command Language TCL vorgestellt

und gezeigt, wie leicht man damit X-Applikationen programmieren kann,

deren Aussehen durchaus von Motif inspiriert ist. In diesem Heft untersuchen

wir eine der vielen Erweiterungen, die auf TCL aufbaut und ebenfalls im Netz

frei verfügbar ist.

Mit tcl-dp kann man in TCL-Programmen die Möglichkeiten von Sockets, also

von Netzverbindungen, ausnützen. Das Angebot reicht von Datagram- und

Stream-Verbindungen über Remote Procedure Calls (RPC) bis hin zu verteilten

Objekten. Dieser Ar tikel skizzier t die Fähigkeiten von tcl-dp und führt

gleichzeitig in die Grundbegriffe der Programmierung mit Sockets und TCP/IP

ein.

Prozesse und Pipelines

Im gleichen Rechner können verwandte Prozesse über Pipelines kommunizieren.

Mit dem TCL-Kommando exec kann man eine Prozeß-Pipeline aufbauen, ähnlich wie

man das mit der Funktion system() in C programmieren würde:

$ tclsh

% exec ps


PID TT STAT TIME COMMAND

6184 p1 S 0:00 —bash

6192 p1 S 0:00 tclsh

% ps | sed 1,3d | wc —l

2

Das UNIX-Kommando ps zeigt die aktuellen Prozesse; wc zählt hier die Zeilen, die in

der Pipeline ankommen. Wenn wir nur wissen wollen, wieviele Prozesse wir

eigentlich besitzen, müssen wir die Titelzeile und Zeilen für die Pipeline-Prozesse mit

sed entfernen.

exec führt seine Argumente im Rahmen einer UNIX-Pipeline aus. Arbeitet man mit

dem TCL-Interpreter tclsh interaktiv, werden unbekannte Kommandos auf der

äußersten Ebene in den Katalogen in PATH gesucht und falls möglich ausgeführt. 1

Wie alle Kommandos hat auch exec ein Resultat, nämlich entweder die Standard-

Ausgabe der Pipeline oder die Prozeßnummern der Pipeline, wenn & als letztes

Argument angegeben ist:

% set a \

[exec ps | sed 1,3d | wc —l]

2

% set a

2

% exec ps | sed 1,3d | wc —l &

6226 6227 6228

% 2

set weist einer Variablen einen Wert zu, der je nach Kontext als String und Liste

angesehen wird. Mit dem Gegenschrägstrich kann eine Zeile in TCL fortgesetzt

1

Dies ist in der Standard-Prozedur unknown in init.tcl implementiert, die allerdings

bei tcl7.3 versehentlich / an Stelle des aktuellen Katalogs durchsucht.


werden. Eckige Klammern [] umgeben ein Kommando, dessen Resultatwert als

Argument verwendet werden soll. Ohne zweites Argument liefert set den Wert

einer Variablen.

Wir erkennen hier, daß exec bei einer Pipeline im Vordergrund wirklich ihre

Standard-Ausgabe als Resultat liefert, denn wir finden sie als Wert der Variablen a.

Für die Hintergrund-Pipeline erhalten wir die Prozeßnummern als Resultat, und die

Standard-Ausgabe fließt asynchron in unsere eigene Ausgabe ein.

Einen Ersatz für die C-Funktion popen() gibt es ebenfalls in TCL: open richtet

normalerweise einen Dateizugriff ein. Wenn das erste Argument allerdings mit dem

Pipeline-Symbol | beginnt, kann man über die Dateiverbindung aus einer Pipeline

lesen oder in eine Pipeline schreiben.

Einführung in TCL

Abbildung 1 zeigt ein in TCL implementiertes Kommando, mit dem man nach einem

Pfad suchen kann, der im eigenen Heimatkatalog beginnt.

1 #!/usr/local/bin/tclsh

2

3 if {$argc != 1} {

4 puts stderr "usage: $argv0 pattern"

5 exit 1

6 }

7 set pattern [lindex $argv 0]

8

9 set in [open "| find $env(HOME) —print" r]

10 set cnt [open "| wc —l" w]

11

12 while {[gets $in line] >= 0} {


13 if [string match $pattern $line] {

14 puts $line

15 puts $cnt $line

16 }

17 }

18

19 close $in; close $cnt; exit 0

Abbildung 1: Pfade finden

Das Skript soll uns hier als kurze Einführung in TCL dienen:

1 Anweisungen werden durch Zeilentrenner oder Semikolons getrennt. Wenn das

erste Zeichen einer Anweisung # ist, liegt ein Kommentar vor, der bis zum Zeilenende

reicht. Diese Zeile ist ein Kommentar, der dafür sorgt, daß diese Textdatei vom

TCL-Interpreter tclsh ausgeführt wird.

3 if ist ein TCL-Kommando, das als Kontrollstruktur wirkt. Das erste Argument ist

ein Ausdruck, das zweite Argument eine Liste von TCL-Kommandos, die ausgeführt

werden, wenn der Ausdruck nicht 0 ist. Weitere Argumente erlauben elseif- und

else-Klauseln. argc ist die Zahl der Argumente, die außer dem TCL-Skript an tclsh

übergeben wurden. Mit $ wird der Wert einer Variablen abgerufen. Hier wird

untersucht, ob wir überhaupt ein Muster als Argument bekommen haben.

4 puts gibt einen String als Zeile aus. Dabei kann eine Dateiverbindung wie stderr

explizit angegeben werden. Innerhalb von Doppelanführungszeichen findet Textersatz

für Variablen und Kommandos statt. Das Resultat ist wieder String und Liste

zugleich. argv0 enthält den Namen, unter dem der Interpreter gestartet wurde, hier

also den Namen des TCL-Skripts.

5 exit beendet den Interpreter und kann einen exit-Code vorschreiben.

7 argv ist die Liste der Argumente, die außer unserem Skript an tclsh übergeben


wurden. lindex liefert hier mit Index 0 das erste Argument der Liste argv.

9 env ist ein Vektor, über den man das Environment des TCL-Skripts lesen und

ändern kann. open richtet eine Dateiverbindung ein; das zweite Argument r sorgt

für Lesezugriff. Hier lesen wir aus einer Pipeline, die von find kommt. find liefert

hier alle Pfade, die im Heimatkatalog beginnen.

10 Eine Dateiverbindung, mit der wir in eine Pipeline zu wc schreiben können, wird

in der Variablen cnt gespeichert. wc zählt hier die angelieferten Zeilen.

12 while ist ebenfalls ein TCL-Kommando mit zwei Argumenten, das als

Kontrollstruktur wirkt. Geschweifte Klammern verhindern jeden Textersatz, deshalb

kann das Kommando while sein erstes Argument jeweils neu bewerten und dann

entscheiden, ob die Kommandos ausgeführt werden sollen, die wir als zweites

Argument übergeben. Wie bei if muß wenigstens die öffnende geschweifte

Klammer des zweiten Arguments auch bei while auf der gleichen Zeile stehen wie

der Kommandoname while selbst.

13 string realisiert alle denkbaren String-Manipulationen. Hier vergleichen wir unser

Shell-Muster mit der Zeile, die gets aus der find-Pipeline gelesen und in der Variablen

line abgelegt hat.

14 Wenn der Vergleich stimmt, liefert string den Wert 1, und puts gibt die Zeile als

Standard-Ausgabe aus.

15 Außerdem wird die Zeile in die zweite Pipeline an wc geschickt.

17 gets liefert -1 am Dateiende und beendet das while-Kommando.

19 close löst eine Dateiverbindung auf und liefert etwaige Fehlermeldungen.

TCL ist eigentlich sehr leicht zu lernen, wenn man sich die Grundregeln klarmacht:

Jedes Zeichen wird nur einmal eingelesen, ersetzt und interpretiert. Zwischenraum

trennt die Worte eines Kommandos. Doppelanführungszeichen dienen vor allem

dazu, Zwischenraum in ein Wort zu bringen. Variablenwerte ($) und


Kommandoaufrufe ([]) werden ersetzt. Ein Wort ist jeweils ein String und eine Liste,

die jedoch nicht mehr weiter zerlegt oder nachbearbeitet werden. Innerhalb von

geschweiften Klammern wird überhaupt nicht ersetzt; das Resultat ist ein String und

eine Liste. Kommandos empfangen folglich Listen als Argumente und können sie

ihrerseits als Kommandofolgen einlesen, ersetzen und interpretieren lassen. So

entstehen konventionelle Kontrollstrukturen als Kommandos und nicht syntaktisch.

Sockets, Streams und Mail

Zwei Prozesse auf verschiedenen Rechnern können über TCP/IP-Mechanismen

miteinander kommunizieren, eine Familie von Protokollen, die weltweit im Internet

verwendet werden.

Auf der untersten Ebene bietet tcl-dp die Möglichkeit, Datagram- oder Stream-

Sockets einzurichten. Wie in C verbergen sich dahinter File-Deskriptoren, über die

man einzelne, kurze Nachrichten oder einen fehlerfreien Byte-Strom verschicken

kann. SMTP, das Protokoll zur Übermittlung von elektronischer Post, verwendet

einen solchen Byte-Strom als Träger:

$ abb2 —notk

220 next Sendmail ...

mail from:axel

250 axel... Sender ok

rcpt to:axel

250 axel... Recipient ok

data

354 Enter mail, end with "."

Subject: ein Test

Hier ist ein Brief

.


250 Ok

quit

221 next closing connection

ˆD

6545 closes file3 3381

Hier bringen wir das TCL-Skript aus Abbildung 2 zur Ausführung und bauen eine

Verbindung zu einem SMTP-Server auf.

1 #!/usr/local/bin/dpwish —f

2

3 set smtp [dp_connect localhost 25]

4 set socket [lindex $smtp 0]

5

6 set bg [exec cat —u = 0} {

10 puts $socket $line

11 }

12 dp_shutdown $socket all

13 }

14

15 puts "[pid] closes $smtp"

16 close $socket; catch {exec kill $b}; exit 0

Abbildung 2: Ein primitiver SMTP Klient

dpwish ist ein TCL-Interpreter, der auch die Kommandos aus dem tcl-dp-Paket

versteht. -notk als Argument verhindert, daß das TK-Toolkit zum Umgang mit dem X

Window System gestartet wird.

Wir schicken dann Kommandozeilen, und der Server reagiert darauf mit

Meldungen, von denen eigentlich nur die Zahlen am Anfang signifikant sind. Mit


mail und rcpt geben wir Sender und Empfänger unseres Briefs an. Nach data folgt

der Text, der mit einem Punkt abgeschlossen wird. quit beendet den Dialog mit

dem Server.

Da dieser Dialog offensichtlich zeilenorientiert ist, ist unser TCL-Skript in Abbildung

2 entsprechend einfach:

3 In dieser Form erzeugt das tcl-dp-Kommando dp_connect einen Stream-Socket

und verbindet ihn mit dem Hostnamen localhost, also unserem eigenen Rechner,

und dort mit dem Port 25, der nach Konvention zum SMTP-Server führt.

4 dp_connect liefert eine Dateiverbindung und die lokale Port-Nummer, über die

unser Klient sendet. In socket legen wir hier die Dateiverbindung ab, denn die lokale

Port-Nummer ist meistens uninteressant. lindex liefert ein Element aus einer Liste.

6 Da wir einen Dialog führen wollen, lassen wir das UNIX-Kommando cat im

Hintergrund die Daten, die der Server an uns schickt, ungepuffert zu unserer

Standard-Ausgabe kopieren. Mit


fork

Das Schöne an TCL ist, daß man von C aus einen Interpreter in eigene Programme

einbauen und ihn mit neuen Kommandos erweitern kann. Abbildung 3 zeigt, daß

selbst ein heikler Systemaufruf wie fork() als Kommando zur Verfügung gestellt

werden kann.

#include

#include

#include

static int ForkCmd (ClientData clientData,

Tcl_Interp * interp, int argc, char * argv [])

{ int pid;

if (argc != 1)

{ interp—>result = "wrong # args";

return TCL_ERROR;

}

switch (pid = fork()) {

case —1:

Tcl_PosixError(interp);

return TCL_ERROR;

case 0:

break;

default:

Tcl_DetachPids(1, & pid);

}

sprintf(interp—>result, "%d", pid);

return TCL_OK;

}

int Tcl_AppInit (Tcl_Interp * interp)


{ Tk_Window main = Tk_MainWindow(interp);

if (Tcl_Init(interp) == TCL_ERROR

|| (main && Tk_Init(interp)) == TCL_ERROR

|| Tdp_Init(interp) == TCL_ERROR)

return TCL_ERROR;

}

Tcl_CreateCommand(interp, "fork", ForkCmd, NULL, NULL);

tcl_RcFileName = "˜/.dpwishrc";

return TCL_OK;

Abbildung 3: Ein ‘‘fork’’ Kommando

ForkCmd() verpackt fork() so, daß diese Funktion mit Hilfe von

Tcl_CreateCommand() für das Kommando fork eingetragen werden kann. Beim

Aufruf kopiert der Systemaufruf fork() den gesamten TCL-Interpreter mit allen

Dateiverbindungen. Im ursprünglichen Prozeß liefert das Kommando fork die

Prozeßnummer der Kopie, in der Kopie liefert fork den Wert 0.

Programme wie tclsh oder dpwish verwenden fast immer das gleiche

Hauptprogramm, das zur Initialisierung eine Funktion Tcl_AppInit() aufruft. Wenn

man eigene Kommandos einbaut, muß man normalerweise nur diese Funktion

erweitern, damit die Kommandos entsprechend eingetragen werden. 2

Abbildung 4 zeigt, wie wir mit fork den Aufruf von cat aus Abbildung 2 durch TCL-

Code ersetzen könnten.

2

fork und viele, viele andere Systemaufrufe gibt es als Kommandos in einem Paket

tclX, das ebenfalls im Netz zu haben ist. Das Paket ist allerdings (noch) nicht mit tcldp

verbunden.


1 if {[fork] == 0} {

2 proc exit {n} {exec kill [pid]}

3 catch {

4 while 1 {

5 set ch [read $socket 1]

6 if {$ch == {}} break

7 puts —nonewline $ch; flush stdout

8 }

9 }

10 puts "[pid] exiting"; exit 0

11 }

Abbildung 4: Ein Subprozeß an Stelle von ‘‘cat’’

2 Da der Subprozeß auch eine Verbindung zum X-Server erben würde, sollten wir

dafür sorgen, daß er nicht etwa auf Windows zugreift oder gar im Zuge von exit alle

Fenster zerstört. Wir ersetzen deshalb das exit-Kommando durch eine eigene

Prozedur, die den Prozeß etwas abrupt beendet. proc ist auch ein einfaches

TCL-Kommando. Das erste Argument ist der Name einer Prozedur, die definiert oder

ersetzt wird. Das zweite Argument ist eine Liste von Parameternamen, über die

man auf die Argumentwerte zugreifen kann. Das dritte Argument ist eine Liste von

TCL-Kommandos als Prozedurkörper.

5 Da unser Partner nicht unbedingt komplette Zeilen schickt, lesen wir mit read

einzelne Zeichen.

6 Am Dateiende liefert read einen leeren Wert, und wir beenden die while-Schleife

mit break.

7 flush sorgt dafür, daß die Zeichen (abgesehen von Null-Bytes) auch einzeln

ausgegeben werden.


Diese Lösung ist recht ineffizient. Bei einer Socket-Verbindung kann man mit

dp_receive auch alle gerade verfügbaren Bytes empfangen:

set line [dp_receive $socket]

if {$line == {}} break

puts —nonewline $line

flush stdout

Mit diesem Körper wird die while-Schleife wesentlich weniger oft durchlaufen.

Alles auf einmal

Wenn man einen Terminal-Emulator unter UNIX implementiert, verwendet man in der

Regel zwei Prozesse, denn Lese- und Schreiboperationen blockieren einen Prozeß,

bis die gewünschten Daten vorhanden sind oder abgefertigt werden konnten.

Es gibt aber auch einen Systemaufruf select(), mit dem man unter anderem

feststellen kann, ob eine Lese- oder Schreiboperation blockieren würde. tcl-dp

enthält ein Kommando dp_filehandler, das verabredet, daß ein Kommando genau

dann aufgerufen wird, wenn eine Lese- oder Schreiboperation möglich ist.

Diese Architektur ist typisch für TCL: Ankunft oder Abfluß von Daten auf einer

Dateiverbindung sind Ereignisse, die mit einer Aktion verbunden werden können.

Die Aktion ist typischerweise ein Aufruf einer TCL-Prozedur, der dann synchron

abgearbeitet wird.

Abbildung 5 demonstriert, wie elegant wir damit in einem einzigen Prozeß einen

Dialog über einen Byte-Strom abwickeln können.

1 if [catch {

2 switch —— $argc {

3 0 { set server {localhost 7} }


4 1 { set server {localhost [lindex $argv 0]} }

5 2 { set server $argv }

6 default {error "usage: $argv0 ??host? port?"}

7 }

8 set server [eval dp_connect $server]

9 set socket [lindex $server 0]

10 } err] {

11 puts stderr $err; exit 1

12 }

13

14 dp_filehandler stdin r "doSend $socket"

15 dp_filehandler $socket r doReceive

16

17 proc doSend {socket mode fd} {

18 if {[gets $fd msg] == —1

19 || ! [dp_send $socket $msg]} {

20 puts "[pid] closing connection"

21 exit [catch {dp_shutdown $socket all}]

22 }

23 }

24

25 proc doReceive {mode fd} {

26 if {[set msg [dp_receive $fd]] == {}} {exit 0}

27 puts —nonewline $msg; flush stdout

28 }

Abbildung 5: Ein zeilenorientierter Stream-Klient

3 Ohne Argumente ruft dieses Skript einen echo-Service auf, der normalerweise zu

Testzwecken auf Port 7 zu erreichen ist:

$ abb5 —notk

hello, world


hello, world

ˆD

7492 closing connection

4 Ein Argument wird als lokale Port-Nummer interpretiert. Damit kann man zum

Beispiel auf Port 13 den daytime-Service anrufen, der unmittelbar nach

Verbindungsaufbau eine einzige Zeile mit Datum und Uhrzeit liefert:

$ abb5 —notk 13

Sun May 15 18:13:15 1994

5 Mit zwei Argumenten kann man dann einen beliebigen Internet-Host und Port

angeben.

6 switch ist ein TCL-Kommando, bei dem ein String nacheinander mit Mustern

verglichen wird. default erkennt hier als letztes Muster eine beliebig falsche

Angabe, und error hinterlegt eine Fehlermeldung.

8 eval sorgt hier dafür, daß die Liste $ser ver als zwei Worte an dp_connect

übergeben wird.

10 catch arbeitet eine Kommandofolge ab und liefert nicht Null, wenn ein Fehler

passiert. Man kann die Fehlermeldung in einer Variablen wie err ablegen lassen.

14 doSend soll aufgerufen werden, wenn Daten von der Standard-Eingabe gelesen

werden können. Wir übergeben der Prozedur zusätzlich die Socket-Verbindung,

denn die Standard-Eingabe muß ja dorthin kopiert werden.

15 doReceive muß sich analog um Antworten von der Socket-Verbindung kümmern.

Hier genügen die normalen Parameter, nämlich die Art des Zugriffs (lesen oder

schreiben) und die Dateiverbindung, auf der Daten bereitstehen.

19 dp_send schickt eine Zeile in einen Byte-Strom und liefert normalerweise die

Anzahl der übertragenen Zeichen. Im Gegensatz zu gets baut dp_send bei Fehlern

die Verbindung automatisch ab und liefert 0 als Resultat.


21 Wir berichten deshalb nur noch mit dem exit-Code, ob die Verbindung korrekt

abgebaut werden konnte.

26 dp_receive funktioniert sehr ähnlich. Wenn ein leerer String abgeliefert wird, ist

auch hier die Socket-Verbindung abgebaut worden, und wir beenden unseren

Klienten.

Datagram-Verbindungen

Ein Byte-Strom funktioniert zwischen Prozessen auf verschiedenen Rechnern so wie

eine Pipeline zwischen verwandten Prozessen auf dem gleichen Rechner: Alle Bytes

kommen an, und wenn ein Prozeß die Verbindung abbaut, merkt das der andere.

Intern ist das natürlich mit gewissem Aufwand verbunden.

Eine wesentlich billigere Lösung ist ein Datagram. Hier wird eine begrenzte

Datenmenge in einer einzigen Nachricht übermittelt — wenn sie ankommt, ist sie

unbeschädigt, aber sie muß nicht ankommen, oder sie kann sogar mehr als einmal

beim Empfänger auftauchen. Wenn wir beispielsweise am daytime-Service

interessiert sind, sollte eigentlich ein Datagram genügen:

$ abb6 —notk —v 13

hello

127.0.0.1 13:

Sun May 15 18:49:00 1994

world

127.0.0.1 13:

Sun May 15 18:49:02 1994

ˆD

7552 exiting


Für jedes Datagram, das wir zum Port 13 schicken, erhalten wir eine Zeile mit einem

Datum zurück. Abbildung 6 zeigt ein TCL-Skript für den Klienten.

1 set usage "usage: $argv0 ?—v? ??host? port?"

2

3 set vflag [expr {[lindex $argv 0] == "—v"}]

4 if $vflag {

5 set argv [lreplace $argv 0 0]

6 incr argc —1

7 }

8

9 switch —— $argc {

10 0 { set server {localhost 7} }

11 1 { set server {localhost [lindex $argv 0]} }

12 2 { set server $argv }

13 default { puts stderr $usage; exit 1 }

14 }

15

16 if [catch {

17 set server [eval dp_address create $server]

18 set socket [lindex [dp_connect —udp 0] 0]

19 } err] {

20 puts stderr $err; exit 1

21 }

22

23 dp_filehandler stdin r "doSendTo $socket $server"

24 dp_filehandler $socket r doReceiveFrom

25

26 proc doSendTo {socket server mode fd} {

27 if {[gets $fd msg] == —1} {

28 puts "[pid] exiting"; exit 0

29 }

30 if [catch {


31 dp_sendTo $socket "$msg\n" $server

32 } err] {

33 puts stderr $err; exit 1

34 }

35 }

36

37 proc doReceiveFrom {mode fd} {

38 global vflag

39 if [catch {set msg [dp_receiveFrom $fd]} err] {

40 puts stderr $err; exit 1

41 }

42 if $vflag {

43 puts [dp_address info [lindex $msg 0]]:

44 }

45 puts —nonewline [lindex $msg 1]; flush stdout

46 }

Abbildung 6: Ein zeilenorientierter Datagram-Klient

3 Zur Illustration soll -v als erstes Argument akzeptiert werden und zusätzlich

Rechneradresse und Port unseres Partners ausgeben. Die geschweiften Klammern

sind hier nötig, denn in Ausdrücken werden Strings nur dann verglichen, wenn sie

nach der Ersetzung im Parser noch als solche erkennbar sind.

5 Wenn -v angegeben wurde, wird dieses erste Argument mit lreplace aus der

Argumentliste argv entfernt.

6 Außerdem wird die Anzahl der verbleibenden Argumente korrigiert.

9 Die anderen Argumente werden analog zu Abbildung 5 interpretiert.

17 Diesmal müssen wir allerdings mit dp_address eine Adresse konstruieren, mit

der dann später die Nachricht adressiert wird.

18 Mit der Option -udp erzeugt dp_connect einen Socket für Datagram-Verkehr.


Durch die Angabe 0 lassen wir uns vom System wieder einen beliebigen lokalen Port

für den Klienten zuteilen.

23 Wieder hinterlegen wir mit dp_filehandler Prozeduren, die verfügbare Daten

transportieren. Zum Verschicken benötigen wir jedoch nicht nur unsere Socket-

Verbindung, sondern auch die Zieladresse. 3

31 dp_sendTo verschickt ein Datagram über einen entsprechenden Socket an eine

Zieladresse. Da gets den abschließenden Zeilentrenner von der Eingabezeile

entfernt hat, setzen wir ihn explizit hinzu.

38 doReceiveFrom muß auf die globale Variable vflag zugreifen.

39 dp_receiveFrom empfängt ein Datagram von einem entsprechenden Socket und

liefert die Adresse des Senders und den Text ab.

43 Wenn vflag gesetzt ist, verwandeln wir die Absenderadresse mit dp_address

info in eine Internet-Adresse für den Host und in eine Port-Nummer zurück und

geben sie aus.

Die Geister, die ich rief ...

Da dp_receiveFrom den Absender einer Nachricht erfährt, kann man einen

Datagram-Server sehr leicht aufbauen.

1 source abb8

2 set socket [spawnServer {dp_connect —udp 0}]

3

4 proc output cmd {

5 global socket from

3

Programmiert man in C, kann man die Zieladresse auch bei einem Datagram-

Socket ein für alle Mal hinterlegen.


6 catch {

7 set pipe [open |$cmd r]

8 while {[gets $pipe line] >= 0} {

9 dp_sendTo $socket "$line\n" $from

10 }

11 }

12 catch {close $pipe}

13 }

14

15 while {! [catch {

16 set msg [dp_receiveFrom $socket]

17 } err]} {

18 set from [lindex $msg 0]

19 set msg [lindex $msg 1]

20 switch —— [lindex $msg 0] {

21 df { output df }

22 mount { output /etc/mount }

23 ps { output "ps ax" }

24 sh { output [lreplace $msg 0 0] }

25 who { output who }

26 }

27 }

28 puts stderr $err; exit 1

Abbildung 7: Ein gefährlicher Datagram-Server

Abbildung 7 demonstriert, daß man bereits mit primitiven Mitteln recht aufregende

Resultate erzielen kann:

$ abb7 —notk

2599 on 2996

$ abb6 —notk 2996

who


axel console May 11 14:57

ats ttyp1 May 14 14:39

sh hostname

next

sh date

Sun May 15 19:54:02 MET DST 1994

sh ls —i makefile

441 makefile

Wer diesen Server zugänglich macht, hat unter Umständen bald nichts mehr zu

lachen. Dabei ist er sehr einfach zu konstruieren:

1 Da wir noch mehrere Server konstruieren wollen, verwenden wir eine

einheitliche Funktion spawnSer ver, deren Code wir zuerst mit source aus der Datei

abb8 einlesen.

2 spawnSer ver akzeptiert Kommandos zur Erzeugung von Sockets und liefert die

entsprechenden Dateiverbindungen. Hier lassen wir uns auch für den Server vom

System mit dp_connect einen freien Port zuweisen.

4 Die TCL-Prozedur output erhält ein UNIX-Kommando als Argument.

5 Sie greift auf die globalen Variablen zu, die die Socket-Verbindung und die

Absender-Adresse enthalten.

6 In einem Server darf absolut kein Fehler zum Abbruch führen, deshalb fangen wir

alle Probleme mit catch ab.

7 output liest Zeilen aus einer Pipeline von dem UNIX-Kommando und kopiert die

Zeilen mit dp_sendTo zurück zum Absender.

12 Die zweite catch-Anweisung garantiert, daß der File-Deskriptor der Pipeline

garantiert auch dann freigegeben wird, wenn irgendwo innerhalb der ersten

catch-Anweisung schon ein Fehler passiert ist.

15 Ein Server besteht immer aus einer Hauptschleife, die auf Arbeit wartet.


18 dp_receiveFrom liefert zuerst die Absender-Adresse.

20 Vom Datagram-Text interessiert uns das erste Wort. Da das TCL-Kommando

switch selbst Optionen akzeptiert, sorgen wir mit -- dafür, daß unser erstes Wort

keinesfalls als switch-Option angesehen wird.

21 Für verschiedene Worte legen wir jeweils eine Pipeline fest, die die gewünschte

Information über output an den Datagram-Socket zurückliefert.

24 Wenn wir ein Datagram lokal von einer Shell bearbeiten lassen, erhalten wir zwar

eine fast unbegrenzte Funktionalität, aber natürlich auch ein riesiges Sicherheitsloch,

denn diese Shell arbeitet mit den Zugriffsrechten desjenigen, der den Datagram-

Server gestartet hat.

Wie man einen Server startet, zeigt Abbildung 8.

1 proc spawnServer args {

2 set sockets {}; set ports {}

3 foreach cmd $args {

4 if [catch {set service [uplevel $cmd]} err] {

5 puts stderr $err; exit 1

6 }

7 lappend sockets [lindex $service 0]

8 lappend ports [lindex $service 1]

9 }

10

11 if [catch {set server [fork]} err] {

12 puts stderr $err; exit 1

13 }

14 if {$server != 0} {

15 puts "$server on $ports"

16 exit 0

17 }

18 close stdout

19


20 return $sockets

21 }

Abbildung 8: Funktion zum Start eines Servers

spawnSer ver soll unter anderem dafür sorgen, daß wir das entsprechende

Kommando nicht in den Hintergrund schicken müssen, obgleich der Server natürlich

parallel zu unseren Anfrageprozessen arbeiten muß.

1 Diese Prozedur verwendet args als letzten Parameternamen und kann deshalb

beliebig viele Argumente akzeptieren.

3 args ist dann eine Liste der restlichen Argumentwerte. foreach weist hier

jeweils einen Argumentwert an die Variable cmd zu und führt dann die Anweisungen

in der Schleife aus.

4 Jedes Argument ist ein TCL-Kommando, das mit uplevel im Kontext des

Aufrufers unserer Prozedur ausgeführt wird. Wie wir in Abbildung 7 sahen, muß das

Kommando eine Socket-Verbindung mit dp_connect einrichten.

7 Wir sammeln die Dateiverbindungen in einer Liste in der Variablen sockets und

die zugehörigen Port-Nummern in por ts.

11 Wenn wir alle Dateiverbindungen einrichten konnten, kopieren wir unseren

Prozeß.

14 Der ursprüngliche Prozeß erhält von fork die Nummer des neu erzeugten

Prozesses, der neue Prozeß erhält den Wert 0.

15 Der ursprüngliche Prozeß gibt die Prozeßnummer des eigentlichen Servers und

die Port-Nummern als Standard-Ausgabe aus und hört auf, damit sein Aufrufer

parallel zum Server fortgesetzt wird. Ist der Aufrufer eine Shell, könnten wir etwa

mit

$ set `abb7 —notk`


die Informationen in Shell-Variablen ablegen, um damit dann Klienten oder ein kill-

Kommando zu versorgen.

18 Der Server erbt alle Dateiverbindungen seines Erzeugers, also auch die Standard-

Ausgabe. Falls diese etwa mit einer Pipeline verbunden ist, müssen wir die

Verbindung unbedingt lösen, sonst würde zum Beispiel die oben gezeigte Zuweisung

in der Shell erst zustandekommen, wenn auch der Server beendet ist.

20 spawnSer ver bricht entweder den aufrufenden Prozeß ab, oder die Prozedur

liefert die erzeugten Socket-Verbindungen als Liste in einem neuen Prozeß.

rshd

Eigentlich sollte man Datagram-Verbindungen nur wählen, wenn man einzelne

Nachrichten zurückschicken will, die relativ kurz sind. Wenn unser Server viel

Information zurückliefern soll, dann lohnt sich der Aufwand, einen Byte-Strom

einzurichten. Abbildung 9 zeigt einen Server, der einen Dialog mit einer Shell erlaubt

und ihn über einen Byte-Strom abwickelt.

1 source abb8

2 set socket [spawnServer {dp_connect —server 0}]

3

4 while {! [catch {

5 set data [lindex [dp_accept $socket] 0]

6 } err]} {

7 catch {exec sh —i &@ $data}

8 catch {close $data}

9 }

10 puts stderr $err; exit 1

Abbildung 9: Ein naiver Server für ‘‘rsh’’


Dieser Server ist eine sehr rudimentäre Fassung von Programmen wie rshd, die eine

Shell auf einem fremden System zur Verfügung stellen. Der Server muß mit dem

Klienten aus Abbildung 5 oder mit einem Programm wie telnet angesprochen

werden:

$ abb8 —notk

2603 on 3544

$ telnet localhost 3544

$ who

axel console May 11 14:57

ats ttyp1 May 14 14:39

telnet richtet einen Byte-Strom zum angegebenen Rechner ein und emuliert eine

Terminal-Verbindung. Wie man sieht, kann man auch eine bestimmte Port-Nummer

verlangen. Wenn nur Text übertragen wird, stören die Konventionen im TELNET-

Protokoll nicht, die sich mit den Terminal-Parametern beschäftigen, und telnet kann

folglich auch mit unserem Server kooperieren.

1 Auch dieses Skript richtet seinen Socket und den eigentlichen Server-Prozeß mit

spawnSer ver ein

2 Mit der Option -ser ver konstruiert dp_connect einen Stream-Socket. Die Port-

Nummer lassen wir uns auch hier vom System zuteilen.

5 Der Aufbau einer Byte-Strom-Verbindung erfolgt in zwei Schritten. dp_accept

wartet auf dem ursprünglich mit dp_connect angelegten Socket auf einen Anruf und

liefert selbst einen neuen Socket sowie die Adresse des Anrufers.

7 Da Stream-Sockets wie ganz normale Dateiverbindungen benützt werden

können, lassen wir mit exec einfach die Shell direkt vom Socket lesen und ihre

Ausgabe und Diagnose-Ausgabe zum Socket schreiben.

8 Wie in jedem Server, müssen wir auch hier sorgfältig darauf achten, daß wir alle

Dateiverbindungen auch wieder auflösen.


9 Wenn die Shell einen Auftrag erledigt hat, ist zwar der Socket für den

Datentransfer wieder aufgegeben worden, aber der ursprüngliche Socket besteht

nach wie vor. Dort gehen inzwischen oder später neue Anrufe ein, die wir in der

while-Schleife nacheinander bearbeiten.

Super-Ser vice

Bisher sieht es so aus, als ob wir für jeden einzelnen Service einen neuen Prozeß

benötigen, der den zuständigen Port bewacht. Mit dp_filehandler können wir

jedoch Aktionen mit beliebig vielen Dateiverbindungen verknüpfen. Damit das

vernünftig funktioniert, müssen wir allerdings auch dafür sorgen, daß die

eigentlichen Service-Leistungen von Subprozessen erbracht werden, die erst

gestartet werden, wenn unser Super-Server entsprechende Anrufe entdeckt.

Abbildung 10 zeigt ein einfaches Beispiel, bei dem je ein Datagram- und ein Stream-

Socket bewacht werden.

1 source abb8

2 set sockets [spawnServer {dp_connect —server 0} \

3 {dp_connect —udp 0}]

4

5 dp_filehandler [lindex $sockets 0] r tcp

6 dp_filehandler [lindex $sockets 1] r udp

7

8 proc tcp {mode fd} {

9 catch {

10 set data [lindex [dp_accept $fd] 0]

11 if {[fork] == 0} {

12 catch {exec sh —i &@ $data}

13 exit 0

14 }


15 }

16 catch {close $data}

17 }

18

19 proc udp {mode fd} {

20 catch {

21 set dgram [dp_receiveFrom $fd]

22 if {[fork] == 0} {

23 catch {

24 set socket [lindex [dp_connect —udp 0] 0]

25 set from [lindex $dgram 0]

26 set msg [lindex $dgram 1]

27 switch ...

28 }

29 exit 0

30 }

31 }

32 }

33

34 proc output {cmd} {

35 upvar socket socket; upvar from from

36 catch ...

37 }

Abbildung 10: Ein einfacher Super-Server

2 Diesmal legen wir mit spawnSer ver zwei Sockets an.

5 Mit jedem Socket wird eine Aktion verknüpft. Anschließend vereinbaren wir

noch die nötigen Prozeduren, und dann wartet die unsichtbare Hauptschleife des

TCL-Interpreters auf Ereignisse.

10 Wenn am Stream-Socket ein Anruf eintrifft, wird zuerst der neue Socket zur

Datenübertragung eingerichtet.


11 Anschließend erbt ein neuer Prozeß diesen Socket.

12 Der Subprozeß wickelt den Auftrag ab und wird dann beendet. Er wartet hier

also ab, bis der Dialog mit einer Shell beendet ist.

16 Im ursprünglichen Server-Prozeß muß der neue Socket unbedingt wieder

aufgegeben werden. Anschließend ist die Rolle des Servers in der Abwicklung des

Auftrags bereits beendet.

21 Die Bearbeitung eines Datagrams verläuft ganz ähnlich. Der Server holt das

Datagram ab.

22 Die Daten werden an einen neuen Prozeß vererbt.

24 Damit nun aber der Server neue Datagram-Aufträge auf seinem publizierten Port

empfangen kann, geben wir dem neuen Prozeß einen neuen Datagram-Socket, über

den er seine eigene Antwort zurückschickt.

25 Aus dem Datagram bestimmen wir Absender und Text.

27 Der switch aus den Zeilen 20 bis 26 in Abbildung 7 decodiert auch hier den Text

und ruft output auf.

29 Auch hier muß der Subprozeß mit exit beendet werden.

35 output stammt eigentlich auch vom einfachen Datagram-Server in Abbildung 7.

Die Variablen socket und from sind jetzt aber nicht global, sondern im Aufrufer von

output definiert. Mit upvar kann man eine ‘‘globalere’’ Variable lokal binden — hier

importieren wir diese Variablen vom Aufrufer zu output.

36 Der restliche Körper der Prozedur output entspricht den Zeilen 6 bis 12 in

Abbildung 7.

Unser Super-Server erzeugt unbeschränkt viele parallele Prozesse. Je nach Art der

Aufträge kann das Probleme geben.

Der offizielle Super-Server inetd liest eine Konfigurationstabelle /etc/inetd.conf ein,

aus der hervorgeht, welche Port-Nummer er mit welchen Kommandos verknüpfen


soll. Die Tabelle legt auch fest, ob nur jeweils ein Antwort-Prozeß für eine Port-

Nummer aktiviert werden soll.

For tsetzung folgt ...

Sockets sind nur die Basis, auf der man Kommunikation mit TCP/IP-Protokollen

betreibt. tcl-dp stellt noch wesentlich nützlichere Mechanismen für verteilte

Programme zur Verfügung. Darüber werde ich im nächsten Heft berichten.

Die Quellen zu diesem Artikel befinden sich wie üblich als

pub/local/hanser/um-94.3 auf unserem FTP-Server ftp.rz.uni-osnabrueck.de.

Weitere Magazine dieses Users
Ähnliche Magazine