6. Implementierung


6.1. Einleitung

Die Implementierung des Call-Servers wurde von Anfang an möglichst modular gehalten, was trotz zahlreicher Änderungen erhalten blieb. Anfangs sollte der Server in der Lage sein, verschiedene Dienste zu realisieren bzw. sollte um solche leicht erweiterbar sein. Anhang [app-files] enthält eine Liste aller zur Implementierung gehörenden Files. Neben dem Hauptprogramm des Servers (iSrv.c) ist dort für jeden der realisierten Dienste ein eigenes File aufgeführt.

Durch die im Laufe der Arbeit zunehmende Mächtigkeit der Skriptsprache CPL, hat sich gezeigt, daß die angedachten und teilweise realisierten Dienste viel flexibler mit CPL realisierbar sind. Deshalb soll hier nur noch der Skript-Service (iSrvScript.c) beschrieben werden.

Call-Server architecture

Abbildung [pic-arch] zeigt die Server-Architektur, einschließlich des geplanten Tcl-Einsatzes (gestrichelte Linie). Die dick umrandeten Linien kennzeichnen die von mir implementierten Komponenten. Die Implementierung umfaßt im wesentlichen:


6.2. Der Skript-Compiler

Zu Beginn der Arbeiten entstand die Frage, ob CPL-Skripte durch einen Interpreter abgearbeitet werden sollen oder erst compiliert und dann interpretiert werden sollen. Für letzteres sprächen geringfügige Laufzeitvorteile (durch den entfallenden Parser- und Übersetzungsvorgang), sowie erhöhte Sicherheit vor Laufzeitfehlern. Meines Erachtens ist einer der größten Nachteile beim Einsatz interpretierender Sprachen (vor allem in Produktionsumgebungen), die Gefahr von Laufzeitfehlern.

Der Laufzeitgewinn durch den Wegfall des Übersetzungsvorgang ist im Verhältnis zur Gesamtzeit für das Abarbeiten eines Skriptes jedoch so gering, daß er vernachlässigt werden kann. Um Skripte sowohl auf syntaktische Richtigkeit als auch deren Ablauf zu testen, gibt es das Programm cpldebug (siehe [cpldebug]).

So werden CPL-Skripte interpretiert, was beim geplanten Einsatz des Tcl-Interpreters ohnehin unumgänglich ist. Jedoch erfolgt die Interpretation nicht, wie in Kommando-Shells üblich, zeilenorientiert, sondern das CPL-Skript wird zunächst in einen Pseudo-Code überführt, der in einem zweiten Schritt abgearbeitet wird. Hierfür zerlegt ein Scanner die Zeichen der Skriptdatei in einen Symbol- oder Token-Strom (lexikalische Analyse). Dieser wird einem Parser zugeführt, der die Syntaxanalyse vornimmt (siehe auch [Aho88:Comp]). Nach bestandener Syntaxprüfung wird ein Pseudo-Code (P-Code) erzeugt, der anschließend von einem Interpreter ausgeführt (siehe [p-code-int]). Für den Übersetzungsvorgang wird die Skriptdatei nur einmal durchlaufen (1-Pass-Compiler).

Parser und Scanner sind in iSrvCplComp.c enthalten. Folgender Ausschnitt zeigt ein rudimentäres C-Programm, das das Skript test.cpl mit Hilfe der in iSrvCplComp.c exportierten Funktionen in P-Code überführt:

#include "iSrvCplComp.h"

main()
{
    FILE*          logf;
    t_script*      script;
    char*          fn = "test.cpl";

    ...
    if (Cpl_NewScript (script, fn, logf) == CPL_ERROR)
      exit (1);
    if (Cpl_ParseMain (script) != CPL_OK)
      exit (1);
    ...
}

Zunächst wird mit Cpl_NewScript() eine neue script-Struktur erzeugt. Außer dem Namen des Skriptes muß ein Pointer auf ein bereits geöffnetes Logfile übergeben werden. Der anschließende Aufruf von Cpl_ParseMain() öffnet die Skriptdatei und startet die Übersetzung. Im Falle von Fehlern werden diese auf stderr ausgegeben, und Cpl_ParseMain() terminiert mit CPL_ERROR.

