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:
- open/ close - otwieranie/ zamykanie pliku
open plik tryb; # zwraca kanał
- puts - zapis
puts [-nonewline] kanal dane
- gets/ read - odczyt linii tekstu/ odczyt ogólny
gets kanal zmienna; # czyta linie
tekstu
gets kanal
read kanał; # czyta wszystkie znaki, aż do końca pliku!
read kanał liczba_znaków
- fconfigure - konfigurowanie kanału
fconfigure kanal -opcja wartość
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:
- klucz asymetryczny -
para
kluczy; jeśli jednym z nich się zaszyfruje wiadomość,
to jedynie drugim można ją odszyfrować
- klucz publiczny -
- klucz prywatny - MUSI
być trzymany w ukryciu!
- klucz symetryczny -
szyfrowanie jest szybsze niż w przypadku klucza asymetrycznego
- podpis elektroniczny -
wiadomość zaszyfrowana kluczem prywatnym
- certyfikat SSL - dowód,
że dana osoba posiada dany klucz publiczny;
powinien być podpisany przez CA;
certyfikat zawiera klucz publiczny
- CA - "autorytet" podpisujący certyfikaty SSL
Co zapewnia SSL/TLS?
- uwierzytelnianie - sprawdzanie czy osoba jest tym za kogo się
podaje
(przykład uwierzytelniania: login+hasło)
ssl/tls umożliwiaja uwierzytelnianie klienta i/lub serwera
- szyfrowanie - nikt nie może zajrzeć do naszych wiadomości gdy
wędrują przez sieć
- zabezpieczenie przed złośliwymi modyfikacjami danych
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:
- uwierzytelnianie (ang. authentication) - sprawdzanie czy
użytkownik jest tym za kogo się podaje
(np sprawdzanie loginu i hasła)
- autoryzacja - sprawdzanie praw dostępu np do pliku
- SASL = Simple
Authentication and Security Layer; opisane w RFC2222
(wszystkie dokumenty RFC: http://www.ietf.org/iesg/)
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?
- po stronie klienta i serwera tworzy się kontekst sasl
- klient i serwer wykonują komende "step" tak długo aż zwróci ona 0
(jeden z param. step to kontekst)
- przez param. "step" podaje się "response" przeciwnej strony;
oczywiście trzeba to "response" przesyłać siecią do przeciwej strony!
- "response" serwera to tzw challange; na początku można podać
pusty challange!
- jeśli step po stronie serwera generuje wyjątek tzn, że
uwierzytelnianie się
nie powiodło !!!
- 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.