SOP322/ćw - temat D1

Gniazda BSD w języku Tcl.

Systemy rozproszone (z przesyłaniem komunikatów).

System rozproszony składa się z wielu procesów,
    z których żaden nie ma dostępu do całego systemu.
Procesy mogą się komunikować "przez przesyłanie komunikatów" (ang. message passing).

W systemie rozproszonym typu klient/serwer dzieli się procesy na klientów i serwery;
    użytkownicy zazwyczaj włączają programy klienckie,
    serwery świadczą usługi dla klientów i często działają w sposob ciągly.

Połączenia między procesami mogą być realizowane jako "połączenia tcp",
    tworzone przy pomocy gniazd BSD;
    przez te połączenia przesyła się komunikaty (trzeba zdef. ramkę komunikatu)

Sposoby przyjmowania komunikatów przez serwer (od wielu klientów):
    - komunikaty przyjmowane asynchronicznie (= zdarzenia, ang. events);
         "event-based/driven server" czyli serwer zdarzeniowy
    - komunikaty przyjmowanie synchronicznie przez osobne wątki;
         "threaded serwer" czyli serwer wątkowy
        

Aplikacje rozproszone i język Tcl.

W języku Tcl można bardzo łatwo programować systemy rozproszone,
nawet niskopoziomowo, przy pomocy gniazdek BSD,
z uwagi na specyficzne cechy języka :
    - zasadę "wszystko jest stringiem"
         tj każda struktura danych ma łatwo dostępną reprezentację stringową
    - ułatwienia "programowania zdarzeniowego" (ang. event-driven programming),
         (asynchroniczne) przyjście komunikatu to zdarzenie; serwer do którego
         podłączyło się wielu klientów musi obsługiwać dużą liczbę zdarzeń ...


Materiały na temat języka Tcl

Gniazda w języku Tcl.

Prosty przykład użycia gniazd:
## klient
#
set host "localhost"
set s [socket $host 10000]
# tworzymy połączenie tcp z serwerem na maszynie $host, port=10000
# komenda socket zwraca gniazdko repr. koniec tego połączenia

puts $s "A ku ku !!!"
# wysylamy dane przez gniazdko s
flush $s
# flush to wypróżnienie buforów
# !!! bardzo ważne bez tego dane nie zostaną "wypchnięte" !!!

close $s


## serwer 1
# - trzeba go uruchomić pod osobnym wish-em
# - od każdego klienta spodziewamy sie 1 linii tekstu zakończonej przez \n
# który wypisujemy na konsoli i zamykamy połączenie tcp ...
#
socket -server obsluga 10000
# ustanawiamy serwer na porcie 10000, na lokalnej maszynie
# zgloszenie się klienta jest obsługiwane przez proc "obsluga"
proc obsluga {s args} {
# s to socket klienta po stronie serwera
set linia [gets $s]
puts "od $s: $linia"
close $s
}


## serwer 2
# - pozwala klientowi wysłać dowolną liczbę linii
# - obsluguje danego klienta tak dlugo, aż nie zamknie on gniazdka!!!
#
socket -server obsluga 10000
proc obsluga {s args} {
fileevent $s readable "obslugaKli $s"
# dla każdego zgłaszającego się klienta definiujemy
# obsługę zdarzenia "readable" na gniazdku tego klienta ...
# uwaga: jeśli można odczytać 1 znak to można odczytać cały komunikat!
}
proc obslugaKli s {
if {[eof $s]} { close $s; return }
# "eof" pojawi się dokładnie wtedy, gdy klient zamknie gniazdko!
# wtedy po stronie serwera usuwamy zasoby związane z tym klientem
set linia [gets $s]
puts "od $s: $linia"
}

Komunikaty w języku Tcl.

