2006.07_Rozbudowa .NET Remoting cz. 2_[Programowanie .N].pdf
(
475 KB
)
Pobierz
441716069 UNPDF
Programowanie
.NET
Marcin Kawalerowicz
Rozbudowa
.NET Remoting cz. 2
podstawy komunikacji pomiędzy zdalny-
mi obiektami w ramach .NET Remoting.
Korzystając z dołączonych do .NET Framework
kanałów stworzyliśmy prosty choć w pełni funk-
cjonalny kalkulator liczb zespolonych. Nauczyliśmy
się jak skonigurować i zabezpieczyć komunikację.
Przyjrzeliśmy się dołączonym do środowiska for-
materom komunikacji. W tym artykule wejdziemy
o jeden stopień wyżej i przestaniemy biernie uży-
wać dostarczonych wraz z .NET Framework tech-
nologii. Rozwiniemy dostępne narzędzia i zbudu-
jemy własne elementy kanału komunikacyjnego.
Nie modyikując logiki kalkulatora rozszerzymy je-
go funkcjonalność o umiejętność protokołowania
przepływających informacji. W tym celu wykorzy-
stamy zgrabną i niezwykle funkcjonalną bibliotekę
pana Jarosława Kowalskiego NLog. Niejako przy
okazji przyjrzymy się bliżej możliwości asynchro-
nicznego przetwarzania komunikacji przez .NET
Remoting.
����������������
���������
�������������������������������������������
���������
������������������������������������������������
���������������
Rysunek 1.
Łańcuch operacji klienta.
odnajduje pierwszą z dowolnie długiego ciągu bra-
mek. Angielskie słowo
sink
oznaczające dosłownie
kuchenny zlewozmywak opisuje o wiele bardziej
obrazowo zastosowanie tego obiektu. Wyobraź-
my sobie kilka zlewów umieszczonych jeden nad
drugim. Strumień wiadomości niczym woda spły-
wa w duł do kolejnych naczyń. Ujścia zlewozmy-
waków nie zostały zabezpieczone korkiem, więc
płynna wiadomość swobodnie przedostaje się do
kolejnego, znajdującego się poniżej zlewu. Tak jak
mycie naczyń w kuchennym zlewozmywaku zmie-
nia strukturę wody (ściśle mówiąc jej czystość, ale
to akurat nie jest dobrą analogią), tak wiadomość
przechodząc przez bramkę zmienia swą postać.
Pierwszą, tworzoną domyślnie bramką w kolej-
ce jest
ClientContextTerminationSink
. Jej zdaniem
jest przekazanie utworzonej wiadomości do pierw-
szej zarejestrowanej przez programistę bramki.
Bramka taka musi między innymi implementować
interfejs
IMessageSink
, którego elementem jest wła-
ściwość
NextSink
, która deiniuje kolejny element
łańcucha. Tak utworzony ciąg może mieć dowolną
długość, ale jego ostatnim ogniwem musi być for-
mater (
IClientFormatterSink
). Jest on specjalnym
Duża łyżka teorii
Wykonanie postawionego wstępie zadania wyma-
ga głębokiego zanurzenia sie w technologię .NET
Remoting i dokładnego prześledzenia drogi jaką
pokonuje informacja. Rozpocznijmy od łatwiejszej
części jaką jest komunikacja w obrębie klienta.
Kolejne etapy przetwarzania wiadomości
przedstawione zostały na rysunku 1 w formie swe-
go rodzaju kolejki. Tak też należy rozpatrywać
ciąg operacji jakim poddawana jest informacja.
W pierwszym etapie klient komunikuje się z obiek-
tem przezroczystego pośrednika (z ang.
transpa-
rent proxy
). Pośrednik ten jest odpowiednikiem
obiektu zdalnego działającym w kontekście klien-
ta (proces tworzenia pośrednika przedstawio-
ny został w pierwszej części artykułu). Obiekt ten
nie będąc w stanie stworzyć właściwej wiadomo-
ści, komunikuje się z obiektem typu
RemotingProxy
,
który odpowiedzialny jest za utworzenie pierwszej
wersji wiadomości. Ten z kolei będąc potomkiem
RealProxy
posługując się właściwością
Identyty
Listing 1.
Koniguracja łańcucha bramek
<
channels
>
<
channel ref=
"http"
>
<
clientProviders
>
<
provider type=
"Commons.NLogMessageSinkProvi
der,Commons"
/
>
<
formatter ref=
"soap"
/
>
<
provider type=
"Commons.NLogStreamSinkProvid
er,Commons"
/
>
Autor jest absolwentem informatyki Politechniki Opol-
skiej. Obecnie pracuje jako programista .NET w mona-
chijskiej irmie Trygon Softwareberatung. Zainteresowa-
nia zawodowe autora obejmują szeroko pojęte progra-
mowanie. Prywatnie miłośnik dobrego kina i równie do-
brej fotograii (szczególnie własnej).
Kontakt z autorem:
mkawalerowicz@poczta.onet.pl
<
/clientProviders
>
<
/channel
>
<
/channels
>
58
www.sdjournal.org Software Developer’s Journal 7/2006
W
poprzednim numerze SDJ poznaliśmy
.NET Remoting
Listing 2.
Dostawca bramki
Listing 4.
Przetworzenie strumienia na ciąg znaków
public
class
NLogStreamSinkProvider
:
IClientChannelSinkProvider
{
IClientChannelSinkProvider
m_next
;
public
NLogStreamSinkProvider
(
IDictionary
properties
,
ICollection
providerData
)
{
}
IClientChannelSink
IclientChannelSinkProvider
.
CreateSink
(
IChannelSender
channel
,
string
url
,
object
remoteChannelData
)
{
IClientChannelSink
next
=
m_next
.
CreateSink
(
channel
,
url
,
remoteChannelData
);
return
new
NLogStremSink
(
next
);
}
IClientChannelSinkProvider
IClientChannelSinkProvider
.
Next
{
get
{
return
m_next
;
}
set
{
m_next
=
value
;
}
}
}
private
string
GetString
(
ref
System
.
IO
.
Stream
stream
)
{
System
.
IO
.
MemoryStream
memStream
=
new
System
.
IO
.
MemoryStream
();
byte
[]
line
=
new
byte
[
128
];
int
count
=
stream
.
Read
(
line
, 0, 128
);
System
.
Text
.
StringBuilder
strStream
=
new
StringBuilder
();
while
(
count
>
0
)
{
memStream
.
Write
(
line
, 0,
count
);
strStream
.
Append
(
System
.
Text
.
Encoding
.
ASCII
.
GetString
(
line
, 0,
count
));
count
=
stream
.
Read
(
line
, 0, 128
);
}
if
(
memStream
.
CanSeek
)
memStream
.
Seek
(
0,
v
System
.
IO
.
SeekOrigin
.
Begin
);
stream
=
memStream
;
return
strStream
.
ToString
();
}
rodzajem bramki, który potrai przetworzyć wiadomość na
strumień danych (zserializować go). Po tym obowiązkowym
elemencie kolejki otwiera się dla programisty kolejna możli-
wość ingerencji w przetwarzanie danych. Implementując in-
terfejs
IClientChanellSink
jest on w stanie przetworzyć czy-
sty strumień danych. Ostatnia w kolejce bramka przekazu-
je wiadomość do medium komunikacyjnego. W ten sposób
dane po odpowiednim przetworzeniu wyruszają w podróż
do serwera.
ści artykułu kalkulator liczb zespolonych, a ściśle rzecz biorąc
jego wersję wykorzystująca protokół HTTP. Klient i serwer bę-
dą aplikacjami konsolowymi, które skonigurujemy za pomo-
cą plików XML.
Tworzoną przez nas bramkę będziemy mogli z powo-
dzeniem wykorzystać w wielu projektach, tak więc najwła-
ściwszym miejscem dla niej będzie współdzielona bibliote-
ka
Commons
. Jak pamiętamy biblioteka ta wykorzystywana
jest zarówno przez klienta jak i serwer. Zawiera ona mię-
dzy innymi interfejs deiniujący metody dostępne w kalku-
latorze.
Stworzona przez nas bramka będzie protokołować
wszelkie przesyłane wiadomości. Co ważne naszym celem
będzie logowanie wiadomości przed i po zserializowaniu.
Chcąc tego dokonać musimy zajrzeć do łańcucha bramek
w dwóch miejscach. Po raz pierwszy implementując inter-
fejs
IMessageSink
i ustawiając nową bramkę tuż przed for-
materem SOAP. Dzięki zastosowaniu formatu SOAP logo-
wany potok będzie miał formę czytelną dla człowieka. Po
raz drugi włączymy się do łańcucha przetwarzania po wyj-
ściu z formatera. W tym przypadku zaglądniemy do wnę-
trza potoku w związku z tym będziemy musieli zaimple-
mentować interfejs
IClientChannelSink
. W obu przypadkach
będziemy musieli zaopatrzyć nasze bramki w tak zwanych
dostawców (ang.
Provider
). Klasa ta jest odpowiedzial-
na za dostarczenie instancji bramki. Wszystkie deiniowa-
ne w artykule bramki wywiedziemy do wspólnego przodka
BaseChannelSinkWithProperties
, dostarczającego pewne
niezbędne właściwości i metody.
Włączenie bramki w łańcuch przetwarzania będzie się
wiązać ze zmianą koniguracji klienta na przedstawioną na Li-
stingu 1.
Kod
NLogStreamSinkProvider
przedstawiony został na Li-
stingu 2. Klasa dostarczyciela bramki musi implementować in-
terfejs
IClientChannelSinkProvider
. Metoda
CreateSink()
tego
interfejsu dostarcza obiektu typu
IClientChannelSink
. W związ-
Przygotowanie bramki
Znając już schemat przetwarzania wiadomości przez klienta
możemy pokusić się o napisanie własnego elementu kanału,
który wykona zaplanowane na wstępie działanie. Jako mate-
riał do eksperymentów wybierzemy opisany w pierwszej czę-
Listing 3.
Koniguracja NLog
<
conigSections
>
<
section name=
"nlog"
type=
"NLog.Conig.ConigSectionHandler, NLog"
/
>
<
/conigSections
>
<
nlog
>
<
targets
>
<
target name=
"console"
type=
"Console"
/
>
<
target name=
"log"
type=
"File"
ilename=
"client.log"
/
>
<
/targets
>
<
rules
>
<
logger name=
"*"
level=
"Info"
writeTo=
"log"
/
>
<
logger name=
"*"
levels=
"Debug,Warn,Error"
writeTo=
"console"
/
>
<
/rules
>
<
/nlog
>
Software Developer’s Journal 7/2006
www.sdjournal.org
59
Programowanie
.NET
Listing 5.
Implementacja metod interfejsu
IClientChannelSink
Jeszcze jedna łyżka teorii
public
void
AsyncProcessRequest
(
IClientChannelSinkStack
sinkStack
,
IMessage
msg
,
ITransportHeaders
headers
,
System
.
IO
.
Stream
stream
)
{
logger
.
Info
(
GetString
(
ref
stream
));
sinkStack
.
Push
(
this
,
null
);
m_nextSink
.
AsyncProcessRequest
(
sinkStack
,
msg
,
headers
,
stream
);
}
public
void
AsyncProcessResponse
(
IClientResponseChannelSinkStack
sinkStack
,
object
state
,
ITransportHeaders
headers
,
System
.
IO
.
Stream
stream
)
{
logger
.
Info
(
GetString
(
ref
stream
));
sinkStack
.
AsyncProcessResponse
(
headers
,
stream
);
}
public
Stream
GetRequestStream
(
IMessage
msg
,
ITransportHeaders
headers
)
{
return
m_nextSink
.
GetRequestStream
(
msg
,
headers
);
}
public
IClientChannelSink
NextChannelSink
{
get
{
return
m_nextSink
;
}
}
public
void
ProcessMessage
(
IMessage
msg
,
ITransportHeaders
requestHeaders
,
System
.
IO
.
Stream
requestStream
,
out
ITransportHeaders
responseHeaders
,
out
System
.
IO
.
Stream
responseStream
)
{
logger
.
Info
(
GetString
(
ref
requestStream
));
m_nextSink
.
ProcessMessage
(
msg
,
requestHeaders
,
requestStream
,
out
responseHeaders
,
out
responseStream
);
logger
.
Info
(
GetString
(
ref
responseStream
));
}
Bramka po stronie klienta jest gotowa. Można już jej z powodze-
niem używać w wielu projektach. Nie modyikując bowiem przesy-
łanej informacji jest przezroczysta dla komunikacji pomiędzy serwe-
rem a klientom.
Przetwarzanie wiadomości przez serwer jest w dużej części
analogiczne do tego co dzieje się w przypadku klienta. Po tej stro-
nie również istnieją bramki przetwarzające wiadomości (imple-
mentujące
IMessageSink
) oraz bramki przetwarzające strumień
danych (implementujące analogiczny do
IClientChannelSink
in-
terfejs
IServerChannelSink
). Podstawową różnicą w przetwa-
rzaniu serwerowym jest sposób tworzenia bramek. W przypad-
ku klienta bramki tworzone są dopiero wtedy gdy zostaje zgłoszo-
ne zapotrzebowanie na referencję obiektu zdalnego. Na serwerze
bramki tworzone są wraz z rejestracją całego kanału komunika-
cyjnego. Rysunek 2 przedstawia łańcuch przetwarzania danych
w kontekście serwera.
niujemy dwa poziomy logowania.
Info
będzie zapisywał wia-
domości do pliku tekstowego a pozostałe poziomy będą wypi-
sywać informacje w oknie konsoli tekstowej. Przed skoniguro-
waniem ustawiamy referencje do biblioteki
NLog.dll
w projek-
cie
Commons
. Kod XML przedstawiony na Listingu 1 dodaje-
my do pliku koniguracyjnego klienta.
Przygotowanie NLog do działania sprowadza się do zde-
iniowania tak zwanego
Loggera
poprzez wywołanie statycz-
nej metody
LogManager.GetLogger()
z nazwą
Loggera
jako pa-
rametrem:
Logger logger = LogManager.GetLogger("NLogSink");
Tak przygotowaego Loggera możemy używać w następują-
cy sposób:
logger.Info("Wpis do pliku client.log");
logger.Debug("Informacja do wyświetlenia na ekranie");
Tworzenie bramki po stronie klienta
Wszystkie części składowe potrzebne do poprawnego wpro-
wadzenia bramki do ciągu przetwarzania są już gotowe. Wie-
my również jak wykonamy postawione na wstępie zadanie
protokołowania informacji. Pora przystąpić do implementacji
samej bramki.
Listing 6.
Przetworzenie wiadomości na ciąg znaków
ku z tym bramki zajmujące się jedynie wiadomością (
IMessage-
Sink
) muszą również implementować ten interfejs, a dostar-
czyciele tych bramek wyglądają prawie identycznie.
Zanim zaimplementujemy właściwą bramkę przyjrzyjmy
się pakietowi NLog.
private
string
GetString
(
IMessage
msg
)
{
StringBuilder
stringMsg
=
new
StringBuilder
();
IDictionaryEnumerator
enumer
=
msg
.
Properties
.
GetEnumerator
();
while
(
enumer
.
MoveNext
())
{
stringMsg
.
Append
(
enumer
.
Key
+
" --- "
+
enumer
.
Value
+
"
\r\n
"
);
}
return
stringMsg
.
ToString
();
}
NLog
NLog to bardzo przydatne narzędzie do tworzenia różnego ro-
dzaju protokołów. Potrai ono zapisywać informacje w plikach
tekstowych, bazach danych czy też przesyłać je pocztą elek-
troniczną. NLog może pracować na kliku „poziomach” z któ-
rych każdy może być logowany w innym miejscu. My zdei-
60
www.sdjournal.org Software Developer’s Journal 7/2006
Tworzymy nową klasę o nazwie
NLogStreamSink
. Przed
przystąpieniem do implementacji niezbędnych interfejsów
musimy przygotować naszą bramkę do działania. Po pierw-
sze, by nie rozerwać łańcucha przetwarzania musimy zade-
klarować następującą po niej bramkę oraz utworzyć konstruk-
tor, który będzie bramkę tą ustawiał.
private IClientChannelSink m_nextSink;
public NLogStremSink(IClientChannelSink nextSink)
{ m_nextSink = nextSink; }
Nie zapomnijmy też zainicjować
Loggera
:
private Logger logger = LogManager.GetLogger("NLogStremSink");
Jako pierwszą zaimplementujemy część odpowiedzialną za
logowanie informacji po przetworzeniu przez formater. Infor-
macja w tym stadium traci swoją strukturę i staje się strumie-
niem danych. W związku z tym musimy strumień ten prze-
chwycić i zamienić na ciąg znaków. Sztuki tej dokonuje po-
mocnicza metoda przedstawiona na Listingu 4.
Stwórzmy klasę
NLogStremSink
Zaimplementujmy interfejs
IClientChannelSink
zgodnie z Listingiem 5.
Na początek przyjrzyjmy się bliżej metodzie
Process-
Message()
. Metoda ta wykonuje wyznaczone przez nas zada-
nie. Przed przesłaniem wiadomości dalej, do kolejnej bramki
łańcucha (
m _ nextSink
) zapisujemy do pliku strumień wejścio-
wy. Zaraz po powrocie wiadomości z
m _ nextSink.Process-
Message()
zapisujemy do pliku strumień wyjściowy. Voila! Za-
danie wykonane.
Pełne zrozumienie przedstawionego na Listingu 3 ko-
du wymaga wyjaśnienia w jaki sposób .NET Remoting może
przetwarzać wiadomości. Do tej pory zajmowaliśmy się wy-
łącznie komunikacją synchroniczną: wiadomość została wy-
słana, a nasz program oczekiwał na jej przetworzenie i od-
powiedź. Bez większych problemów możemy tak przygoto-
wać komunikację, by odbywała sie asynchronicznie. Nale-
ży zaznaczyć, że komunikacja asynchroniczna jest częścią
składową wszystkich dostarczonych wraz z .NET Remoting
kanałów. Nasza implementacja bramki logującej wpisuje się
po prostu w standardy narzucone przez .NET Framework.
Przygotowując bramkę do działania asynchroniczne-
go musimy przed przekazaniem przetwarzania do kolejne-
go elementu łańcucha umieścić naszą bramkę na pewne-
go rodzaju stosie. Stos ten przechowuje wszystkie bram-
ki, które mają zostać poinformowane o nadejściu odpowie-
dzi. Bramkę można umieścić na stosie wywołując metodę
sinkStack.Push(this, null)
. Po tej operacji należy przeka-
zać przetworzenie dalej
m _ nextSink.AsyncProcessRequest()
.
.NET Framework posługując się przygotowanym stosem
sam zatroszczy się o wywołanie metody
AsyncProcess-
Response()
kiedy nadejdzie odpowiedź.
Protokołowanie wiadomości
Uporaliśmy się z logowaniem strumieni wyjściowych i wej-
ściowych. Spróbujmy teraz zapisać do pliku szczegóły na
temat wiadomości. Podobnie jak w przypadku strumienia
stworzymy metodę która przetworzy nasza wiadomość na
ciąg znaków zdatny do zapisania do pliku. Zadanie to reali-
zuje kod przedstawiony na Listingu 6.
Software Developer’s Journal 7/2006
Programowanie
.NET
�������������������
Listing 8.
Przetwarzanie wiadomości zwrotnej w
komunikacji asynchronicznej.
��������������
public
class
MessageReplySink
:
IMessageSink
{
// deklaracja loggera
// deklaracja następnej bramki
//konstruktor bramki
// pusta implementacja AsyncProcessMessage
// właściwość NextSink
public
IMessage
SyncProcessMessage
(
IMessage
msg
)
{
logger
.
Info
(
GetString
(
msg
));
return
m_nextSink
.
SyncProcessMessage
(
msg
);
}
}
������������������������������������������������
��������������������������������������
������������������������������������������
��������������������������������������������
��������������������������
����������������
Rysunek 2.
Ciąg operacji serwera (HTTP)
Interfejs
IMessageSink
zaimplementujemy zgodne z Li-
stingiem 7. Tak jak w przypadku logowani strumienia imple-
mentacja dla komunikacji synchronicznej w metodzie
Sync-
ProcessMessage()
jest dość oczywista. Wiadomość jest kon-
wertowana na ciąg znaków i zapisywana w pliku, po czym
na rzecz następnej bramki
m _ nextSink as IMessageSink
wy-
woływana jest metoda
SyncProcessMessage()
, która zwraca
odpowiedź. Zwrotna wiadomość jest w końcu protokołowa-
na za pomocą NLog.
Dokładniejszego wyjaśnienia wymaga natomiast imple-
mentacja asynchronicznego wykonania łańcucha. W tym
przypadku konieczne jest jego przerwanie oraz dołączenie
dodatkowego „oczka”, które będzie w stanie przetworzyć od-
powiedź. Dodatkowym elementem jest jeszcze jedna bramka,
która wprowadzona w łańcuch wyłapie odpowiedź w metodzie
SyncProcessMessage()
i przekaże ja z powrotem do następne-
go elementu. Implementacja dodatkowej bramki przedstawio-
na została na Listingu 8.
Informacja kończąc swą podróż poprzez medium komu-
nikacyjne traia do bramki transportowej serwera. Jeśli ko-
munikacja odbywa się za pomocą protokołu HTTP kolejna
bramka może stworzyć dla danego obiektu jego opis w po-
staci WSDL (dzieje się tak jeśli zapytanie kończy się cią-
giem znaków
?wsdl
lub
?sdl
). W kolejnym kroku następują
działania zdeiniowane w opcjonalnych bramkach przetwa-
rzających strumień danych. Strumień danych jest w końcu
deserializowany najpierw do formatu SOAP, a później do
postaci binarnej (dla komunikacji HTTP). Zdeserializowa-
na wiadomość może być teraz przetworzona przez opcjo-
nalne bramki wiadomości, by w końcu traić na serię kana-
łów, które wykonują właściwą pracę. Decydują czy istnie-
je konieczność utworzenia obiektu zdalnego, sprawdzają
czy czas życia obiektu zdalnego przypadkiem już nie upły-
nął, decydują czy możliwe jest wywołanie określonej meto-
Listing 7.
Implementacja IMessageSink
public
IMessageCtrl
AsyncProcessMessage
(
IMessage
msg
,
IMessageSink
replySink
)
{
IMessageSink
messageReplySink
=
new
MessageReplySink
(
replySink
);
logger
.
Info
(
GetString
(
msg
));
return
(
m_nextSink
as
IMessageSink
)
.
AsyncProcessMessage
(
msg
,
messageReplySink
);
}
public
IMessageSink
NextSink
{
get
{
return
m_nextSink
as
IMessageSink
;
}
}
Klient asynchroniczny
Asynchroniczne wywołanie metody po stronie klienta wiąże się
z wykorzystaniem delegatów. Jeśli nie pracowałeś jeszcze z dele-
gatami to na potrzeby tego artykułu powinieneś wiedzieć jedynie, że
są to obiekty, za pomocą których można wywołać pewne metody.
Delegaty będąc referencją do metody o określonej sygnaturze mo-
gą być wykorzystane do asynchronicznego jej wykonania. Na rzecz
delegata można wywołać metodę
BeginInvoke()
. Po jej wywołaniu
sterownie zostanie od razu zwrócone do programu. Wynik działania
można odzyskać poprzez wywołanie
EndInvoke()
. Implementacja
asynchronicznego klienta może wyglądać więc następująco:
public class ComplexNumbersAsyncClient
{
delegate double ModulusDelegate(ComplexNumber cn);
public static void Main(string[] args)
{
// inicjalizacja zdalnego obiektu kalkulatora: calculator
// tworzenie obiektu liczby zespolonej: cn
Modulus mod = new Modulus(calculator.Modulus);
IAsyncResult ar = mod.BeginInvoke(cn, null, null);
double d = mod.EndInvoke(ar);
}
}
public
IMessage
SyncProcessMessage
(
IMessage
msg
)
{
logger
.
Info
(
GetString
(
msg
));
IMessage
msgResult
=
(
m_nextSink
as
IMessageSink
)
.
SyncProcessMessage
(
msg
);
logger
.
Info
(
GetString
(
msgResult
));
return
msgResult
;
}
62
www.sdjournal.org Software Developer’s Journal 7/2006
Plik z chomika:
Kapy97
Inne pliki z tego folderu:
2007.05_Silnik reguł biznesowych Windows Workflow Foundation w praktyce_[Programowanie .N].pdf
(711 KB)
2006.07_Rozbudowa .NET Remoting cz. 2_[Programowanie .N].pdf
(475 KB)
2006.05_.NET Remoting cz. 1._[Programowanie .N].pdf
(417 KB)
2006.05_Microsoft Office v. 12 _[Programowanie .N].pdf
(684 KB)
2006.05_AJAX w ASP.NET_[Programowanie .N].pdf
(436 KB)
Inne foldery tego chomika:
Algorytmy
Antyhaking
Aplikacje Biznesowe
Aspekty
Bazy Danych
Zgłoś jeśli
naruszono regulamin