Während des Übersetzungsvorganges wird eine Symboltabelle aufgebaut, auf die über die script-Struktur zugegriffen wird. Diese Tabelle enthält Einträge für Variablen, Zustandsnamen, Prozedurnamen und Konstanten und wird in Cpl_NewScript() mit den sys-Variablen vorbelegt. In einer weiteren Tabelle werden alle im Skript definierten Zustände sowie deren Startadressen (1. Op-Code im Anweisungsblock hinter ) gesammelt. In jedem Zustand werden die definierten Events mit den Startadressen der Anweisungsblöcke festgehalten. Alle Datenstrukturen, die CPL betreffen, sind in iSrvCpl.h vereinbart. Zusammen mit einer Variablen, die den aktuellen Zustand kennzeichnet, bildet die Zustandstabelle die State-Machine, die nun vom Interpreter ausgeführt wird.

Neben dem scheinbar höheren Aufwand für das Erzeugen des P-Codes, hat diese Form der Skriptabarbeitung mehrere Vorteile:

Vor allem der letzte Punkt hat mich dazu gebracht, CPL-Skripte nicht zeilenorientiert abzuarbeiten.


6.3. Der P-Code-Interpreter

Nach erfolgreicher Übersetzung steht der P-Code in einer Tabelle auf die über die script-Struktur zugegriffen wird. Letztere enthält zusätzlich einen Programcounter, ein Index, der auf den jeweils nächsten auszuführenden Op-Code in die Code-Tabelle zeigt. Ein Op-Code ist lediglich eine Struktur mit einem Op-Code und drei optionalen Operanden. Dies können Offsets in die Symboltabelle oder Sprungadressen sein. Tabelle [tab-opcodes] zeigt die Umsetzung von CPL Anweisungen in Op-Codes, dabei kennzeichnet sym Operanden, die in der Symboltabelle liegen. Sprungadressen sind mit PC gekennzeichnet.


6.4. Scheduling und Timing

Nach dem Compilieren des Skriptes ist die State-Machine bereits initialisiert, so kann nun die Interpretation des P-Codes mit Cpl_Schedule() gestartet werden. Als Parameter wird die script-Struktur diesmal in einer übergeordneten call-Struktur übergeben, welche die Kontextinformationen des aktuellen Anrufes enthält.

main ()
{
    ...
    Cpl_Schedule (&call, evEnter, NULL);
    IsdnMainLoopp ();

} /* main() */

Zusätzlich erhält Cpl_Schedule() einen Event-Typ und einen optionalen String-Parameter übergeben. Nach der Ausführung des ersten Op-Codes setzt Cpl_Schedule einen Timer mit 0 und terminiert. Dadurch, daß nur ein Befehl ausgeführt wird, ist sichergestellt, daß die Kontrolle so schnell wie möglich an die Isdn-Main-Loop zurückgegeben wird, damit ggf. anstehende ISDN-Events behandelt werden können. Da der mit 0 gesetzte Timer sofort abgelaufen ist, wird durch die Handler-Routine erneut Cpl_Schedule aufgerufen. Die CPL-Anweisungen, bei denen so verfahren wird, sind in Tabelle [tab-opcodes] ohne aufgeführt.

CPL statement

generated op codes CPL statement generated op codes
accept opAccept sym exec opExec sym
redirect opRedirect sym audiorec opAudiorec sym
<proc-call> opCall sym cvt cvt sym sym sym
enter enter sym exit exit
hangup opHangup sym if opCond PC
opGoto PC incr opIncr sym sym
invite opInvite proc opReturn
timer local/global opStartTimer sym sym timer stop opStopTimer sym

Op-Codes generated by the script compiler

Die mit gekennzeichneten Befehle gelten hingegen als kritisch, d.h. ihre Ausführungszeit kann nicht vorhergesagt werden. Aus diesem Grunde wird für und ein eigener Prozeß erzeugt, dessen stdout/stdin jeweils über eine Pipe mit dem Hauptprozeß verbunden ist. Bei der Ausführung dieser beiden Befehle ist der Hauptprozeß also nur für die Zeit der Prozeßerzeugung blockiert.

6.4.1. _Event-Behandlung mit CplSchedule()

Wie schon angedeutet, werden eintretende Events mit Cpl_Schedule() behandelt. Die möglichen Events sind in iSrvCpl.h durch den Aufzählungstyp t_events vereinbart. Beim Eintreten eines Events ruft also der betreffende Programmteil Cpl_Schedule() mit dem Eventtyp auf. Bei Timeouts und erkannten DTMF-Zeichenfolgen wird in dem dritten Parameter der Timername bzw. die erkannte Zeichenfolge übergeben. Cpl_Schedule() sucht in der Event-Tabelle des aktuellen Zustandes nach einer entsprechenden Eventdefinition und setzt die Code-Ausführung an der entsprechenden Stelle fort. Wurde in der Tabelle keine passende Event-Vereinbarung gefunden, so wird das Event ignoriert.