Wygodne narzędzie do przesyłania komunikatów:
    procedury send_msg i recv_msg ...
         ich kod zawiera równocześnie def ramki komunikatu;
         ramka jest tak zdefiniowana, że może przenosić dowolne dane (także binarne!!!);
             !!! to ostatecznie rozwiązuje problem przekazywania dowolnych struktur danych
                 między klientem i serwerem !!!
         procedury *_msg wymagają określonej konfiguracji kanału/gniazda:
             wyłączona translacja znaków końca linii  (-translation binary)
             encoding = utf-8, tj wszystkie znaki kodowane w formacie utf-8
                 (Uwaga: string tcl to ciąg znaków unicode; jeśli wysyłamy string kanałem,
                     to możemy zdefiniować jak znaki mają być kodowane
                     przy pomocy opcji -encoding "sposob kodowania")
             kanały pracuja w trybie BLOKUJĄCYM !!! (to jest ustawienie std.)

Jak obsługiwać wielu klientów po stronie serwera?
    można to zrobić na 3 sposoby:
        1. zdef obslugę zdarzeń readable dla każdego gniazda klienta po stronie serwera
               i obslugiwać zdarzenia "przyjścia komunikatu" na jednym wątku;
                   czyli bezpośrednio tworzymy tzw maszynę (skończenie) stanową
                      to jest najprostsza metoda, i najprościej się analizuje takie programy,
                         jednak czasami wymaga skomplikowanego kodu ...
                  to jest "serwer zdarzeniowy" ...
        2. dla każdego zgłaszającego się klienta tworzyć na serwerze osobny wątek
               i przekazać mu gniazdo klienta;
               następnie wątek synchronicznie obsluguje komunikaty przychodzące od klienta
               być może na zasadzie pytanie/odpowiedź (= RPC, Remote Procedure Call)
              to jest "serwer wątkowy" ...
        3. dla każdego przychodzącego komunikatu tworzyć osobny wątek
               i przekazywać mu komunikat, który ma obsłużyć
               ............................

## proc do przesyłania komunikatów:
#
proc podlacz {host port} {
# tej procedury używamy tylko po stronie klienta!
  set s [socket $host $port]
  fconfigure $s -translation binary
  fconfigure $s -encoding utf-8
# konfiguracja kanału/gniazda
  return $s
}
proc send_msg {s msg} {
  set dlugosc [string length $msg]
  set wyslac [binary format i $dlugosc]
  append wyslac $msg
  puts -nonewline $s $wyslac; flush $s
}
proc recv_msg {s msg} {
  set x1 [read $s 4]
  if {$x1=="" && [eof $s]} return
  if {[string length $x1]!=4} {error "blad recv_msg /1"}
  binary scan $x1 i dlugosc
  set x2 [read $s $dlugosc]
  if {[string length $x2]!=$dlugosc} {error "blad recv_msg /2"}
  upvar $msg y; set y $x2
}


## klient używający *_msg
#
set s [podlacz $host 10000]
send_msg $s "a ku ku !!!"
send_msg $s {"a ku ku !!!" 1 2 3 4 5}
send_msg $s [list "zapisz" "plik.gif" {...gif...}]
# w tym przypadku komunikat to lista 3 elementow;
# 3 element to zawartość pliku gif, czyli "dane binarne"
# mozna to traktowac jako rozkaz "zapisz" wysłany do serwera
 # z 2 parametrami: nazwą pliku i danymi które mają być w tym pliku zapisane
recv_msg $s odpowiedz
   # 2 parametr recv_msg to nazwa zmiennej, w której ma być zapisany komunikat
puts "przyszła następująca odpowiedz: $odpowiedz"

