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
441716069.021.png 441716069.022.png 441716069.023.png 441716069.024.png 441716069.001.png 441716069.002.png
.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
441716069.003.png 441716069.004.png 441716069.005.png 441716069.006.png
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
441716069.007.png 441716069.008.png 441716069.009.png 441716069.010.png 441716069.011.png
 
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
441716069.012.png
 
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
441716069.013.png 441716069.014.png 441716069.015.png 441716069.016.png 441716069.017.png 441716069.018.png 441716069.019.png 441716069.020.png
 
Zgłoś jeśli naruszono regulamin