Neue Events, wie beispielsweise das Auftreten von Laufzeitfehlern, bei denen sysStatus 0 wird, können also eingeführt werden, indem

  1. in iSrvCpl.h der neue Typname eingetragen wird,
  2. im Skript-Compiler in der Funktion Cpl_ParseEventDef() die entsprechende Syntax geparsed wird. Cpl_EventAdd() macht den Rest, d.h. die Event-Definition wird in der Tabelle des aktuellen Zustandes zusammen mit dem aktuellen Programcounter abgelegt.
  3. An einer entsprechenden Stelle im Interpreter iSrvCplInt.c oder iSrvCplExec.c das Ereignis abgefragt und Cpl_Schedule() aufgerufen wird.


6.5. Testen von CPL-Skripts

Da es beim ernsthaften Einsatz des Call-Servers nicht zumutbar ist, daß während der Behandlung eines Anrufes Fehler im Skript zu seinem Abbruch führen, wird ein Programm benötigt, mit dem Skripte getestest werden können. Das Programm cpldebug ist hierfür gedacht. Nach einer Syntaxprüfung wird das Skript abgearbeitet. Die möglichen Events können per Tastatur simuliert werden.

Da cpldebug ohne ISDN und Netzwerkzugang arbeitet, beschränkt sich die Ausführung der einzelnen Kommandos auf deren Ausgabe am Bildschirm. Folgender Ausschnitt zeigt die Abarbeitung des Skriptes red.cpl (Benutzereingaben sind unterstrichen):

BEGIN CODE EXECUTION: resuming... PC=0000 call 'SayHello': branching 0008 PC=0008 log 'HELLO' PC=0009 return PC=0001 redirect 'rtp://becks:9090'

>>>S000: Enter event id (0=evEnter, 1=Disc, 2=Timeout, 3=DTMF, 4=speech, 5=Reschedule):

Während der Code-Ausführung wird der Program-Counter (PC=xxxx) mit dem dazugehörigen Befehl ausgegeben. Nach dem Abarbeiten des Blocks des ersten Zustands, bleibt das Programm stehen und wartet auf Eingabe eines Events. S000 ist der augenblickliche Zustand, in dem sich die State-Machine befindet. Alle Zustände eines Skriptes sind beginnend bei 0000 fortlaufend durchnumeriert. Das Skript red.cpl sieht wie folgt aus:

state FirstState {
    on enter {
      SayHello
      redirect "rtp://becks:9090"
    }
    on disconnect {
      enter FinalState
    }
}

state FinalState {
    on enter {
      hangup
      exit
    }
}

proc SayHello {
    log "HELLO"
}

Da in diesem Skript außer Enter keine anderen Events definiert sind, führt lediglich die Eingabe von 0 bzw. 1, für die Simulation des Enter- bzw. Disconnect-Events zu einer Reaktion des Programms. Durch Angabe eines optionalen Parameters (Integer > 0) beim Aufruf von cpldebug, können Symbol- und Zustandstabelle des Interpreters ausgegeben werden:

SYMBOL TABLE:
   ind  line  type  current value
   000  00001 var   'sysVersion'
   001  00001 var   'sysCallDuration'
   002  00001 var   'sysDate'
   003  00001 var   'sysDay'
   004  00001 var   'sysTime'
   005  00001 var   'sysStatus'
   006  00001 var   'sysCallingParty'
   007  00001 var   'sysCalledParty'
   008  00001 var   'sysDtmf'
   009  00001 var   'sysHostname'
   010  00001 var   'sysTrace'
   011  00001 var   'sysUsername'
   012  00002 state 'FirstState' StateId = 0
   013  00005 proc  'SayHello' PC = 8
   014  00005 const 'rtp://becks:9090'
   015  00009 state 'FinalState' StateId = 1
   016  00021 const 'HELLO'

STATE TABLE:
  FirstState
    ON ENTER      PC=0000
    ON DISCONNECT PC=0003
  FinalState
    ON ENTER      PC=0005

CODE TABLE:
      0000  CALL SayHello (PC=0008)
      0001  REDIRECT @14
      0002  STOP
      0003  ENTER FinalState
      0004  STOP
      0005  HANGUP
      0006  EXIT
      0007  STOP
      0008  LOG @16
      0009  RET

Die Zustandstabelle enthält neben dem Zustandsnamen sowie den definierten Events deren Einstiegspunkte in die Code-Tabelle. Letztere wird am Ende ebenfalls ausgegeben. Die mit @ versehenen Argumente sind Referenzen in die Symboltabelle.


File was created Wed Feb 26 18:31:47 1997 by tex2html