## serwer używający *_msg
#
socket -server obsluga 10000
proc obsluga {s args} {
fconfigure $s -translation binary
fconfigure $s -encoding utf-8
fileevent $s readable "obslugaKli $s"
}
proc obslugaKli {s} {
upvar ::dane,$s dane
# z kazdym gniazdkiem klienta po stronie serwera łączymy
# zmienna globalną o nazwie dane,$s
# (może to być tablica, czyli zbiór zmiennych -tak jak w tym przykładzie)
# dla tej zmiennej tworzymy alias "dane" ...

recv_msg $s msg
# odbieramy pojedynczy komunikat
if {[eof $s]} { close $s; unset dane; return }
# czyszczenie, jeśli klient zamknął gniazdko

switch [lindex $msg 0] {
# obsluga komuniaktu; tutaj zakłądamy, że jest to lista,
# w której pierwszy element to nazwa rozkazu ...
rozkaz1 {
# wykonujemy rozkaz 1 ...
set dane(jakies_pole) "???"
# mamy wygodny dostęp do prywatnych danych klienta po stronie serwera!!!
}
rozkaz2 {
# wykonujemy rozkaz 2 ...
}
rozkaz3 {
# wykonujemy rozkaz 3 ...
}
}
}

# ---

Zadanie T1 "czat tekstowy"
Zaprogramuj tradycyjny czat tekstowy pozwalający "rozmawiać" dowolnej liczbe użytkowników
posiadających oprogramowanie klienckie ...
Powinien on mieć następujące cechy:
1. każdy użytkownik widzi 2 okna: duże w którym sa komunikaty od społeczności,
oraz małe w którym sam pisze komunikat (tj krótki tekst) który zamierza wysłać ...
2. na spodzie znajdują się guzik "wyslij", pole entry "login", guzik "podłącz" i "odłącz",
guzik "kto tam jest" - wyświetla liste podłączonych użytkowników (w danej chwili),
guzik "pobudka" - budzi użytkowników np wydając jakiś dzwięk.
3. cała społeczność widzi gdy ktoś się podłącza/ odłącza
Wskazówki:
Szkielet tworzący GUI programu :
text .t1 -height 25 -width 70; pack .t1 -fill both -expand 1  
text .t2 -height 10; pack .t2 -fill both
frame .f1; pack .f1
button .f1.b1 -text "Wyslij" -command { ... }; pack .f1.b1 -side left
# zamiast ... należy wpisać kod obsługi naciśnięcia guzika ...
entry .f1.e1 -width 15 -textvariable login; pack .f1.e1 -side left
# wpisany w to pole login bedzie dostępny przez zm. globalną login!
button .f1.b2 -text "Podłącz" -command { ... }; pack .f1.b2 -side left
button .f1.b3 -text "Odłącz" -command { ... }; pack .f1.b3 -side left


Zadanie T2 "czat audio (bez dzwięku!)"
Chodzi o możliwość przekazywania strumienia audio między użytkownikami
w trybie halfduplex (tzn jeden nadaje - wielu słucha).
Obsługę dzwięku dostarcza pakiet "snack", jednak w tym zadaniu nie trzeba go używać;
chodzi tylko o to aby zapewnić, że w danej chwili "jeden nadaje a wielu słucha",
przy czym WYMAGA się aby cały czas była spełniona "zasada X":
    jeśli istnieje słuchający to musi istnieć nadający.
       (zasada ta wynika ze specyficznych cech pakietu snack...)
Oprogramowanie klienta powinno zawierać guzik "chcę nadawać",
który jest czerwony gdy możemy mówić i zielony gdy słuchamy...
  .b config -bg green/red; # tak się zmienia kolor widgetu button!


Operacje I/O w Tcl - szczegóły.

Kanał  - to tcl-owy odpowiednik deskryptora w systemie operacyjnym;
kanały mogą dotyczyć plików lub gniazdek (czyli końcówek połączenia tcp);

Operacje I/O:
Tryb blokujący/ nieblokujący kanału.
    tryb nieblokujący włączamy konfigurując odp kanał:
        fconfigure $s -blocking 0
    tryb nieblokujący dla puts:
        działa inaczej niż w j. C gdzie jeśli nie można pisać to jest zwracany bład;
        tutaj puts nieblokujące nigdy nie blokuje, ale dane są automatycznie
           buforowane przez tcl i wysyłane gdy to się stanie możliwe ...

(niedokonczone !!!)

Zdarzenia w Tcl.

Kolejka zdarzeń.
    skrypt tcl uruchamiany przez "wish" ma pojedynczą pętle zdarzeń
      
która pobiera zdarzenia z kolejki zdarzeń i je obsługuje
       zdarzanie różnego typu są w tej samej kolejce !!!
           (oprócz fileevents są też zdarenia GUI, wątkowe, timery i inne)
       komendy blokujące (np blokujące gets) wyłączają chwilowo
          pętle zdarzeń !!!
    skrypty uruchamiane przez "tclsh" muszą same utworzyć pętle zdarzeń
       przy pomocy komendy "vwait zmienna"
          komenda ta czeka na modyfikacje "zmienna", równocześnie obslugując
             zdarzenia z kolejki zdarzeń ...
          UWAGA: nie powinno się używać vwait w proc obslugi zdarzenia,
             gdyż to prowadzi do niebezpiecznych błędów!!!!!!!!!!
    UWAGA 2: często dochodzi do następującego błedu:
        pewna komenda blokuje pętle zdarzeń, gdy są aktywne mechanizmy
           opierające się na pętli zdarzń ... wtedy one przestają działać

Problem zakleszczenia "duże pytanie/ duża odpowiedź".
    problem ten wystepuje gdy wysyłamy do serwera duże pytanie
       a serwer zwraca duzą odpowiedż;
          przy nieumiejętnym programowaniu łatwo doprowadzić tu do zakleszczenia
    UWAGA: użycie trybu nieblokującego puts wcale nie jest dobrym rozwiązaniem!!!
        gdyż w takim kodzie
            fconfigure $s -blocking 0
            puts $s "duze dane"
            fconfigure $s -blocking 1
            set odp [read $s]; # spodziewamy sie dużej odpowiedzi
        operacja read może zablokować pętle zdarzeń,
            na której opiera się przesyłanie danych nieblokującego puts !!!
    Jak to należy zaprogramować ???
        odp:
           puts blokujące,
           read nieblokujące i asynchroniczne (przy pomocy fileevent) !!!

(niedokonczone !!!)

Maszyna (skończenie) stanowa.

Serwer obsługujący komunikaty przychodzące asynchronicznie (zdarzenia)
można traktować jako maszynę (skonczenie) stanową ...
Maszynę taką można przedstawić jako graf, w którym wierzchołki to stany,
a komunikaty/zdarzenia to etykiety krawędzi.


Zadanie T3
Zaprogramuj serwer (zdarzeniowy) realizujący następującą maszynę stanową:
- są w użyciu 3 komunikaty A, B, C;
- po przyjściu komunikatu A czeka się na komunikat B, a następnie C.
Komunikaty przychodzące w niewłaściwym momencie maja być ignorowane!
Aby kod był bardziej elegancki możesz użyć proc transition (zdef poniżej) zamiast switch.
Zinterpretujmy powyższą maszynę stanową przyjmując, że obsługa A wygląda tak:
        # początek obsługi A
        coś robię ...
        czekam na B
        coś robię ...
        czekam na C
        coś robię ...
        # koniec obsługi A
Dla programisty jest naturalne umieszczenie kodu obslugi A w pojedynczej procedurze.
Odp. na pytanie czy jest to mozliwe w serwerze zdarzeniowym?
Co trzeba zrobić aby było możliwe??
## proc transition
# parametry:
# msg - komunikat
# cases - {msg1 cond1 code1 msg2 cond2 code2 ...}
# zasada dzialania:
# jesli przyszedl komunikat msg_i oraz cond_i jest spelnione
# to wykonuje sie code_i
proc transition {msg cases} {
foreach {msg2 cond code} $cases {
if {$msg2==$msg && [uplevel [list expr $cond]] } {
uplevel $code
break
}
}
}


UDP czyli prawdziwe komunikaty.

(niedokonczone !!!)



.... protokoły nad gniazdami BSD:

SSL/TLS czyli bezpieczne gniazdka.

SSL = Secure Socket Layer
TLS = Transport Layer Security
OpenSSL = implementacja SSL/TLS

Strona główna OpenSSL: http://www.openssl.org/
Definicja TLS i SSL: http://pl.wikipedia.org/wiki/TLS
Certyfikaty SSL: http://tldp.org/HOWTO/SSL-Certificates-HOWTO/index.html
   !!! tu jest dobry opis działania ssl - patrz rozdz 1.2 !!!

Pakiet Tls języka Tcl oraz przykłady: tcl_openssl.tar.gz

Skrócony opis pojęć SSL/TLS:

Co zapewnia SSL/TLS?


Mały CA: skrypt CA.pl

patrz uwagi w "security01b.tcl"
(dopuki nie pojawią się lepsze wskazówki można używać plików pem w tcl_openssl.tar.gz)


Przykład użycia pakietu Tls w języku Tcl:

Uwaga: dokumentacja pakietu tls jest w katalogu tls1.5
   a także tutaj: ActiveTcl/8.4/tls/tls.html
## uwierzytelnianie serwera --------------------------------------------
# - klient upewnia się, że serwer jest tym za kogo się podaje ...

lappend auto_path tls1.5
package require tls
# uwaga: jesli pakiet się nie ładuje to naprawdopodobniej
# brakuje bibliotek libcrypto.so.0.9.7 i libssl.so.0.9.7
# kopie tych bibl. znajduja sie w katalogu tls1.5
# wystarczy ustawic zmienna LD_LIBRARY_PATH:
# export LD_LIBRARY_PATH=...../tls1.5
# uwaga2: znajdujące się w tcl_openssl.tar.gz certyfikaty (cacert.pem i nowy-public.pem)
# sa wazne do stycznia 2010 !!!
# ... po tej dacie należy wygenerować nowe certyfikaty

## serwer
tls::socket -server obsluga \
-keyfile nowy-private.pem -certfile nowy-public.pem \
-password haslo \
10000
# -keyfile: klucz pryw serwera (moze byc zawarty w cert!)
# -certfile: cert serwera (zawiera klucz pub)
proc haslo {} {return "qwerty"}; # haslo do klucza pryw serwera
proc obsluga {s args} {
_puts "server socket $s"; # zakladam ze uruchamiamy to z konsola2c.tcl
tls::handshake $s
}

gets sock???

## klient
set s [tls::socket -require 1 -cafile cacert.pem localhost 10000]
# -require 1: klient żąda sprawdzenia certyfikatu serwera
# -cafile: certyfikat CA (chodzi m.in. o klucz publiczny CA w tym pliku
# dzięki któremu klient sprawdza podpis na certyfikacie serwera)
tls::handshake $s

puts $s "A ku ku !!!"; flush $s


## uwierzytelnianie serwera ORAZ klientow ---------------------------
# - serwer sprawdza swoich klientów
# - jak widac, nic nie szkodzi ze ser i kli uzywaje tego samego cert!

lappend auto_path tls1.5
package require tls

## serwer
proc haslo {} {return "qwerty"}
tls::socket -server obsluga -require 1 -cafile cacert.pem \
-password haslo -keyfile nowy-private.pem -certfile nowy-public.pem \
10000
proc obsluga {s args} {
_puts "server socket $s"
tls::handshake $s
}

# wyciąganie informacji o kliencie (z jego certyfikatu)
tls::status sock???

gets sock???

## klient
proc haslo {} {return "qwerty"}
set s [tls::socket -require 1 -cafile cacert.pem \
-password haslo -keyfile nowy-private.pem -certfile nowy-public.pem \
localhost 10000
]
tls::handshake $s

puts $s "A ku ku !!!"; flush $s


SASL czyli std. sposoby uwierzytelniania.

Pojęcia:
SASL to zbiór standardowych mechanizmów uwierzytelniania;
programy używające sasl mogą łatwo przełączyć się z jednego mechanizmu na inny;
bywa, że na początku serwer przedstawia klientowi listę obsługiwanych mechanizmów,
a klient wybiera jeden z nich i go używa...

Co robią programy używające SASL?
  1. po stronie klienta i serwera tworzy się kontekst sasl
  2. klient i serwer wykonują komende "step" tak długo aż zwróci ona 0
    (jeden z param. step to kontekst)
  3. przez param. "step" podaje się "response" przeciwnej strony;
    oczywiście trzeba to "response" przesyłać siecią do przeciwej strony!
  4. "response" serwera to tzw challange; na początku można podać pusty challange!
  5. jeśli step po stronie serwera generuje wyjątek tzn, że uwierzytelnianie się
    nie powiodło !!!
  6. sasl pobiera info o użytkownikach z procedur callback, które trzeba zdefiniować
    (po stronie ser i kli)
Język Tcl posiada pakiet SASL (tcl_sasl.tar.gz);
dokumentacja pakietu sasl: activetcl/8.4/tcllib/sasl/sasl.html

Przykład lokalnego użycia sasl w Tcl:
lappend auto_path ./pkg
# w podkatalogu ./pkg powinny sie znajdowac pakiety:
# md5, sasl, tls1.5 (kazdy we wlasnym katalogu!)
package re SASL

proc cliCallback {context command args} {
puts "dbg: [info level 0]"
switch -exact -- $command {
login { return "" }
username { return $::tcl_platform(user) }
password { return "moje haslo" }
realm { return "" }
hostname { return [info host] }
default { return -code error unxpected }
}
}
proc serCallback {context command args} {
puts "dbg: [info level 0]"
switch -exact -- $command {
login { return "" }
username { return $::tcl_platform(user) }
password { return "moje haslo" }
realm { return "" }
hostname { return [info host] }
default { return -code error unxpected }
}
}

SASL::mechanisms server
#% DIGEST-MD5 CRAM-MD5 LOGIN PLAIN ANONYMOUS
# mechanizmy dostepne po stronie serwera

set cli [SASL::new -mechanism PLAIN -callback cliCallback -type client]
set ser [SASL::new -mechanism PLAIN -callback serCallback -type server]
# tworzymy kontekst po klienta i serwera
# (w tym przykładzie w pojedynczym procesie!)

#SASL::reset $cli; SASL::reset $ser
# resetowanie kontekstów (nadają sie do powtórnego użycia)
#SASL::cleanup $cli; SASL::cleanup $ser
# usuwanie kontekstów

## co robi klient sasl? co robi serwer sasl?
# - kli i ser wykonują komendę "step" aż zwróci ona 0
# - klientowi przekazuje się "response" serwera (tzw challange)
# serwerowi przekazuje się "response" klienta
# (trzeba je przesyłać przez sieć!)
# - jeśli step na serwerze generuje błąd tzn, że
# uwierzytelnianie się nie powiodło!
# dlatego serwer powinien robic:
# set err [catch {SASL::step ...} ret]
# if {$err!=0} { wystąpił błąd; $ret zawiera opis błędu }
# if {$err==0} { błędu nie ma; $ret zawiera wynik komendy }

# cli: 1, 3, ...
SASL::step $cli [SASL::response $ser]
# komenda "SASL::response $ser" zwraca "response" serwera

# ser: 2, 4, ...
SASL::step $ser [SASL::response $cli]


## to samo co wyżej ale z pomocnymi wydrukami:
#
# cli: 1, 3, ...
set x [SASL::step $cli [SASL::response $ser]]
puts "cli/response: [SASL::response $cli]"
set x

# ser: 2, 4, ...
set x [SASL::step $ser [SASL::response $cli]]
puts "ser/response: [SASL::response $ser]"
set x

Zadanie T4
Zmodyfikuj czat tekstowy z zadania T1 tak aby:
- używał gniazdek bezpiecznych (bez uwierzytelniania przy pomocy certyfikatów)
- do uwierzytelniania użytkownika używał SASL/ mechanizmu CRAM-MD5
Do sprawozdania wstaw jedynie zmodyfikowane fragmenty kodu.