r10-05, Programowanie, ! Java, Java Server Programming


W niniejszym rozdziale

Rozdział 10.

Komunikacja aplet-serwlet

Niniejszy rozdział przedstawia kilka technik, przy pomocy których aplety mogą komunikować się z serwletami. Zaprezentowane zostanie nieco inne podejście, niż to, jakiego można by się spodziewać. Zamiast przyjmować, że istnieje aplet i serwlet, które muszą się ze sobą porozumieć, przyjęto, że istnieje aplet, który musi porozumieć się z pewnym elementem serwera, i opisane zostanie, dlaczego czasami elementem tym powinien być serwlet.

Aby rozpocząć, proszę pomyśleć o apletach, które muszą porozumieć się z serwerem. Istnieje kilka dobrych przykładów. Proszę spojrzeć na aplet administracyjny, który działa na serwerze Java Web Server. Proszę pomyśleć, jak on działa — jest wykonywany na kliencie, ale konfiguruje serwer. Aby to uczynić, aplet i serwer muszą być w niemal stałym kontakcie. Inny przykład, proszę spojrzeć na jeden z popularnych apletów pogawędek. Jeden klient coś mówi, a cała reszta to widzi. Jak to działa? Na pewno aplety nie komunikują się ze sobą. Zamiast tego, każdy aplet wysyła swoje wiadomości do centralnego serwera, a serwer zajmuje się uaktualnieniem informacji innych klientów. W końcu, proszę sobie wyobrazić aplet, który śledzi cenę zbioru akcji i dokonuje ciągłego uaktualniania. Jak aplet poznaje aktualne ceny i, co ważniejsze, skąd wie, kiedy się one zmieniają? Odpowiedzią jest, że komunikuje się on z serwerem.

Opcje komunikacji

Zainteresowanie handlem akcjami wzrasta razem z indeksami giełdowymi, więc kontynuowany będzie hipotetyczny aplet, odpowiadający za akcje. Należy ostrzec, że aplet ten pozostanie hipotetyczny. Zostanie on wykorzystany jedynie jako punkt odniesienia w dyskusji nad kwestiami należącymi do komunikacji aplet-serwlet. Ale proszę się nie martwić, w dalszej części tego rozdziału występuje duża ilość kodu przedstawiająca techniki tu opisane, jedynie w nieco prostszych przykładach.

Ten aplet akcji musi pobierać ceny z jakiegoś serwera. Przyjmując, że jest to zwykły, niegodny zaufania aplet, jest tyko jedno wyjście — z komputera, z którego został pobrany. Każda próba połączenia się z innym komputerem daje wynik w SecurityException, tak więc należy przyjąć, że aplet pobiera ceny z serwera, z którego został pobrany. Pytanie pozostaje — jak aplet i serwer mogą się ze sobą porozumiewać?

Aplety godne i niegodne zaufania

Kiedy aplet Javy zostaje osadzony w stronie WWW, przeglądarka może pobrać go i wykonać automatycznie. Jeżeli się nad tym zastanowić, jest to bardzo niebezpieczna sprawa. Tak więc, aby chronić klienta, JDK 1.0 przyjmował, że wszystkie aplety są niegodne zaufania i uruchamiał je pod kontrolą SecurityManager, który w poważny sposób ograniczał ich funkcjonalność. Na przykład, menedżer bezpieczeństwa zapewniał niemożność apletów do zapisywania informacji w systemie plików użytkownika, odczytywania pewnych właściwości systemu, przyjmowania przychodzących połączeń przez porty, czy też uruchamiania połączeń wychodzących z każdym komputerem, który nie był serwerem macierzystym. Własności te zabezpieczały klienta, ale ograniczały użyteczność apletów.

Konsekwentnie, JDK 1.1 wprowadził koncepcję apletów godnych zaufania — apletów, które mogły działać, jak zwykłe aplikacje z pełnym dostępem do komputera klienta. Aby aplet został uznany za godny zaufania, musi zostać elektronicznie podpisany przez osobę z firmy, której ufa klient (zaznaczonej w przeglądarce użytkownika). Podpis uwierzytelnia pochodzenie apletu i gwarantuje spójność podczas transferu tak, aby klient wiedział, że kod apletu nie został złośliwie zmieniony. Pozwoliło to na tworzenie bardziej użytecznych apletów, ale było podejściem typu wszystko albo nic.

Aby dać klientowi więcej kontroli, JDK 1.2 wprowadził złożony system kontroli dostępu. W tym nowym systemie, podpisany elektronicznie aplet może być częściowo uznany za godny zaufania i może on otrzymać pewne możliwości bez uzyskania pełnej kontroli nad systemem. Pozwala to na przyznawanie apletom nieznanego pochodzenia niewielkich przywilejów (takich jak zapis w jednym, konkretnym katalogu), bez dostarczania im możliwości przeglądania twardego dysku klienta. Producenci przeglądarek powoli wprowadzają obsługę nowych wersji JDK, ale na szczęście możliwe jest uaktualnienie JVM przeglądarki przy pomocy modułu rozszerzającego Java Plug-in, bezpłatnego produktu dostępnego pod adresem http://java.sun.com/products/plugin.

Połączenia przez HTTP i zwykłe porty

Przed stworzeniem JDK 1.1 i serwletów istniały dwie możliwości komunikacji aplet-serwer:

Każde z tych podejść posiada wady i zalety. Połączenie HTTP apletu z programem CGI działa dobrze z następujących powodów:

Jednak połączenie HTTP z programem CGI posiada również pewne wady:

Aplet i serwlet mogą się również porozumiewać poprzez połączenie zwykłym portem apletu z procesem serwera nie-HTTP. Podejście do posiada następujące zalety w stosunku do podejścia opartego na HTTP:

Lecz połączenie przez zwykły port posiada również wady w stosunku do podejścia opartego na HTTP:

Standardowe historyczne podejście wymagało od apletów porozumiewania się przy pomocy HTTP z programami CGI na serwerze. Jest to łatwe oraz działa we wszystkich typach przeglądarek, nawet tych pracujących za firewallami. Wykorzystanie połączenia przez zwykły port zostało generalnie zarezerwowane dla sytuacji, w której jest to absolutnie konieczne, takich jak ta, w której aplet i serwer wymagają komunikacji dwukierunkowej. A także, nawet w tym wypadku, często możliwe jest wykorzystanie połączeń HTTP w celu symulacji komunikacji dwukierunkowej, co ma służyć przejściu przez firewalle, jak przedstawione to będzie w późniejszym przykładzie.

Serwlety i serializacja obiektu

Niedawne wprowadzenie serwletów Javy i serializacji obiektów tchnęło nowe życie w te tradycyjne techniki komunikacji aplet-serwer. Serwlety zastępują wolno uruchamiające się programy CGI, poprawiając wydajność komunikacji aplet-serwer opartej na HTTP i czyniąc częste połączenia aplet-serwer możliwymi. Chociaż ogólnie rzecz biorąc prawdą jest, że aplet i serwlet ciągle wymagają czasu do ponownego otworzenia połączenia dla każdego żądania i odpowiedzi, aplet nie musi już dłużej czekać, aż serwer uruchomi program CGI w celu obsługi każdego z powtarzających się żądań.

Serializacja obiektów Javy uprościła kwestie związane z formatowaniem odpowiedzi. W związku z tym, że zarówno aplety, jak i serwlety napisane są w Javie, naturalnym jest, że powinny się one komunikować poprzez wymianę obiektów Javy. Na przykład, kiedy hipotetyczny aplet akcji pyta odpowiedni serwlet o najwyższy dzienny kurs akcji Suna, może otrzymać CenaAkcji w formie zserializowanego obiektu. Z niego można pobrać najwyższą wartość jako float oraz czas tej wartości jako Czas. Jest to spójne i dostarcza prostego zabezpieczenia typów. Proszę jednak pamiętać, że serializacja obiektów działa jedynie z apletami pracującymi wewnątrz przeglądarek obsługujących JDK 1.1 i późniejszych.

JDBC, RMI i CORBA

JDK 1.1 zawiera dwie dodatkowe własności wywierające wpływ na komunikację aplet-serwlet — JDBC i RMI. API JDBC (Java Database Connectivity — Łączność z Bazami Danych Javy), omówiony w rozdziale 9, „Łączność z bazami danych”, pozwala na połączenie się z relacyjną bazą danych na tym samym lub innym komputerze. Aplety Javy napisane od wersji JDK 1.1 mogą wykorzystywać JDBC do komunikowania się z bazą danych na serwerze. Ta przeznaczona do specjalnych celów komunikacja generalnie nie wymaga komunikacji aplet-serwlet. Jednak często okazuje się pomocne, aby aplet (szczególnie napisany dla JDK 1.0) nie łączył się bezpośrednio z bazą danych (lub z proxy ba serwerze WWW) a zamiast tego łączył się z serwletem obsługującym łączność z bazą danych zamiast apletu, jak opisano w ramce „Serwlety pośredniczące” w rozdziale 9, „Łączność z bazami danych”. Na przykład, aplet który chce wyszukać adres danej osoby może połączyć się z serwletem przy pomocy HTTP, przekazać nazwisko tej osoby wykorzystując parametry HTTP, po czym otrzymać adres jako specjalnie sformatowany łańcuch lub obiekt zserializowany. To zastosowanie komunikacji aplet-serwlet opiera się w znacznym stopniu na istniejących protokołach takich jak HTTP, tak więc nie zostanie tu szerzej opisane.

API RMI (Remote Method Invocation — Zdalne Wywoływanie Metod) pozwala apletowi na wywołanie metod obiektu Javy uruchomionego na serwerze oraz, w pewnych wypadkach, pozwala również obiektowi na serwerze na wywoływanie metod apletu. Zalety wykorzystania RMI w komunikacji aplet-serwer są bardzo znaczące:

Nie odbywa się to jednak bez pewnych kosztów. Wykorzystanie HTTP ma wpływ na wydajność, a paradygmat żądanie-odpowiedź HTTP nie obsługuje odwołań wstecznych. Wady RMI są równie zajmujące:

Większa ilość informacji na temat programowania RMI dostępna jest w książkach „Java Network Programming” autorstwa Elliotte Rusty Harold (O'Reilly) i „Java Distributed Computing” autorstwa Jima Farleya (O'Reilly).

CORBA (Common Object Request Broker Architecture — Wspólna Architektura Żądań Obiektów) to technologia podobna do RMI, która pozwala na komunikację pomiędzy obiektami rozproszonymi napisanymi w różnych językach. Przy pomocy CORBA'y i jej protokołu komunikacyjnego IIOP (Internet Inter-ORB Protocol), klient C++ może porozumiewać się z serwletem Javy. Przedstawienie tej techniki przekracza zakres niniejszej książki. Większa ilość informacji dostępna jest pod adresami http://www.omg.org i http://java.sun.com/products/jdk/idl.

Podejście hybrydowe

Teraz, kiedy przeanalizowane zostały już wszystkie opcje, pytanie pozostaje: jak przykładowy aplet akcji powinien porozumiewać się ze swoim serwerem cen akcji? Odpowiedź brzmi: to zależy.

Jeżeli można zagwarantować, że wszyscy potencjalni klienci posiadają jego obsługę, elegancja i moc RMI czynią go idealnym wyborem. Brzmi to jednak tak, jak przyjmowanie, że wszyscy znajomi uwielbiają dowcipy na temat Star Treka. Może to być prawda przy dokładnym wybieraniu przyjaciół (lub klientów), lecz generalnie nie zdarza się to w prawdziwym świecie.

Kiedy RMI nie jest dostępny, dwukierunkowe możliwości połączenia przez port nie-HTTP sprawiają, że wygląda ono atrakcyjnie. Niestety, ta dwukierunkowa komunikacja staje się nieistniejącą komunikacją, kiedy aplet kończy się na firewallu.

Zawsze jest stary sposób, komunikacja HTTP. Jest ono proste w implementacji i pracuje na każdym kliencie z obsługą Javy. A jeżeli można zagwarantować, że klient obsługuje JDK 1.1 (jest to łatwiejsze do zagwarantowania, niż obsługa RMI przez klientów), można wykorzystać serializację obiektów.

Przypuszczalnie najlepszym rozwiązaniem jest wykorzystanie wszystkich rozwiązań. Java sprawia, że możliwe jest połączenie technik komunikacji aplet-serwlet HTTP, nie-HTTP i RMI, umieszczając obsługę ich wszystkich w jednej aplikacji. Dlaczego ktoś miałby chcieć to zrobić? Dlatego, że jest to poręczna technika, kiedy aplet chce porozumieć się przy pomocy RMI lub protokołu nie-HTTP, lecz musi przełączyć się na HTTP, kiedy okaże się to potrzebne (w przypadkach takich jak znalezienie się przed firewallem). Poprzez zastosowanie tego samego serwera do obsługi wielu klientów, podstawowa logika serwera i stan serwera mogą być zebrane w jednym miejscu. Kiedy środowisko jest pod kontrolą można usunąć jeden lub więcej z tych protokołów. Lecz czy nie jest miło wiedzieć, że nie jest to konieczne?

Dla skomplikowanych aplikacji pracujących na serwerze aplikacji standardowym projektem jest udostępnienie zdalnego obiektu RMI klientom RMI, urządzenia słuchającego na portach klientom portów a serwletu klientom HTTP. Obiekty te wykorzystują wspólnie zbiór logicznych klas biznesowych w celu obsługi żądań klienta (podobnie jak w restauracji z wynosem można zamawiać poprzez telefon, faks lub pocztę elektroniczną). Jednak w pozostałej części niniejszego rozdziału zostanie to nieco uproszczone, i przedstawiona zostanie komunikacja RMI, przez porty i HTTP obsługiwana przez pojedynczy serwlet. Jeden serwlet, wiele protokołów dostępu.

Serwer godziny

W celu prostego przedstawienia każdej z technik komunikacji, napisany zostanie aplet proszący serwer o aktualną datę i godzinę. Aplet na początku wykorzystuje połączenie HTTP, później połączenie przez port nie-HTTP, a w końcu połączenie RMI. Oczywiście, aplet może normalnie pobierać obecny czas z systemu, na którym pracuje. Aby nadać temu przykładowi cień praktyczności, można przyjąć, że aplet potrzebuje dokładnego znacznika czasu dla jakiegoś zdarzenia i nie może polegać na tym, że komputer klienta posiada prawidłowo ustawiony zegar.

Aplet

W całym niniejszym podrozdziale wykorzystane będzie ten sam aplet. Szkielet kodu tego apletu, ApletGodziny jest przedstawiony na przykładzie 10.1. W tym momencie aplet ten po prostu tworzy interfejs użytkownika, na którym wyświetlane będą pobrane przez niego czasy, jak przedstawiono na rysunku 10-1. W dalszej części tego rozdziału zaimplementowane zostaną metody pobierzDataHttpTekst(), pobierzDataHttpObiekt(), pobierzDataPortTekst(), pobierzDataPortObiekt() i pobierzDataRMIObiekt().

0x01 graphic

--> Rysunek 10.1.[Author:PG]

Interfejs użytkownika apletu ApletGodziny

Proszę zauważyć, że przykłady w niniejszym rozdziale korzystają z kilku metod JDK 1.0, które zostały zarzucone w JDK 1.1. Zostało to uczynione w celu poprawienie przenośności. Podczas kompilowania przykładów w nowych JDK wyświetlone zostaną ostrzeżenia na temat zarzucenia, mogą one jednak zostać bezpiecznie zignorowane.

Przykład 10.1.

ApletGodziny, bez ulepszeń

import java.applet.*;

import java.awt.*;

import java.io.*;

import java.util.*;

public class ApletGodziny extends Applet {

TextField httpTekst, httpObiekt, portTekst, portObiekt, RMIObiekt;

Button odswiez;

public void init() {

// Konstruowanie interfejsu użytkownika

setLayout(new BorderLayout());

// Po lewej stronie dodanie etykiet dla różnych mechanizmów komunikacji

Panel zachod = new Panel();

zachod.setLayout(new GridLayout(5, 1));

zachod.add(new Label("Tekst HTTP: ", Label.RIGHT));

zachod.add(new Label("Obiekt HTTP: ", Label.RIGHT));

zachod.add(new Label("Tekst portu: ", Label.RIGHT));

zachod.add(new Label("Obiekt portu: ", Label.RIGHT));

zachod.add(new Label("Obiekt RMI: ", Label.RIGHT));

add("Zachod", zachod);

// Po prawej utworzenie pól tekstowych wyświetlających otrzymane wartości czasu

Panel centrum = new Panel();

centrum.setLayout(new GridLayout(5, 1));

httpTekst = new TextField();

httpTekst.setEditable(false);

centrum.add(httpTekst);

httpObiekt = new TextField();

httpObiekt.setEditable(false);

centrum.add(httpObiekt);

portTekst = new TextField();

portTekst.setEditable(false);

centrum.add(portTekst);

portObiekt = new TextField();

portObiekt.setEditable(false);

centrum.add(portObiekt);

RMIObiekt = new TextField();

RMIObiekt.setEditable(false);

centrum.add(RMIObiekt);

add("Centrum", centrum);

// Na dole utworzenie przycisku uaktualniającego czas

Panel poludnie = new Panel();

odswiez = new Button("Odswież");

poludnie.add(odswiez);

add("Poludnie", poludnie);

}

public void start() {

odswiez();

}

private void odswiez() {

// pobranie i wyświetlenie wartości czasu

httpTekst.setText(pobierzDataHttpTekst());

httpObiekt.setText(pobierzDataHttpObiekt());

portTekst.setText(pobierzDataPortTekst());

portObiekt.setText(pobierzDataPortObiekt());

RMIObiekt.setText(pobierzDataRMIObiekt());

}

private String pobierzDataHttpTekst() {

// Pobranie obecnego czasu przy pomocy opartego na tekście połączenia HTTP

return "niedostępny";

}

private String pobierzDataHttpObiekt() {

// Pobranie obecnego czasu przy pomocy opartego na obiektach połączenia HTTP

return "niedostępny";

}

private String pobierzDataPortTekst() {

// Pobranie obecnego czasu przy pomocy opartego na tekście połączenia z portem

//nie-HTTP

return "niedostępny";

}

private String pobierzDataPortObiekt() {

// Pobranie obecnego czasu przy pomocy opartego na obiektach połączenia z portem

//nie-HTTP

return "niedostępny";

}

private String pobierzDataRMIObiekt() {

// Pobranie obecnego czasu przy pomocy komunikacji RMI

return "niedostępny";

}

public boolean obslugaWyjatek(Event zdarzenie) {

// Po przyciśnięciu przycisku "Odśwież" odświeżenie ekranu

// Wykorzystanie zdarzeń JDK 1.0 w celu zapewnienia maksymalnej przenośności

switch (zdarzenie.id) {

case Event.ACTION_EVENT:

if (zdarzenie.target == odswiez) {

odswiez();

return true;

}

}

return false;

}

}

Aby aplet ten był dostępny do pobrania przez przeglądarkę klienta, musi być umieszczony w macierzystym katalogu dokumentów serwera, razem z plikiem HTML odwołującym się do niego. Kod HTML może wyglądać następująco:

<HTML>

<HEAD><TITLE>Aplet godziny</TITLE></HEAD>

<BODY>

<CENTER><H1>Aplet godziny</H1></CENTER>

<CENTER><APPLET CODE=ApletGodziny CODEBASE=/ WIDTH=300 HEIGHT=180>

</APPLET></CENTER>

</BODY></HTML>

Parametr CODEBASE wskazuje na katalog (z perspektywy klienta), gdzie umieszczony jest plik klasy apletu. Wartość parametru CODEBASE / oznacza, że pliki klasy zostaną pobrane z katalogu macierzystego dokumentów dla domyślnej aplikacji WWW. Jeżeli parametr CODEBASE nie zostanie określony, jego domyślną wartością jest katalog, w którym znajduje się plik HTML. Przyjmując, że plik HTML nosi nazwę godziny.html, aplet ten jest dostępny pod adresem http://serwer:port/godziny.html, a klasa apletu zostanie pobrana z URL-a http://serwer:port/ApletGodziny.class.

Oparta na tekście komunikacja HTTP

Na początku omówiona zostanie implementacja podejścia najpopularniejszego — oparta na tekście komunikacja HTTP.

Serwlet

Aby aplet ApletGodziny mógł pobierać aktualny czas z serwera, musi on porozumiewać się z serwletem, który zwraca aktualny czas. Przykład 10.2 przedstawia taki serwlet. Odpowiada on na wszystkie żądania POST i GET tekstową reprezentacją aktualnego czasu.

Przykład 10.2.

Serwlet SerwletGodziny obsługujący prosty dostęp HTTP

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class SerwletGodziny extends HttpServlet{

public Data getDate() {

return new Data();

}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

odp.setContentType("text/plain");

PrintWriter out = odp.getWriter();

wyj.println(getDate().toString());

}

}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

doGet(zad, odp);

}

}

Powyższa klasa serwletu powinna zostać umieszczona w standardowym dla serwerów miejscu, katalog_kontekstowy/WEB-INF/classes. Zakładając, że są one umieszczone w domyślnym kontekście, są dostępne dla każdej przeglądarki WWW przy pomocy URL-a http://serwer:port/servlet/SerwletGodziny.

Powrót do apletu

W tym momencie, aby ApletGodziny mógł porozumiewać się z serwerem, musi zachowywać się tak, jak przeglądarka i ustanowić połączenie HTTP z URL-em serwletu, jak to przedstawia implementacja pobierzDataHttpTekst() w przykładzie 10.3.

Przykład 10.3.

ApletGodziny pobierający czas przy pomocy HTTP

import java.net.URL; // nowy dodatek

import com.oreilly.servlet.HttpMessage; // klasa wspierająca, przedstawiona dalej

private String pobierzDataHttpTekst() {

try {

// Tworzenie URL-a odnoszącego się do serwletu

URL url = new URL(getCodeBase(), "/servlet/SerwletGodziny");

// Tworzenie com.oreilly.servlet.HttpMessage w celu porozumienia się z tym URL-em

HttpMessage wiad = new HttpMessage(url);

// Wysłanie wiadomości GET serwletowi, bez łańcucha zapytania

// Pobranie odpowiedzi jako łańcucha InputStream

InputStream in = wiad.sendGetMessage();

// Dołączenie InputStream do DataInputStream

DataInputStream wynik =

new DataInputStream(new BufferedInputStream(in));

// Odczytanie pierwszej linii odpowiedzi, która powinna być łańcuchową

// reprezentacją aktualnego czasu

String data = wynik.readLine();

// Zamknięcie InputStream

in.close();

// Zwrócenie odczytanego czasu

return data;

}

catch (Exception e) {

// Jeżeli nastąpił problem, wynik zapisany w System.out

// (zazwyczaj konsoli Javy) i zwrócenie null

e.printStackTrace();

return null;

}

}

Powyższa metoda odczytuje aktualny czas na serwerze przy pomocy opartego na tekście połączenia HTTP. Po pierwsze, tworzy obiekt URL, który odwołuje się do serwletu SerwletGodziny pracującego na serwerze. Komputer macierzysty serwera i port dla tego URL-a pochodzi z własnej metody apletu getCodeBase(). Gwarantuje to zgodność komputera i portu z danymi o miejscu, z którego został pobrany aplet. Następnie metoda tworzy obiekt HttpMessage w celu porozumienia się z tym URL-em. Obiekt ten wykonuje całą ciężką pracę, w tym ustanawianie połączenia. Aplet prosi go o wykonanie żądania GET do SerwletGodziny, po czym odczytuje odpowiedź ze zwróconego InputStream.

Kod HttpMessage jest przedstawiony na przykładzie 10.4. Jest on dość swobodnie wymodelowany na podstawie klasy ServletMessage autorstwa Roda McChesneya.

Przykład 10.4.

Klasa wspierająca HttpMessage

package com.oreilly.servlet;

import java.io.*;

import java.net.*;

import java.util.*;

public class HttpMessage {

URL servlet = null;

Hashtable headers = null;

public HttpMessage(URL servlet) {

this.servlet = servlet;

}

public InputStream sendGetMessage() throws IOException {

return sendGetMessage(null);

}

public InputStream sendGetMessage(Properties args) throws IOException {

String argString = ""; // domyślny

if (args != null) {

argString = "?" + toEncodedString(args);

}

URL url = new URL(servlet.toExternalForm() + argString);

// Wyłączenie buforowania

URLConnection con = url.openConnection();

con.setUseCaches(false);

// Wysłanie nagłówków

sendHeaders(con);

return con.getInputStream();

}

public InputStream sendPostMessage() throws IOException {

return sendPostMessage(null);

}

public InputStream sendPostMessage(Properties args) throws IOException {

String argString = ""; // default

if (args != null) {

argString = toEncodedString(args); // notice no "?"

}

URLConnection con = servlet.openConnection();

// Przygotowanie do wysyłania i odbierania

con.setDoInput(true);

con.setDoOutput(true);

// Wyłączenie buforowania

con.setUseCaches(false);

// Ominięcie błędu w Netscape'ie

con.setRequestProperty("Content-Type",

"application/x-www-form-urlencoded");

// Wysłanie nagłówków

sendHeaders(con);

// Zapisanie argumentów jako danych wysyłanych

DataOutputStream out = new DataOutputStream(con.getOutputStream());

out.writeBytes(argString);

out.flush();

out.close();

return con.getInputStream();

}

public InputStream sendPostMessage(Serializable obj) throws IOException {

URLConnection con = servlet.openConnection();

// Przygotowanie do wysyłania i odbierania

con.setDoInput(true);

con.setDoOutput(true);

// Wyłączenie buforowania

con.setUseCaches(false);

// Ustawienie typu zawartości jako application/x-java-serialized-object

con.setRequestProperty("Content-Type",

"application/x-java-serialized-object");

// Wysłanie nagłówków

sendHeaders(con);

// Zapisanie argumentów jako danych wysyłanych

ObjectOutputStream out = new ObjectOutputStream(con.getOutputStream());

out.writeObject(obj);

out.flush();

out.close();

return con.getInputStream();

}

public void setHeader(String name, String value) {

if (headers == null) {

headers = new Hashtable();

}

headers.put(name, value);

}

// Wysłanie zawartości tablicy asocjacyjnej nagłówków do serwera

private void sendHeaders(URLConnection con) {

if (headers != null) {

Enumeration enum = headers.keys();

while (enum.hasMoreElements()) {

String name = (String) enum.nextElement();

String value = (String) headers.get(name);

con.setRequestProperty(name, value);

}

}

}

public void setCookie(String name, String value) {

if (headers == null) {

headers = new Hashtable();

}

String existingCookies = (String) headers.get("Cookie");

if (existingCookies == null) {

setHeader("Cookie", name + "=" + value);

}

else {

setHeader("Cookie", existingCookies + "; " + name + "=" + value);

}

}

public void setAuthorization(String name, String password) {

String authorization = Base64Encoder.encode(name + ":" + password);

setHeader("Authorization", "Basic " + authorization);

}

private String toEncodedString(Properties args) {

StringBuffer buf = new StringBuffer();

Enumeration names = args.propertyNames();

while (names.hasMoreElements()) {

String name = (String) names.nextElement();

String value = args.getProperty(name);

buf.append(URLEncoder.encode(name) + "=" + URLEncoder.encode(value));

if (names.hasMoreElements()) buf.append("&");

}

return buf.toString();

}

}

Można by się spodziewać, że klasa HttpMessage ustanawia połączenie przez zwykły port do serwera, po czym przechodzi na HTTP. To podejście na pewno działałoby, nie jest jednak konieczne. Klasy wyższego poziomu java.net.URL i java.net.URLConnection dostarczają już tej funkcjonalności we właściwy sposób.

Poniżej przedstawiony jest krótki opis działania HttpMessage. Klasa ta jest zaprojektowana do komunikacji z jednym tylko URL-em, podanym w konstruktorze. Może ona wysyłać do niego wielokrotne żądania GET i/lub POST, ale zawsze łączy się tylko z jednym URL-em.

Kod wykorzystywany przez HttpMessage do wysyłania wiadomości GET jest stosunkowo prosty. Po pierwsze, sendGetMessage() tworzy zakodowany w URL-u łańcuch zapytania z przekazanej listy java.util.Properties. Dołącza ten łańcuch zapytania do zapisanego URL-a, tworząc nowy obiekt URL. W tym momencie można by się zdecydować na wykorzystanie tego nowego obiektu (pod nazwą url) w celu porozumienia się z serwerem. Wywołanie url.openStream() zwróciłoby łańcuch InputStream zawierający odpowiedź. Jednak, niestety dla celów tego przykładu, domyślnie wszystkie połączenia wykonane przy pomocy obiektu URL są buforowane. Nie jest to pożądane — zwrócony ma być czas aktualny, a nie czas ostatniego żądania. Tak więc HttpMessage musi wyłączyć buforowanie.

Klasa URL nie obsługuje bezpośrednio tej kontroli niskiego poziomu, tak więc HttpMessage pobiera URLConnection obiektu URL i instruuje go, aby nie wykorzystywał buforowania. Ostatecznie, HttpMessage zwraca InputStream obiektu URLConnection, który zawiera odpowiedź serwletu.

Kod wykorzystywany przez HttpMessage do wysłania żądania POST (sendPostMessage()) jest podobny. Podstawową różnicą między nimi jest fakt, że ten drugi zapisuje przechowywaną w URL-u informacje bezpośrednio w kodzie żądania. Jest to zgodne z protokołem wysłania informacji przez żądania POST. Inna różnica to fakt, że HttpMessage obowiązkowo ustawia typ zawartości żądania na application/x-www-form-urlencoded. Informuje to serwer, że zawartość POST zawiera informacje o parametrach.

Należy wspomnieć, że HttpMessage to klasa ogólnego przeznaczenia, służąca do komunikacji HTTP. Nie musi ona być wykorzystywana przez aplety, ani też łączyć się z serwletami. Może z niej skorzystać każdy klient Javy, który musi połączyć się z dowolnymi zasobami HTTP. Jest ona jednak dołączona do pakietu com.oreilly.servlet, ponieważ często okazuje się przydatna w komunikacji aplet-serwlet.

Aby klasa HttpMessage mogła być wykorzystywana przez aplety, musi być udostępniona przez pobranie jej równolegle do klas apletów. Oznacza to, że musi być ona umieszczona w odpowiednim miejscu w katalogu macierzystym dokumentów serwera. Dla serwera Tomcat, pozycja ta to katalog_macierzysty/webapps/ROOT/com/oreilly/servlet. Poleca się skopiowanie klasy z miejsca, w którym pakiet com.oreilly.servlet został oryginalnie zainstalowany (zazwyczaj katalog_macierzysty/classes/com/oreilly/servlet).

Proszę zauważyć, że HttpMessage posiada kilka swoich metod, które pozwalają na wysyłanie nagłówków żądań, cookies i podstawowych informacji uwierzytelniających do serwera jako części żądania. Metoda setHeader(String nazwa, String wartość) dodaje do żądania nagłówek o odpowiedniej nazwie i wartości. Jeżeli nagłówek został ustawiony wcześniej, nowa wartość zastępuje starą. Metoda setCookie(String nazwa, String wartość) dodaje do żądania cookie o odpowiedniej nazwie i wartości. Metoda ta jest szczególnie przydatna przy omijaniu ograniczeń śledzenia sesji dla apletów działających wewnątrz Java Plug-In, jak omówiono w rozdziale 7, „Śledzenie sesji”. Ostatecznie, metoda setAuthorization(String nazwa, String hasło) dodaje dane użytkownika do żądania, pozwalając apletowi na dostęp do stron chronionych hasłem. Wykorzystuje klasę com.oreilly.servlet.Base64Encoder, która nie jest przedstawiona w niniejszej książce, lecz jest dostępna pod adresem http://servlets.com. Dla wszystkich powyższych metod, ustawienia pozostają niezmienione pomiędzy żądaniami, a wywołujący jest odpowiedzialny za to, aby w nazwie i wartości nie występowały żadne niedozwolone znaki.

Aby nawiązać bezpieczne połączenie z apletu, należy przekazać HttpMessage URL rozpoczynający HTTPS. Jest to tak łatwe! (Proszę tylko nie zapomnieć o tym, że serwer musi mieć włączoną obsługę HTTPS.) Własność ta działa jednak jedynie w przypadku apletów, ponieważ przeglądarka, w której uruchomiono aplet może pomóc w wynegocjowaniu połączenia HTTPS. W przypadku klienta nie będącego apletem konstruktor URL zwróci błąd nieznanego protokołu HTTPS. Można to ominąć wykorzystując klasę com.oreilly.servlet.HttpsMessage dostępną pod adresem http://www.servlets.com. Zawiera ona samodzielną obsługę HTTPS autorstwa Matta Towersa. Artykuł JavaWorld opisujący jej działanie znajduje się pod adresem http://www.javaworld.com/javaworlsd/javatips/jw-javatip96.html.

W tym momencie istnieje już działający aplet odczytujący aktualny czas z serwera przy pomocy opartej na tekście komunikacji HTTP aplet-serwer. Po wypróbowaniu jego działania można zobaczyć, że data „Tekst HTTP” jest wypełniona, podczas gdy inne są oznaczone jako „niedostępny”.

Oparta na obiektach komunikacja HTTP

Po kilku modyfikacjach, można dostosować ApletGodziny tak, aby pobierał aktualny czas jako zserializowany obiekt Data.

Serwlet

W celu zachowania wstecznej kompatybilności, można zmienić SerwletGodziny tak, aby zwracał zserializowany obiekt jedynie, gdy wykonane zostanie odpowiednie żądanie przez przekazanie parametru format o wartości obiekt. Odpowiedni kod przedstawiony jest w przykładzie 10.5.

Przykład 10.5.

SerwletGodziny wykorzystujący HTTP w celu pracy na obiekcie

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class SerwletGodziny extends HttpServlet{

public Data getDate() {

return new Data();

}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

// Jeżeli klient przekaże "format=obiekt"

// Wysłanie Data jako obiektu zserializowanego

if ("obiekt".equals(zad.getParameter("format"))) {

ObjectOutputStream wyj = new ObjectOutputStream(odp.getOutputStream());

wyj.writeObject(getDate());

}

// W przeciwnym wypadku wysłanie Data jako zwykłego łańcucha

else {

PrintWriter wyj = odp.getWriter();

wyj.println(getDate().toString());

}

}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

doGet(zad, odp);

}

}

Jak widać na powyższym przykładzie, wysyłanie zserializowanego obiektu Javy jest stosunkowo proste. Technika ta może być wykorzystana w celu wysłania wszystkich prymitywnych typów i/lub obiektów Javy, które posiadają implementację interfejsu Serializable, włączając w to obiekty Vector, zawierające obiekty Serializable. W jednym łańcuchu ObjectOutputStream może zostać zapisana duża ilość obiektów, jeżeli klasa odbierająca obiekty odczytuje je w tej samej kolejności i nadaje im te same typy.

Można zauważyć, że serwlet nie ustawił typu zawartości odpowiedzi w celu wskazania, że jest ona zserializowanym obiektem Javy. Powodem tego jest aktualny brak standardowych typów MIME reprezentujących obiekty zserializowane. Jednak nie ma to znaczenia. Typ zawartości jest jedynie wskaźnikiem dla klienta, jak ma on obsłużyć lub wyświetlić odpowiedź. Jeżeli aplet już przewiduje odbieranie konkretnego zserializowanego obiektu Javy, wszystko działa bez zakłóceń. Jednak czasami warto jest zastosować własne typy MIME (specyficzne dla danej aplikacji), tak aby serwlet mógł wskazać apletowi zawartość jego odpowiedzi.

Aplet

Kod apletu odczytującego zserializowany obiekt Data jest bardzo podobny do kodu odczytującego zwykły tekst. Metoda pobierzDataHttpObiekt() jest przedstawiona na przykładzie 10.6.

Przykład 10.6.

ApletGodziny wykorzystujący HTTP do odczytania obiektu

private String pobierzDataHttpObiekt() {

try {

// Tworzenie URL-a odnoszącego się do serwletu

URL url = new URL(getCodeBase(), "/servlet/SerwletGodziny");

// Tworzenie com.oreilly.servlet.HttpMessage w celu porozumienia się z tym URL-em

HttpMessage wiad = new HttpMessage(url);

// Tworzenie listy właściwości Properties w celu podania format=obiekt

Properties wlasc = new Properties();

wlasc.put("format", "obiekt");

// Wysłanie wiadomości GET serwletowi, przekazanie „wlasc” jako łańcucha zapytania

// Pobranie odpowiedzi jako łańcucha ObjectInputStream

InputStream in = wiad.sendGetMessage(wlasc);

ObjectInputStream wynik = new ObjectInputStream(in);

// Odczytanie obiektu Data z potoku

Object obi = wynik.readObject();

Date data = (Date)obi;

in.close();

// Zwrócenie łańcuchowej reprezentacji Data

return data.toString();

}

catch (Exception e) {

// Jeżeli nastąpił problem, wynik zapisany w System.out

// (zazwyczaj konsoli Javy) i zwrócenie null

e.printStackTrace();

return null;

}

}

Występują dwie różnice pomiędzy tą konkretną metodą i metodą pobierzDataHttpTekst(). Po pierwsze, metoda ta tworzy listę właściwości Properties w celu nadania parametrowi formatu wartości obiektu. Działanie to ma na celu nakazanie serwletowi SerwletGodziny zwrócenia obiektu zserializowanego. Po drugie, nowa metoda odczytuje zwróconą zawartość jako Obiekt, przy pomocy klasy ObjectInputStream i jej metody readObject().

Jeżeli serializowana klasa nie jest częścią Core API Javy (czyli nie jest od razu dostępna apletowi), musi ona również zostać udostępniona we właściwym miejscu w katalogu macierzystym dokumentów serwera. Aplet może zawsze otrzymywać zserializowane zawartości obiektu, musi jednak pobrać plik jego klasy w celu pełnej jego rekonstrukcji.

W tym momencie aplet może odczytywać aktualny czas przy pomocy zarówno opartej na tekście, jak i na obiektach, komunikacji HTTP. Po wypróbowaniu przykładu (przy pomocy przeglądarki WWW lub przeglądarki apletów obsługującej JDK 1.1) można dostrzec, że pola „Tekst HTTP” i „Obiekt HTTP” zostały wypełnione.

Przesyłanie zserializowanego obiektu lub pliku

Przed przejściem do następnych metod, należy opisać jeszcze jedną (dotychczas nieomówioną) metodę z klasy HttpMessagesendPostMessage(Serializable). Metoda ta pomaga apletowi wysłać zserializowany obiekt do serwletu metodą POST. Taki transfer obiektu nie jest szczególnie użyteczny w opisywanym tu przykładzie (i raczej do niego nie pasuje), lecz zostanie wspomniany, ponieważ może okazać się użyteczny, gdyby aplet musiał wysłać skomplikowane struktury danych na serwer. Na przykład, aplet może wysłać obiekt Date, obiekt Person lub tablicę HighScore zawierającą obiekty Date i Person. Aplet może nawet wysłać dokument XML, jako dokument JDOM (http://jdom.org) lub jako zwykły plik. Proszę jedynie pamiętać, że przy wysyłaniu pliku powinno wysłać się zawartość jako obiekt byte[], nie File, ponieważ obiekt File przechowuje jedynie nazwę pliku, a nie jego zawartość. Przykład 10.7 zawiera kod metody sendPostMessage(Serializable).

Przykład 10.7

Wysyłanie obiektu zserializowanego

// Wysyła zserializowany obiekt przy pomocy żądania POST.

// Ustawia typ zawartości na application/x-java-serialized-object

public InputStream sendPostMessage(Serializable obj) throws IOException {

URLConnection con = servlet.openConnection();

// Przygotowanie do wysyłania i odbierania

con.setDoInput(true);

con.setDoOutput(true);

// Wyłączenie buforowania

con.setUseCaches(false);

// Ustawienie typu zawartości jako application/x-java-serialized-object

con.setRequestProperty("Content-Type",

"application/x-java-serialized-object");

// Wysłanie nagłówków

sendHeaders(con);

// Zapisanie argumentów jako danych wysyłanych

ObjectOutputStream wyj = new ObjectOutputStream(con.getOutputStream());

wyj.writeObject(obj);

wyj.flush();

out.close();

return con.getInputStream();

}

Aplet wykorzystuje sendPostMessage(Serializable) tak, jak wykorzystuje sendPostMessage(Properties). Poniżej przedstawiony jest kod apletu wysyłający wszystkie napotkane wyjątki do serwletu:

catch (Exception w) {

URL url = new URL(getCodeBase(), "/servlet/DziennikWyjatkow");

HttpMessage wiad = new HttpMessage(URL);

InputStream = wiad.sendPostMessage(e);

}

Tymczasem serwlet otrzymuje wyjątek Exception przez swoją metodę doPost(), w następujący sposób:

ObjectInputStream obiin = new ObjectInputStream(zad.getInputStream());

Object obi = obiin.readObject();

Exception w = (Exception) obi;

Serwlet może otrzymywać typ wysłanego obiektu jako podtyp (druga część typu zawartości). Proszę zauważyć, że powyższa metoda sendPostMessage(Serializable) pobiera tylko jeden obiekt w danym czasie oraz, że pobiera jedynie obiekty Serializable (to znaczy, żadnych prymitywnych typów).

Komunikacja przez port

Teraz opisane zostaną sposoby komunikacji apletu i serwletu przy pomocy połączeń portów nie-HTTP.

Serwlet

Rola serwletu w tej technice komunikacji ogranicza się do pasywnego słuchania. Z powodów bezpieczeństwa, jedynie aplet może nawiązać połączenie przez zwykły port. Generalnie, serwlet musi być skonfigurowany do rozpoczęcia nasłuchu począwszy od metody init(), a skończywszy na metodzie destroy(). Pomiędzy tymi dwoma metodami, powinien utworzyć wątek obsługujący dla każdego otrzymanego połączenia z klientem.

W przypadku połączenia HTTP, te trudne w obsłudze szczegóły są zarządzane przez serwer WWW. Serwer ten prowadzi nasłuch przychodzących żądań HTTP i odpowiednio je rozsyła, wywołując odpowiednio metody serwletu service(), doGet() lub doPost(). Jednak kiedy serwlet nie wykorzystuje komunikacji HTTP, serwer WWW nie może dostarczyć żadnej pomocy. Właściwie to serwlet działa jako swój własny serwer i w związku z tym musi zarządzać połączeniami przez port samodzielnie.

Może to brzmieć nieco zastraszająco. Prawda jest taka, że można stworzyć superklasę serwletu, która przejmie na siebie szczegóły związane z zarządzaniem połączeń przez port. Klasa ta, nazwana DaemonHttpServlet, może być wykorzystana przez każdy serwlet, który ma zostać udostępniony poprzez połączenie przez port nie-HTTP.

DaemonHttpServlet rozpoczyna nasłuch żądań klienta w swojej metodzie init(), a kończy na metodzie destroy(). W międzyczasie, dla każdego odebranego połączenia wywołuje abstrakcyjną metodę handleClient(Socket). Metoda ta powinna zostać zaimplementowana przez każdy serwlet, który jest podklasą DaemonHttpServlet.

Przykład 10.8 przedstawia sposób, w jaki SerwletGodziny wykorzystuje klasę DaemonHttpServlet i implementuje handleClient() w celu stania się dostępnym przez połączenie przez port nie-HTTP.

Przykład 10.8.

Serwer HTTP

import java.io.*;

import java.net.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.oreilly.servlet.DaemonHttpServlet;

public class SerwletGodziny extends DaemonHttpServlet {

public Date getDate() {

return new Date();

}

public void init() throws ServletException {

// Jak poprzednio, nie ma potrzeby wywoływania super.init()

// z bezargumentowej metody init, trzeba jednak wywołać super.init(config) ze

// starszej metody init(ServletConfig). Szczegóły można znaleźć w rozdziale 3.

}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

// Jeżeli klient przesłał "format=obiekt",

// to wysłanie daty jako obiektu zserializowanego

if ("obiekt".equals(zad.getParameter("format"))) {

ObjectOutputStream wyj = new ObjectOutputStream(odp.getOutputStream());

wyj.writeObject(getDate());

}

// W przeciwnym wypadku wysłanie daty jako zwykłego łańcucha ASCII

else {

PrintWriter wyj = odp.getWriter();

wyj.println(getDate().toString());

}

}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

doGet(zad, odp);

}

public void destroy() {

// W tym miejscu, inaczej niż poprzednio, po przejęciu destroy() należy wywołać

// super.destroy()

super.destroy();

}

// Obsługa połączenia klienta przez port poprzez dodanie wątku PolaczenieGodziny

public void handleClient(Socket klient) {

new PolaczenieGodziny(this, klient).start();

}

}

class PolaczenieGodziny extends Thread {

SerwletGodziny serwlet;

Socket klient;

PolaczenieGodziny(SerwletGodziny serwlet, Socket klient) {

this.serwlet = serwlet;

this.klient = klient;

setPriority(NORM_PRIORITY - 1);

}

public void run() {

try {

// Odczytanie pierwszej linii wysłanej przez klienta, jako tekstu Latin-1

BufferedReader in = new BufferedReader(

new InputStreamReader(

klient.getInputStream(), "ISO-8859-1"));

String line = in.readLine();

// Jeżeli brzmiała ona „Obiekt”, zwrócenie Data jako obiektu zserializowanego

if ("obiekt".equals(line)) {

ObjectOutputStream wyj =

new ObjectOutputStream(klient.getOutputStream());

wyj.writeObject(serwlet.getDate());

wyj.close();

}

// W przeciwnym wypadku wysłanie Data jako zwykłego łańcucha

else {

// Owinięcie PrintStream dookoła OutputStream portu Socket

PrintStream wyj = new PrintStream(klient.getOutputStream());

wyj.println(serwlet.getDate().toString());

wyj.close();

}

// Należy zapewnić zamknięcie połączenia

klient.close();

}

catch (IOException w) {

serwlet.log("Wyjątek IOException podczas obsługi żądania klienta", w);

}

catch (Exception w) {

serwlet.log("Wyjątek Exception podczas obsługi żądania klienta", w);

}

}

}

Klasa SerwletGodziny pozostała w większej części niezmieniona w porównaniu z poprzednią formą. Główną różnicą polega na tym, że jest ona teraz rozszerzeniem klasy DaemonHttpServlet i wykorzystuje metodę handleClient(Socket), która tworzy nowy wątek PolaczenieGodziny. Ten egzemplarz PolaczenieGodziny ponosi odpowiedzialność za obsługę konkretnego połączenia przez port.

PolaczenieGodziny działa w sposób opisany poniżej. Kiedy zostaje utworzone, zapamiętuje odwołanie do SerwletGodziny, tak więc może wywołać metodę serwletu getDate() i odwołanie do portu Socket, w celu zapewnienia komunikacji z klientem. PolaczenieGodziny ustawia swój priorytet o jeden poziom niżej od normalnego, aby wskazać, że połączenie to może zaczekać, kiedy inne wątki wykonują ważniejszą pracę.

Bezpośrednio po utworzeniu wątku PolaczenieGodziny, SerwletGodziny rozpoczyna wątek, powodując wywołanie swojej metody run(). W tej metodzie, PolaczenieGodziny porozumiewa się z klientem przy pomocy nienazwanego (ale na pewno nie będącego HTTP) protokołu. Rozpoczyna od odczytania pierwszej linii wysłanej przez klienta. Jeżeli wartość tej linii wynosi obiekt, zwraca on aktualny czas jako zserializowany obiekt Data. Jeżeli linia ta zawiera dowolną inną wartość, zwraca aktualny czas w postaci zwykłego łańcucha. Kiedy kończy swoją działalność, zamyka połączenie.

Superklasa

Niskiego poziomu zarządzanie portem jest wykonywane przez klasę DaemonHttpServlet. Ogólnie rzecz biorąc, klasa ta może być zastosowana bez modyfikacji, warto jest jednak zrozumieć jej wnętrze. Jaj kod przedstawiony w przykładzie 10.9. Proszę zauważyć, że również ona jest napisana według Servlet API 2.0 w celu zapewnienia wstecznej kompatybilności.

Przykład 10.9.

Superklasa DaemonHttpServlet

package com.oreilly.servlet;

import java.io.*;

import java.net.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public abstract class DaemonHttpServlet extends HttpServlet {

protected int DEFAULT_PORT = 1313; // nie określony ostatecznie

private Thread daemonThread;

public void init(ServletConfig config) throws ServletException {

super.init(config);

// rozpoczęcie wątku demona

try {

daemonThread = new Daemon(this);

daemonThread.start();

}

catch (Exception e) {

log("Problem starting socket server daemon thread" +

e.getClass().getName() + ": " + e.getMessage());

}

}

// Zwraca port połączenia, na którym ten serwlet będzie nasłuchiwał

// Serwlet może określić port na trzy sposoby — przy pomocy parametru init

//socketPort, przez określenie zmiennej DEFAULT_PORT przed wywołaniem super.init(),

//lub przez przejecie implementacji tej metody

protected int getSocketPort() {

try { return Integer.parseInt(getInitParameter("socketPort")); }

catch (NumberFormatException e) { return DEFAULT_PORT; }

}

abstract public void handleClient(Socket client);

public void destroy() {

try {

daemonThread.stop();

daemonThread = null;

}

catch (Exception e) {

log("Problem stopping server socket daemon thread: " +

e.getClass().getName() + ": " + e.getMessage());

}

}

}

// Praca ta wykonywana jest przez klasę pomocniczą tak, aby podklasy DaemonHttpServlet

// może bez problemów zdefiniować swoją własną metodę run()

class Daemon extends Thread {

private ServerSocket serverSocket;

private DaemonHttpServlet servlet;

public Daemon(DaemonHttpServlet servlet) {

this.servlet = servlet;

}

public void run() {

try {

// Stworzenie portu serwera akceptującego połączenia

serverSocket = new ServerSocket(servlet.getSocketPort());

}

catch (Exception e) {

servlet.log("Problem establishing server socket: " +

e.getClass().getName() + ": " + e.getMessage());

return;

}

try {

while (true) {

// Po otrzymaniu połączenia, wywołanie metody handleClient() serwera.

// Proszę zauważyć, że metoda ta jest blokująca. Na serwlet spada

// odpowiedzialność utworzenia wątku obsługującego długo trwające połączenia.

try {

servlet.handleClient(serverSocket.accept());

}

catch (IOException ioe) {

servlet.log("Problem accepting client's socket connection: " +

ioe.getClass().getName() + ": " + ioe.getMessage());

}

}

}

catch (ThreadDeath e) {

// Kiedy wątek zostaje zabity, zamknięcie portu serwera

try {

serverSocket.close();

}

catch (IOException ioe) {

servlet.log("Problem closing server socket: " +

ioe.getClass().getName() + ": " + ioe.getMessage());

}

}

}

}

Metoda init() DaemonHttpServlet tworzy i rozpoczyna nowy wątek Daemon, który odpowiada za nasłuchiwanie połączeń przychodzących. Metoda destroy() zatrzymuje wątek. Sprawia to że imperatywem staje się, żeby każdy serwlet będący podklasą DaemonHttpServlet wywoływał super.init() i super.destroy(), jeżeli serwlet ten wykorzystuje własne metody init() i destroy(). (Serwlet napisany według Servlet API 2.1 i wykorzystujący bez argumentów init() nie musi wywoływać super.init(), lecz musi wywołać super.destroy().)

Wątek Daemon rozpoczyna działalność utworzeniem ServerSocket w celu nasłuchiwania na konkretnym porcie połączenia. Numer portu jest określany przez wywołanie metody serwletu getSocketPort(). Wartość przez nią zwracana to wartość parametru inicjacji SocketPort, lub, w wypadku nieistnienia tego parametru, obecna wartość zmiennej DEFAULT_PORT. Serwlet może zdecydować się na przejęcie implementacji getSocketPort, jeżeli jest to konieczne.

Po utworzeniu ServerSocket, wątek Daemon czeka na nadchodzące żądania przy pomocy wywołania serverSocket.accept(). Metoda jest blokująca — zatrzymuje wykonywanie tego wątku do czasu połączenia się przez klienta. Kiedy się to dzieje, metoda accept() zwraca obiekt Socket, który wątek Daemon natychmiast przekazuje do metody serwletu handleClient(). Ta metoda zazwyczaj tworzy wątek obsługujący i natychmiast powraca, powodując przejście wątku Daemon w stan gotowości do przyjęcia następnego połączenia.

Oczyszczenie portu jest tak samo ważne, jak jego konfiguracja. Należy upewnić się, że port połączenia działa tak długo jak serwlet, ale nie dłużej. W tym momencie metoda destroy() DaemonHttpServlet wywołuje metodę stop() wątku Daemon. Wywołanie to nie zatrzymuje jednak natychmiast wątku Daemon. Powoduje jedynie wystąpienie wyjątku ThreadDeath wątku Daemon w jego aktualnym miejscu wykonania. Wątek Daemon przejmuje ten wyjątek i zamyka port połączenia.

Przy stworzeniu serwletu działającego jako serwer nie-HTTP mogą wystąpić dwa problemy. Po pierwsze, jedynie jeden serwlet w jednym czasie może nasłuchiwać na konkretnym porcie. Sprawia to, że absolutnie konieczne staje się, aby każdy serwlet-demon wybierał swój własny port połączenia — poprzez ustawienie swojego parametru inicjacji socketPort, ustawiając zmienną DEFAULT_PORT przed wywołaniem super.init(config) lub bezpośrednim przejęciem getSocketPort(). Po drugie, serwlet-demon musi być załadowany na serwer i wywołana musi być jego metoda init(), zanim będzie on mógł przyjmować przychodzące połączenia nie-HTTP. W związku z tym należy nakazać serwerowi ładowanie go podczas startu, lub upewnić się, że jest on zawsze dostępny przez HTTP, zanim można będzie uzyskać do niego dostęp bezpośredni.

Aplet

Kod apletu łączącego się z serwletem przy pomocy komunikacji nie-HTTP, przede wszystkim metody pobierzDataPortTekst() i pobierzDataPortObiekt(), jest przedstawiony w przykładzie 10.10.

Przykład 10.10.

ApletGodziny pobierający czas przy pomocy połączenia przez zwykły port.

import java.net.socket; // Nowy dodatek

static final int DEFAULT_PORT=1313; // Nowy dodatek

private int getSocketPort() {

try { return Integer.parseInt(getParameter("socketPort")); }

catch (NumberFormatException w) { return DEFAULT_PORT; }

}

private String pobierzDataPortTekst() {

InputStream in = null;

try {

// Ustanowienie połączenia przez port z serwletem

Socket port = new Socket(getCodeBase().getHost(), getSocketPort());

// wyświetlenie pustej linii, oznaczającej chęć pobrania czasu jako zwykłego tekstu

PrintStream wyj = new PrintStream(port.getOutputStream());

wyj.println();

wyj.flush();

// Odczytanie pierwszej linii odpowiedzi

// Powinna ona zawierać aktualny czas

in = port.getInputStream();

DataInputStream wynik =

new DataInputStream(new BufferedInputStream(in));

String data = wynik.readLine();

// Zwrócenie otrzymanego łańcucha

return data;

}

catch (Exception w) {

// Jeżeli wystąpił problem, wyświetlenie w System.out

// (zazwyczaj konsoli Javy) i zwrócenie null

w.printStackTrace();

return null;

}

finally {

// Zawsze zamknięcie połączenia

// Poniższy kod jest wykonywany niezależnie od wykonania poprzednich działań

if (in != null) {

try { in.close(); }

catch (IOException ignored) { }

}

}

}

private String pobierzDataPortObiekt() {

InputStream in = null;

try {

// Ustanowienie połączenia przez port z serwletem

Socket port = new Socket(getCodeBase().getHost(), getSocketPort());

// Wyświetlenie linii "obiekt", wskazującej chęć pobrania Data jako obiektu

// zserializowanego

PrintStream wyj = new PrintStream(port.getOutputStream());

wyj.println("obiekt");

wyj.flush();

// Stworzenie ObjectInputStream odczytującego odpowiedź

in = port.getInputStream();

ObjectInputStream wynik =

new ObjectInputStream(new BufferedInputStream(in));

// Odczytanie obiektu i zapamiętania go jako Data

Object obi = wynik.readObject();

Date data = (Date)obi;

// Zwrócenie łańcuchowej reprezentacji otrzymanej Data

return data.toString();

}

catch (Exception w) {

// Jeżeli wystąpił problem, wyświetlenie w System.out

// (zazwyczaj konsoli Javy) i zwrócenie null

w.printStackTrace();

return null;

}

finally {

// Zawsze zamknięcie połączenia

// Poniższy kod jest wykonywany niezależnie od wykonania poprzednich działań

if (in != null) {

try { in.close(); }

catch (IOException ignored) { }

}

}

}

W obu powyższych metodach, aplet rozpoczyna działanie przez utworzenie obiektu Socket wykorzystywanego do komunikacji z serwerem. Aby go utworzyć, musi znać nazwę komputera i numer portu, na którym nasłuchuje serwlet. Określenie komputera jest proste — musi to być ten sam komputer, z którego został on pobrany, dostępny przy pomocy wywołania getCodeBase().getHost(). Określenie portu jest trudniejsze, jako że zależy wyłącznie od serwletu, z którym łączy się aplet. Aplet ten wykorzystuje metodę getSocketPort() w celu określenia tego. Przedstawiona powyżej implementacja getSocketPort() zwraca wartość parametru apletu socketPort lub (jeżeli parametr ten nie jest podany) wartość zmiennej DEFAULT_PORT.

Po ustanowieniu połączenia przez port, aplet wykorzystuje nienazwany protokół w celu porozumienia się z serwerem. Protokół ten wymaga, aby aplet wysłał jedną linię w celu wskazania, czy aktualny czas wysyłany w odpowiedzi miał formę tekstu, czy obiektu. Jeżeli linia ta zawiera słowo obiekt, aplet otrzymuje obiekt. Jeżeli zawiera cokolwiek innego, otrzymuje zwykły tekst. Po wysłaniu tej linii, aplet może odczytać odpowiedź we właściwy sposób.

Aplet i serwlet mogą kontynuować porozumiewanie się przy pomocy tego portu. Jest to jedna z głównych zalet niestosowania komunikacji HTTP. Jednak w tym przypadku, aplet otrzymał pożądane informacje i może po prostu zamknąć połączenie. Zamknięcie to jest wykonywane w bloku finally. Umieszczenie zamknięcia w tym miejscu zapewnia koniec połączenia niezależnie od tego, czy try spowoduje dowolny wyjątek, czy nie.

Po dodaniu dwóch powyższych metod aplet jest niemal kompletny. Po uruchomieniu go w tym momencie, wynikiem będzie wyświetlenie dat we wszystkich polach poza „Obiekt RMI”.

Komunikacja RMI

We wcześniejszej części tego rozdziału powiedziano, że jednym z powodów nie wykorzystywania komunikacji RMI jest jej skomplikowanie. Chociaż jest to prawda, jest również prawdą, że przy pomocy innej superklasy serwletu, kod wymagany, aby serwlet był dostępny przez komunikację RMI, może być aż śmiesznie prosty. Po pierwsze, dokładnie opisany zostanie proces nadawania serwletowi właściwości obiektu zdalnego. Następnie, po udowodnieniu prostoty tego działania wyjaśniona zostanie cała praca mająca miejsce w tle.

Serwlet

Wszystkie obiekty zdalne RMI muszą wykorzystywać specyficzny interfejs, Interfejs ten wykonuje dwa działania — deklaruje, które metody obiektu zdalnego mają zostać udostępnione zdalnym klientom, oraz rozszerza interfejs Remote w celu wskazania, że jest to interfejs obiektu zdalnego. W przypadku przykładowego serwletu SerwletGodziny, można stworzyć interfejs SerwerGodziny przedstawiony w przykładzie 10.11.

Przykład 10.11. Interfejs SerwerGodziny

import java.util.Date;

import java.rmi.Remote;

import java.rmi.RemoteException;

public interface SerwerGodziny extends Remote {

public Date getDate() throws RemoteException;

}

Powyższy interfejs deklaruje udostępnianie przez SerwletGodziny klientom zdalnym metody getDate(). Proszę zauważyć, że podpis getDate() został nieco zmieniony — wywołuje ona teraz RemoteException. Dla każdej metody udostępnionej przez RMI musi być zadeklarowane wywołanie wyjątku. Chociaż sama metoda nie musi go wywoływać, może on być wywołany przez system w celu wskazania błędu w usługach sieciowych.

Kod SerwletGodziny pozostaje w przeważającej części niezmieniony w porównaniu ze swoją wersją oryginalną. Właściwie jedyne zmiany to aktualna implementacja SerwerGodziny i rozszerzanie com.oreilly.servlet.RemoteHttpServlet, superklasy pozwalającej temu serwletowi na pozostanie niezmienionym. Serwlet wykorzystuje również metodę destroy(), która wywołuje super.destroy(). Prawdą jest, że metoda ta jest właściwie bezużyteczna w niniejszym przykładzie, ale przypomina o tym, że każda metoda destroy() zaimplementowana w zdalnym serwlecie musi wywoływać super.destroy() w celu dostarczenia metodzie destroy() obiektu RemoteHttpServlet możliwości zakończenia komunikacji RMI. Przykład 10.12 przedstawia nowy kod SerwletGodziny.

Przykład 10.12.

SerwletGodziny obsługuje teraz dostęp RMI

import java.io.*;

import java.net.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.oreilly.servlet.RemoteDaemonHttpServlet; // Nowy dodatek

public class SerwletGodziny extends RemoteDaemonHttpServlet // Nowy dodatek

implements SerwerGodziny { // Nowy dodatek

// Pojedyncza metoda z SerwerGodziny

// Uwaga: klauzula na temat wywoływania nie jest tu potrzebna

public Data getDate() {

return new Data();

}

public void init(ServletConfig konfig) throws ServletException {

super.init(konfig);

// Miejsce na dodatkowy kod

}

public void doGet(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

// Jeżeli klient prześle "format=obiekt", to wysłanie Data jako obiektu

// zserializowanego

if ("obiect".equals(zad.getParameter("format"))) {

ObjectOutputStream wyj = new ObjectOutputStream(odp.getOutputStream());

wyj.writeObject(getDate());

}

// W przeciwnym wypadku wysłanie Data jako zwykłego łańcucha ASCII

else {

PrintWriter wyj = odp.getWriter();

wyj.println(getDate().toString());

}

}

public void doPost(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

doGet(zad, odp);

}

public void destroy() {

// Po przejęciu destroy() należy wywołać super.destroy()

super.destroy();

}

W taki właśnie sposób tworzy się zdalny obiekt serwletu. Kompilacja zdalnego obiektu serwletu jest taka sama, jak dla każdego innego serwletu, z jednym dodatkowym krokiem. Po skompilowaniu kodu źródłowego serwletu należy skompilować klasę serwletu przy pomocy kompilatora RMI rmic. Kompilator RMI pobiera plik klasy obiektu zdalnego i tworzy szkieletową i końcową wersję klasy. Klasy te pracują w tle w celu umożliwienia komunikacji RMI. Nie należy przejmować się szczegółami, trzeba jednak wiedzieć, że końcówka pomaga klientowi w wywoływaniu metod na zdalnym obiekcie, a szkielet pomaga serwerowi w obsłudze tych wywołań.

Wykorzystywanie rmic jest podobne do stosowania javac. W przypadku tego przykładu można skompilować SerwletGodziny przy pomocy następującego polecenia:

% rmic SerwletGodziny

Proszę zauważyć, że rmic należy dostarczyć nazwę klasy Javy — nie pliku — do skompilowania. W związku z tym, jeżeli kompilowany serwlet jest częścią pakietu powinien zostać podany rmic jako nazwa.pakietu.NazwaSerwletu. Program rmic może pobierać ścieżkę klasy do poszukiwania przy pomocy parametru -classpath, a także katalog docelowy dla plików szkieletu i końcówki przy pomocy parametru -d.

Po wykonaniu powyższego polecenia rmic, powinny ukazać się dwa nowe pliki klas — SerwletGodziny_Stub.class (końcówka) i SerwletGodziny_skel.class (szkielet). Poniżej przedstawione zostanie ich zastosowanie. Po pierwsze, należy pamiętać, że nie jest konieczne ponowne uruchamianie kompilatora RMI za każdym razem, kiedy modyfikowany jest kod zdalnego serwletu. Jest tak, ponieważ klasy szkieletu i końcówki są wbudowane w interfejs serwletu, a nie w implementację tego interfejsu. W związku z tym, należy zregenerować go jedynie po modyfikacji interfejsu SerwerGodziny (lub interfejsu do niego równoważnego).

Teraz następuje ostatni krok w tworzeniu zdalnego serwletu — kopiowanie kilku plików klas do katalogu macierzystego dokumentów serwera, z którego to miejsca mogą być one pobrane przez aplet. Pobrane muszą zostać dwa pliki klas — końcówka SerwletGodziny_Stub.class i zdalny interfejs klasy SerwerGodziny.class. Klient (w tym przypadku aplet) potrzebuje końcówki w celu wykonania swojej części komunikacji RMI, a sama końcówka wykorzystuje interfejs zdalnej klasy. Proszę pamiętać, że serwlet również wykorzystuje te klasy, więc należy skopiować je do katalogu macierzystego dokumentów serwera oraz pozostawić je w ścieżce klas serwera. Rysunek 10.2 przedstawia pozycje wszystkich plików serwera.

0x01 graphic

Rysunek 10.2.

To jest koniec! Jeżeli postępuje się według powyższych instrukcji możliwe jest utworzenie w krótkim czasie działającego zdalnego serwletu. Poniżej przedstawiona jest klasa RemoteHttpServlet i opisane działania zachodzące w tle.

Superklasa

Obiekt zdalny musi wykonać dwa działania, aby przygotować się do komunikacji RMI — musi się wyeksportować i zarejestrować. Kiedy obiekt zdalny eksportuje siebie, zaczyna nasłuchiwanie na porcie, oczekując na przychodzące wywołania metod. Kiedy obiekt zdalny rejestruje się, podaje serwerowi rejestrującemu swoją nazwę i numer portu tak, aby klient mógł go zlokalizować (a zwłaszcza poznać numer jego portu) i porozumieć się z nim. Te dwa zadania są obsługiwane przez klasę RemoteHttpServlet, przedstawioną w przykładzie 10.13.

Przykład 10.13.

Superklasa RemoteHttpServlet

package com.oreilly.servlet;

import java.io.*;

import java.net.*;

import java.rmi.*;

import java.rmi.server.*;

import java.rmi.registry.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public abstract class RemoteHttpServlet extends HttpServlet

implements Remote {

protected Registry registry;

public void init(ServletConfig config) throws ServletException {

super.init(config);

try {

// Eksportowanie siebie

UnicastRemoteObject.exportObject(this);

// Rejestrowanie siebie

bind();

}

catch (RemoteException e) {

log("Problem binding to RMI registry: " + e.getMessage());

}

}

public void destroy() {

// Wyrejestrowanie siebie

unbind();

}

// Zwraca nazwę pod którą obiekt zostanie zarejestrowany

protected String getRegistryName() {

// Pierwszą wybieraną nazwą jest parametr inicjacji "registryName"

String name = getInitParameter("registryName");

if (name != null) return name;

// Drugim wyborem jest nazwa tej klasy

return this.getClass().getName();

}

// Zwraca port na którym nasłuchuje (lub powinien nasłuchiwać) serwer rejestrujący

protected int getRegistryPort() {

// Pierwszym wybieranym portem jest parametr inicjacji "registryPort"

try { return Integer.parseInt(getInitParameter("registryPort")); }

// Drugim wyborem jest domyślny port rejestracji (1099)

catch (NumberFormatException e) { return Registry.REGISTRY_PORT; }

}

protected void bind() {

// Próba znalezienia właściwego działającego już rejestru

try {

registry = LocateRegistry.getRegistry(getRegistryPort());

registry.list(); // Verify it's alive and well

}

catch (Exception e) {

// Couldn't get a valid registry

registry = null;

}

// Jeżeli go nie ma, należy go utworzyć

// (Ekwiwalent uruchomienia "rmiregistry")

if (registry == null) {

try {

registry = LocateRegistry.createRegistry(getRegistryPort());

}

catch (Exception e) {

log("Could not get or create RMI registry on port " +

getRegistryPort() + ": " + e.getMessage());

return;

}

}

// Jeżeli program dotarł do tego miejsca, to istnieje prawidłowy rejestr

// Teraz należy zarejestrować ten egzemplarz serwletu w tym rejestrze

// „Rebind” podmienia inne obiekty wykorzystujące tą nazwę

try {

registry.rebind(getRegistryName(), this);

}

catch (Exception e) {

log("Could not bind to RMI registry: " + e.getMessage());

return;

}

}

protected void unbind() {

try {

if (registry != null) registry.unbind(getRegistryName());

}

catch (Exception e) {

log("Problem unbinding from RMI registry: " + e.getMessage());

}

}

}

Osoby, które wcześniej wykorzystywały lub czytały o RMI prawdopodobnie napotkały zdalne obiekty rozszerzające klasę java.rmi.server.UnicastRemoteObject. Jest to standardowy — i właściwie polecany — sposób tworzenia zdalnego obiektu. Jednak klasa RemoteHttpServlet nie jest rozszerzeniem UnicastRemoteObject — rozszerza ona HttpServlet. Jak wiadomo, Java nie obsługuje wielodziedziczenia. Oznacza to, że RemoteHttpServlet musi wybrać, czy jest rozszerzeniem UnicastRemoteObject, czy HttpServlet — nawet, jeżeli potrzebuje funkcjonalności obu klas. Jest to trudny wybór. Niezależnie od tego, której klasy nie rozszerza, musi po prostu reimplementować ją samodzielnie. Ostatecznie rozszerzono HttpServlet, ponieważ łatwiej jest napisać ponownie funkcjonalność UnicastRemoteObject niż HttpServlet.

To ponowne napisanie wymaga, aby klasa RemoteHttpServlet wykonywała dwa działania, których nie musiałaby wykonywać, jeżeli byłaby rozszerzeniem UnicastRemoteObject. Musi ona zadeklarować, że rozszerza interfejs Remote. Wszystkie obiekty zdalne muszą wykorzystywać ten interfejs, ale zazwyczaj poprzez rozszerzanie UnicastRemoteObject, klasa otrzymuje to za darmo. Wysiłek związany z samodzielnym wykonaniem tego działania nie jest duży, jako że interfejs Remote właściwie nie definiuje żadnych metod. Obiekt deklaruje, że implementuje interfejs Remote, aby być traktowany jako obiekt zdalny.

Drugim działaniem, jakie musi wykonać RemoteHttpServlet jest ręczne wyeksportowanie siebie. Zazwyczaj jest to wykonywane automatycznie przez konstruktora UnicastRemoteObject(). Wykonanie tego bez konstruktora nie jest jednak problemem. Klasa UnicastRemoteObject posiada statyczną metodę exportObject(Remote), którą każdy obiekt Remote może wykorzystać w celu wyeksportowania siebie. RemoteHttpServlet wykorzystuje tę metodę i eksportuje siebie przy pomocy poniższej pojedynczej linii:

UnicastRemoteObject.exportObject(this);

Powyższe dwa kroki, implementacja Remote i eksportowanie siebie są wykonywane przez RemoteHttpServlet, ponieważ nie jest ona rozszerzeniem UnicastRemoteObject.

Pozostała część kodu RemoteHttpServlet wykonuje rejestrowanie i wyrejestrowanie siebie w rejestrze RMI. Jak powiedziano wcześniej, serwer rejestrujący RMI działa jako miejsce, w którym klienty mogą zlokalizować obiekty serwera. Zdalny obiekt (obiekt serwera) rejestruje siebie w rejestrze pod konkretną nazwą. Klienty mogą więc wyszukać ten obiekt według nazwy w rejestrze. W takim razie, aby być dostępny dla klientów, serwlet musi odnaleźć (lub stworzyć) serwer rejestrujący i zarejestrować się w tym serwerze pod konkretną nazwą. W języku specjalistycznym nosi to nazwę dowiązania do rejestru. RemoteHttpServlet wykonuje to dowiązanie przy pomocy swojej metody bind(), wywoływanej wewnątrz metody init().

Metoda bind() wykorzystuje dwie metody wspierające, getRegistryPort() i getRegistryName() w celu określenia portu, na którym powinien działać serwlet i nazwy, pod którą powinien on zostać zarejestrowany. W przypadku przykładowej implementacji, port jest określany przy pomocy parametru inicjacji registryPort, lub posiada domyślną wartość 1099. Nazwa jest pobierana z parametru inicjacji registryName lub posiada domyślną nazwę klasy serwletu — w tym przypadku SerwletGodziny.

Poniżej opisana jest dokładniej metoda bind(). Rozpoczyna ona działanie przez wykorzystanie następującego kodu, próbując odnaleźć właściwy działający już rejestr.

registry = LocateRegistry.getRegistry(getRegistryPort());

registry.list();

Pierwsza linia próbuje odnaleźć rejestr pracujący na danym porcie. Druga prosi rejestr o listę jego obecnie zarejestrowanych obiektów. Jeżeli oba wywołania zakończą się sukcesem, wynikiem będzie prawidłowy rejestr. Jeżeli dowolne wywołanie spowoduje wyjątek Exception, metoda bind() dowiaduje się, że nie istnieje prawidłowy rejestr i samodzielnie go tworzy. Wykonuje to działanie przy pomocy następującej linii kodu:

registry = LocateRegistry.createRegistry(getRegistryPort());

Po wykonaniu tych działań metoda bind() powinna znaleźć lub utworzyć serwer rejestrujący. Jeżeli pobranie rejestru nie powiodło się, a nie powiodło się też jego tworzenie, powraca ona i serwlet pozostaje niezarejestrowany. Następnie RemoteHttpServlet dowiązuje się do rejestru przy pomocy poniższej linii kodu:

registry.rebind(getRegistryName(), this);

Wykorzystuje ona metodę Registry.rebind() zamiast metody Registry.bind() aby wskazać, że to dowiązanie powinno zastąpić wszystkie poprzednie dowiązania wykorzystujące tę nazwę. Dowiązanie to trwa dopóki serwlet nie zostaje zniszczony, w którym to miejscu metoda destroy() RemoteHttpServlet wywołuje jego metodę unbind(). Kod wykorzystywany przez unbind() w celu zniszczenia dowiązania do rejestru jest niezwykle prosty:

if (registry != null) registry.unbind(getRegistryName());

Po prostu prosi ona rejestr o zniszczenie dowiązania do swojej nazwy.

Wskazówka!!!!

W którym miejscu uruchomić rejestr?

Szeroko akceptowanym sposobem uruchamiania serwera rejestrującego RMI jest samodzielny program Javy rmiregistry. Polecane jest jednak opuszczenie rmiregistry i pozwolenie RemoteHttpServlet na samodzielne utworzenie rejestru. Jest to łatwiejsze i bardziej wydajne. Pierwszy serwlet korzystający z rejestru może go utworzyć. Z powodu uruchamiania rejestru wewnątrz serwletu, rejestr pracuje przy pomocy tej samej JVM, co serwlet. Pozwala to na wykorzystanie tylko jednej wirtualnej maszyny Javy dla serwera, wszystkich jego serwletów (obiektów zdalnych) i rejestru. Niektóre serwery aplikacji na starcie uruchamiają rejestr wewnątrz własnej JVM.

Możliwe jest również całkowite ominięcie uruchomienia rejestru, przy pomocy serwletów. Klient może połączyć się ze specjalnym serwletem przy pomocy HTTP i zażądać danego obiektu zdalnego (przy pomocy parametru), a serwlet może zwrócić mu dane odpowiedzi jako zserializowaną końcówkę obiektu zdalnego — podobnie jak zwraca te dane rejestr. Zastosowanie serwletu zamiast rmiregistry pozwala serwletowi na zaimplementowanie polityki bezpieczeństwa ograniczającej prawa przeglądania obiektów, pozwala serwletowi na nadanie każdemu klientowi odwołania do różnych obiektów zdalnych oraz (ponieważ serwlet może tworzyć zdalny obiekt w ramach obsługi żądań) powoduje, że obiekt zdalny jest dostępny nawet przed jego utworzeniem.

Proszę zauważyć, że zdalny serwlet musi zostać załadowany na swój serwer, a jego metoda init() musi zostać wywołana, zanim będzie on gotowy do komunikacji RMI. Podobnie jak w przypadku serwletu-demona, należy nakazać serwerowi ładowanie go przy uruchamianiu lub upewnić się, że jest on zawsze dostępny przez HTTP zanim będzie dostępny bezpośrednio.

Aplet

Teraz należy odwrócić uwagę od serwera i skupić się na kliencie. Kod wykorzystywany przez ApletGodziny do wywołania metody GetDate() nowego SerwletGodziny() jest przedstawiony w przykładzie 10.14.

Przykład 10.14.

ApletGodziny pobierający czas przy pomocy RMI

import java.rmi.*; // Nowy dodatek

import java.rmi.registry.*; // Nowy dodatek

private String getRegistryHost() {

return getCodeBase().getHost();

}

private int getRegistryPort() {

try { return Integer.parseInt(getParameter("registryPort")); }

catch (NumberFormatException w) { return Registry.REGISTRY_PORT; }

}

private String getRegistryName() {

String nazwa = getParameter("registryName");

if (nazwa == null) {

nazwa = "SerwletGodziny"; // domyślnie

}

return nazwa;

}

private String pobierzDataRMIObiekt() {

try {

Registry rejestr =

LocateRegistry.getRegistry(getRegistryHost(), getRegistryPort());

SerwerGodziny godziny =

(SerwerGodziny)rejestr.lookup(getRegistryName());

return godziny.getDate().toString();

}

catch (ClassCastException w) {

System.out.println("Otrzymany obiekt to nie SerwerGodziny: " +

w.getMessage());

}

catch (NotBoundException w) {

System.out.println(getRegistryName() + " nie dowiązany: " + w.getMessage());

}

catch (RemoteException w) {

System.out.println("Wyjątek zdalny: " + w.getMessage());

}

catch (Exception w) {

System.out.println("Problem z pobraniem odwołania do SerwerGOdziny: " +

w.getClass().getName() + ": " + w.getMessage());

}

return null;

}

Pierwsze trzy metody to metody wspomagające. getRegistryHost() zwraca komputer, na którym powinien pracować serwer rejestrujący. Musi to zawsze być komputer, z którego pobrany został aplet. getRegistryPort() zwraca port, na którym powinien nasłuchiwać serwer rejestrujący. Zazwyczaj jest to domyślny port rejestru 1099, chociaż może on zostać przejęty przy pomocy parametru registryPort. getRegistryName() zwraca nazwę, pod którą serwlet powinien zostać zarejestrowany. Jej domyślna wartość to SerwletGodziny, lecz może ona zostać przejęta przy pomocy parametru registryName.

Właściwe poszukiwanie zdalnego obiektu serwletu i wywołanie jego metody getDate() następuje w poniższych trzech liniach metody pobierzDataRMIObiekt():

Registry rejestr =

LocateRegistry.getRegistry(getRegistryHost(), getRegistryPort());

SerwerGodziny godziny =

(SerwerGodziny)rejestr.lookup(getRegistryName());

return godziny.getDate().toString();

Pierwsza linia odnajduje rejestr dla danego komputera i danego portu. Druga linia wykorzystuje ten rejestr do wyszukania zdalnego obiektu zarejestrowanego pod daną nazwą, wysyłając ten obiekt do obiektu SerwerGodziny. Trzecia linia wywołuje metodę getDate() tego obiektu i otrzymuje w odpowiedzi zserializowany obiekt Data. Następnie, w tej samej linii, zwraca reprezentację łańcuchową (String) tego obiektu Data.

Pozostała część metody pobierzDataRMIObiekt() obsługuje mogące wystąpić wyjątki. Przejmuje ona wyjątek ClassCastException, jeżeli wywoływany obiekt to nie SerwerGodziny, NotBoundException jeżeli rejestr nie posiada obiektu zarejestrowanego pod tą nazwą, a RemoteException w przypadku, gdy występuje błąd w usługach sieciowych. Przejmuje także ogólny wyjątek Exception, w przypadku wystąpienia innych problemów.

Można się zastanawiać, dlaczego ApletGodziny wykorzystuje Registry.lookup(String) zamiast java.rmi.Naming.lookup(String) do pobrania swojego odwołania do zdalnego serwletu. Nie ma ku temu żadnego powodu — to tylko sprawa osobistego gustu. Aplet ten pracowałby podobnie po wymianie dwóch pierwszych linii pobierzDataRMIObiekt() na następujący kod:

SerwerGodziny godziny =

(SerwerGodziny)Naming.lookup("rmi:// + getRegistryHost() +

":" + getRegistryPort() +

"/" + getRegistryName());

To jest koniec piątej i ostatniej metody ApletGodziny. Proszę uruchomić teraz aplet. Czy wszystkie pola są wypełnione? Nie powinny być. Pola odpowiadające komunikacji przez port powinny być puste. Proszę przypomnieć sobie, że podczas zmiany SerwletGodziny na obiekt zdalny, usunięta została obsługa komunikacji przez port. Zostanie ona ponownie umieszczona poniżej.

W pełni funkcjonalny serwlet

Potrzebny jest pojedynczy serwlet dostępny przez komunikację HTTP, nie-HTTP i RMI. Serwlet tego typu może być rozszerzeniem nowej superklasy, com.oreilly.servlet.RemoteDaemonHttpServlet, wykorzystującym możliwości opisane dotychczas dla RemoteHttpServlet i DaemonHttpServlet.

Poniżej przedstawiony jest kod deklarujący ten w pełni funkcjonalny serwlet:

import java.io.*;

import java.net.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.oreilly.servlet.RemoteDaemonHttpServlet;

public class SerwletGodziny extends RemoteDaemonHttpServlet

implements SerwerGodziny {

public Data getDate() {

return new Data();

}

// Pozostała część bez zmian

Powyższy kod jest niemal identyczny jak przykład 10.8. Jest to po prostu tamten przykład napisany ponownie, aby zadeklarować, że rozszerza on RemoteDaemonHttpServlet i implementuje SerwerGodziny.

Kod superklasy RemoteDaemonHttpServlet również jest niemal identyczny jak RemoteHttpServlet. Występują jedynie dwie zmiany — rozszerza on DaemonHttpServlet zamiast HttpServlet, a jego metoda destroy() jako pierwsza wywołuje super.destroy():

package com.oreilly.servlet;

import java.io.*;

import java.net.*;

import java.rmi.*;

import java.rmi.server.*;

import java.rmi.registry.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public abstract class RemoteDaemonHttpServlet extends DaemonHttpServlet

implements Remote {

public void destroy() {

super.destroy();

unbind();

}

// Pozostała część bez zmian

W tym momencie ApletGodziny może połączyć się z poprawionym zdalnym serwerem-demonem i utworzyć pełny wynik przedstawiony wcześniej na rysunku 10.1.

Serwer pogawędek

Przykład serwera godziny z poprzedniego podrozdziału przedstawiał wady i zalety stosowania wszystkich trzech technik komunikacji aplet-serwer. Nie korzystał jednak z zalet utrzymywania połączenia w przypadku połączenia przez port. Nie przedstawiał również prostoty komunikacji RMI i elegancji wywołań wstecznych RMI (w których serwlet może wywoływać metody apletu). Nie przedstawiał poważnego powodu, dla którego jeden serwlet powinien obsługiwać wszystkie techniki komunikacji — nie istniał powód dla którego należało utrzymywać skomplikowany kod w jednym miejscu. Tak więc przed zakończeniem dyskusji na temat komunikacji aplet-serwlet przedstawiony zostanie przykład bardziej skomplikowany — serwer pogawędek (chat), zaimplementowany jako serwlet, który obsługuje klientów łączących się przez HTTP, porty nie-HTTP i RMI.

Ten serwer pogawędek zostanie utworzony przy pomocy wszystkich trzech technik komunikacji, aby mógł korzystać z zalet najlepszego, najbardziej wydajnego rozwiązania dla każdego klienta. Na przykład, kiedy klient obsługuje RMI, serwlet może być traktowany jako obiekt zdalny, a (gdzie to możliwe) może traktować aplet również jako zdalny obiekt. Kiedy klient nie obsługuje HTTP, ale obsługuje bezpośrednie połączenia przez port, serwer pogawędek może skorzystać z trwałości portów i łączyć się z klientem przy pomocy protokołu portu nie będącego HTTP. I, oczywiście, jeżeli inne techniki zawiodą, serwer pogawędek wykorzystać HTTP. Nie jest to polecane, ponieważ HTTP jako protokół bezstanowy wymaga od klienta aktywności w celu dokonania uaktualnień. Lecz dla wielu klientów HTTP jest jedynym możliwym rozwiązaniem.

Serwer pogawędek jest implementowany jako pojedyncza klasa z pojedynczym egzemplarzowaniem, ponieważ posiada on dużą ilość związanych ze sobą danych i sporą ilość kodu, który musiałby być powtarzany. Podzielenie go na trzy klasy, jedną dla każdego protokołu, wymagałoby nadmiernej komunikacji między nimi i trzykrotnego powtarzania podstawowego kodu serwera pogawędek. Implementacja serwera pogawędek jako serwletu dostarcza prostej metody udostępnienia jednego obiektu przy pomocy wszystkich trzech technik komunikacji. Jako że jest on serwletem HTTP, posiada wbudowaną obsługę HTTP. A dzięki temu, że jest rozszerzeniem klasy RemoteDaemonHttpServlet, może również łatwo uzyskać możliwość obsługi portów nie-HTTP i komunikacji RMI.

Proszę zauważyć, że chociaż kod zostanie przedstawiony w całości, nie zostanie wyjaśniona każda pojedyncza linia. Spowodowałoby to powiększenie niniejszego rozdziału poza rozsądny rozmiar, jeżeli się to już nie stało. Tak więc zostaną wyjaśnione kwestie ściśle związane z komunikacją aplet-serwlet, a analiza kodu i zrozumienie wszystkich szczegółów będą pozostawione Czytelnikowi.

Projekt

Rysunek 10.3 przedstawia aplet pogawędek w działaniu. Proszę zauważyć, że wykorzystuje on duży element TextArea w celu wyświetlenia toczącej się konwersacji. Mały element TextArea poniżej służy użytkownikowi do wpisywania i wysyłania jednolinijkowych wiadomości. Kiedy dowolny użytkownik wpisze wiadomość zostaje ona wysłana do serwera i rozprowadzona na różne sposoby do innych klientów.

0x01 graphic

Rysunek 10.3.

Aplet pogawędek w działaniu

Klienci HTTP wysyłają swoje wiadomości na serwer przy pomoc metody HTTP POST. Aplet pobiera nową wiadomość z elementu TextInput, kiedy użytkownik naciska Enter, koduje wiadomość w URL-u i wysyła ją do serwletu jako parametr wiadomości. Jest to działanie bardzo proste. Nieco bardziej skomplikowany jest sposób zarządzania pobieraniem wiadomości od innych użytkowników przez klienta pogawędek HTTP. Wykorzystuje on metodę HTTP GET do odebrania każdej wiadomości, występuje jednak pewien problem: nie wie on, kiedy pojawia się nowa wiadomość do odebrania. Jest to problem związany z paradygmatem jednokierunkowej komunikacji żądanie/odpowiedź. Klient musi albo od czasu do czasu wysłać jakąś wiadomość w celu uaktualnienia, lub symulować komunikację dwukierunkową przy pomocy serii blokujących żądań GET. Oznacza to, że klient pogawędek inicjuje żądanie GET, które działa blokująco, dopóki serwer nie zdecyduje, że nadszedł czas na zwrócenie jakiejś informacji. W niniejszym przykładzie zaimplementowana zostanie ta symulowana komunikacja dwukierunkowa.

Klienci uzyskujący dostęp przez port, w celu zachowania spójności, wysyłają swoje wiadomości w identyczny sposób jak klienci HTTP, przy pomocy metody HTTP POST. Mogą oni wysyłać swoje wiadomości przy pomocy połączeń przez zwykły port, lecz daje to jedynie minimalny zysk w wydajności, który, przynajmniej w tym przypadku, nie przeważa zwiększonej złożoności. Klienci korzystający ze zwykłych portów wykorzystują je jednak do odbierania wiadomości od innych użytkowników. W tym przypadku symulowana łączność dwukierunkowa zostaje zastąpiona przez komunikację prawdziwą. Kiedy dowolna nowa wiadomość jest odbierana przez serwlet, jest wysyłana bezpośrednio z serwletu do klientów korzystających z portów przy pomocy opartych na zwykłym tekście połączeń przez port.

Klienci pogawędek RMI wykonują swoje żądania POST i GET przy pomocy wywołań metod. Aby wysłać nową wiadomość, aplet po prostu wywołuje metodę zdalnego serwletu nadajWiadomosc(String). Aby odebrać nowe wiadomości, może zastosować jedną z dwóch opcji. Może wywołać blokującą serwlet metodę pobierzNastepnaWiadomosc() lub, przy pomocy wywołań wstecznych, może poprosić serwer o wywołanie swojej własnej metody ustawNastepnaWiadomosc(String) za każdym razem, kiedy nadana zostaje nowa wiadomość. W niniejszym przykładzie zastosowana zostanie technika wywołań wstecznych.

Pierwszym z wszystkich powyższych apletów jest aplet-dyspozytor. Pozwala on użytkownikowi na wybór techniki komunikacji aplet-serwlet (HTTP, port lub RMI), któryą chce on wykorzystać i, w oparciu o jego wybór, generuje stronę zawierającą właściwy aplet. Prawdą jest, że pojedynczy aplet mógłby obsługiwać wszystkie te trzy techniki i automatycznie wybierać między nimi w oparciu o środowisko uruchomieniowe, lecz działanie takie w tym miejscu jedynie niepotrzebnie skomplikowałoby przykład. Serwlet-dyspozytor podaje również apletowi nazwę jego użytkownika, zostanie to jednak opisane później.

Serwlet

Pełne wydruki interfejsu SerwerPogaw i klasy SerwletPogaw, która go wykorzystuje są przedstawione w przykładach 10.15 i 10.16.

Przykład 10.15.

Interfejs SerwerPogaw, wykorzystywany przez SerwletPogaw

import java.rmi.Remote;

import java.rmi.RemoteException;

public interface SerwerPogaw extends Remote {

public String pobierzNastepnaWiadomosc() throws RemoteException;

public void nadajWiadomosc(String message) throws RemoteException;

public void dodajKlient(KlientPogaw klient) throws RemoteException;

public void usunKlient(KlientPogaw klient) throws RemoteException;

}

Przykład 10.16.

W pełni funkcjonalny serwer-serwlet pogawędek

import java.io.*;

import java.net.*;

import java.rmi.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.oreilly.servlet.RemoteDaemonHttpServlet;

public class SerwletPogaw extends RemoteDaemonHttpServlet

implements SerwerPogaw {

// zrodlo działa jako dystrybutor nowych wiadomości

ZrodloWiadomosci zrodlo = new ZrodloWiadomosci();

// portKlienci przechowuje odwołania do wszystkich klientów łączących się przez

// porty

Vector portKlienci = new Vector();

// rmiKlienci przechowuje odwołania do wszystkich klientów łączących się przez RMI

Vector rmiKlienci = new Vector();

// doGet() zwraca następną wiadomość. Blokuje dopóki nie ona nie wystąpi.

public void doGet(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

odp.setContentType("text/plain");

PrintWriter wyj = odp.getWriter();

// Zwrócenie następnej wiadomości (blokowanie)

wyj.println(pobierzNastepnaWiadomosc());

}

// doPost() przyjmuje nową wiadomość i wysyła ją do wszystkich

// aktualnie nasłuchujących klientów łączących się przez HTTP i porty.

public void doPost(HttpServletRequest zad, HttpServletResponse odp)

throws ServletException, IOException {

// Przyjęcie wiadomości jako parametru „wiadomosc”

String wiadomosc = zad.getParameter("wiadomosc");

// Wysłanie jej do wszystkich nasłuchujących klientów

if (wiadomosc != null) nadajWiadomosc(wiadomosc);

// Ustawienie kodu stanu aby zaznaczyć, że nie będzie odpowiedzi

odp.setStatus(odp.SC_NO_CONTENT);

}

// pobierzNastepnaWiadomosc() zwraca następną nową wiadomość.

// Blokuje dopóki ona nie wystąpi.

public String pobierzNastepnaWiadomosc() {

// Stworzenie pojemnika na wiadomości czekającego na nową wiadomość ze źródła

// wiadomości

return new PojemnikWiadomosc().pobierzNastepnaWiadomosc(zrodlo);

}

// nadajWiadomosc() informuje wszystkich aktualnie nasłuchujących klientów że jest

// nowa wiadomość. Powoduje odblokowanie wszystkich wywołań do

// pobierzNowaWiadomosc().

public void nadajWiadomosc(String wiadomosc) {

// Wysłanie wiadomości do wszystkich klientów HTTP poprzez przekazanie wiadomości do

// źródła wiadomości

zrodlo.wyslijWiadomosc(wiadomosc);

// Bezpośrednie wysłanie wiadomości do wszystkich klientów połączonych przez port

Enumeration enum = portKlienci.elements();

while (enum.hasMoreElements()) {

Socket klient = null;

try {

klient = (Socket)enum.nextElement();

PrintStream wyj = new PrintStream(klient.getOutputStream());

wyj.println(wiadomosc);

}

catch (IOException w) {

// Problem z klientem, zamknięcie i oddalenie go

try {

if (klient != null) klient.close();

}

catch (IOException ignored) { }

portKlienci.removeElement(klient);

}

}

// Bezpośrednie wysłanie wiadomości do wszystkich klientów RMI

enum = rmiKlienci.elements();

while (enum.hasMoreElements()) {

KlientPogaw klientPogaw = null;

try {

klientPogaw = (KlientPogaw)enum.nextElement();

klientPogaw.ustawNastepnaWiadomosc(wiadomosc);

}

catch (RemoteException w) {

// Problem z komunikacją z klientem, zamknięcie go

usunKlient(klientPogaw);

}

}

}

protected int getSocketPort() {

// Nasłuchiwanie na porcie 2428 (proszę spojrzeć na telefon, dlaczego)

return 2428;

}

public void obslugaKlient(Socket klient) {

// Nowy klient łączący się przez port. Dodanie go do listy.

portKlienci.addElement(klient);

}

public void dodajKlient(ChatClient klient) {

// Nowy klient RMI. Dodanie go do listy.

rmiKlienci.addElement(klient);

}

public void usunKlient(ChatClient klient) {

// Usunięcie wybranego klienta z listy.

rmiKlienci.removeElement(klient);

}

}

// ZrodloWiadomosc działa jako źródło nowych wiadomości.

// Klienci zainteresowani przyjmowaniem nowych wiadomości mogą obserwować ten obiekt

class ZrodloWiadomosc extends Observable {

public void wyslijWiadomosc(String wiadomosc) {

setChanged();

notifyObservers(wiadomosc);

}

}

// PojemnikWiadomosc działa jako odbierający nowe wiadomości.

// Nasłuchuje źródła.

class PojemnikWiadomosci implements Observer {

String wiadomosc = null; // ustawienie według uaktual() I odczytanie według

// pobierzNastepnaWiadomosc()

// Wywoływany przez źródło wiadomości kiedy odbiera nową wiadomość

synchronized public void uaktual(Observable o, Object arg) {

// odebranie nowej wiadomości

wiadomosc = (String)arg;

// Obudzenie czekającego wątku

notify();

}

// Pobranie następnej wiadomości wysłanej ze źródła wiadomości

synchronized public String pobierzNastepnaWiadomosc(ZrodloWiadomosci zrodlo) {

// Powiedz źródłu, że ma powiadamiać o nowych wiadomościach

zrodlo.addObserver(this);

// Czekanie aż metod uaktual() odbierze wiadomość

while (wiadomosc == null) {

try { wait(); } catch (Exception ignored) { }

}

// Powiedz źródłu żeby przestało informować o nowych wiadomościach

zrodlo.deleteObserver(this);

// Zwrócenie otrzymanej wiadomości

// ale wcześniej wyczyszczenie zmiennej egzemplarza wiadomości

// aby uaktual() i pobierzNastepnaWiadomosc mogły zostać wywołane ponownie.

String wiadomoscKopia = wiadomosc;

wiadomosc = null;

return wiadomoscKopia;

}

}

Metody pobierzNastepnaWiadomosc() i nadajWiadomosc(String wiadomosc) są najbardziej interesującymi częściami SerwletPogaw. Metoda pobierzNastepnaWiadomosc zwraca następną wiadomość, kiedy ona nadejdzie, blokując port dopóki jej nie ma. Aby umożliwić to blokowanie, wykorzystuje ona klasy ZrodloWiadomosc i PojemnikWiadomosc. Bez wchodzenia w zbytnie szczegóły tych dwóch klas, można powiedzieć, że serwlet konstruuje nowy PojemnikWiadomosc i prosi pojemnik o pobranie następnej wiadomości ze źródła. Aby to wykonać, pojemnik rejestruje się jako obserwator źródła i wywołuje wait() w celu blokowania. Kiedy źródło otrzymuje nową wiadomość, pojemnik (będący obserwatorem) jest powiadamiany o zmianie przy pomocy wywołania jego metody uaktual(). Metoda PojemnikWiadomosc uaktual() zapamiętuje ostatnią wiadomość ze źródła w swojej zmiennej wiadomości, po czym wywołuje notify(). Powoduje to odblokowanie metody pobierzNastepnaWiadomosc() i zwrócenie wiadomości.

Metoda nadajWiadomosc() powiadamia wszystkich klientów, kiedy występuje nowa wiadomość. Klientów HTTP poprzez wysłanie wiadomości do ZrodloWiadomosci, innych klientów bezpośrednio poprzez przeglądanie list klientów. Dla każdego klienta połączonego przez port, wyświetla wiadomość do portu klienta. Dla każdego klienta RMI, wywołuje metodę ustawNastepnaWiadomosc(String). Jest to omawiane wcześniej wywołanie wsteczne. Jeżeli, w dowolnym momencie, występuje problem z klientem RMI lub łączącym się przez port, usuwa tego klienta z listy.

Dwie listy, portKlienci i rmiKlienci, zyskują elementy, kiedy klienci nadają wiadomość serwletowi. Kiedy klient łączy się przez port, wywoływana jest metoda serwletu obslugaKlient(Socket), a nowy klient jest dodawany do Vector portKlienci. Klienci RMI muszą dodać się do listy sami poprzez wywołanie metody serwletu dodajKlient(klientPogaw).

Metody doGet() i doPost() SerwletPogaw są właściwie małymi obwódkami metod pobierzNastepnaWiadomosc() i nadajWiadomosc(). Obwódka doGet() jest tak cienka, że niemal przezroczysta — doGet() wysyła jako odpowiedź jakikolwiek łańcuch String, który zostaje zwrócony przez pobierzNastepnaWiadomosc(). Obwódka doPost() jest nieco bardziej widoczna, Pobiera ona wysłaną wiadomość z parametru formularza danych POST wiadomosc, nadaje wiadomość poprzez przekazanie jej metodzie nadajWiadomosc() oraz ustawia kod stanu odpowiedzi na SC_NO_CONTENT, aby wskazać, że w odpowiedź nie posiada żadnej zawartości. Właściwie wykonywanie żądania GET jest równoznaczne z wywołaniem pobierzNastepnaWiadomosc(), a wykonywanie żądania POST jest równoznaczne z wywołaniem nadajWiadomosc(). Jedyny problem jest taki, że w projekcie HTTP istnieje mały odstęp czasu pomiędzy otrzymaniem wiadomości przez klienta i zażądaniem przez niego nowej, co może spowodować ominięcie pewnych wiadomości w sytuacji dużego obciążenia. Lepszy projekt (pozostawiony Czytelnikowi) wykorzystywałby obiekt HttpSession klienta w celu utworzenia kolejki oczekujących wiadomości.

Można zauważyć, że port połączenia, na którym nasłuchuje SerwletPogaw to 2428. Przejmowanie metody getSocketPort() w sposób taki, w jaki czyni to SerwletPogaw to prosty sposób ustawienia portu połączenia bez korzystania z parametru inicjacji.

Aplet HTTP

Kod pierwszego apletu, apletu pogawędek HTTP, jest przedstawiony w przykładzie 10.17.

Przykład 10.17.

Klient pogawędek wykorzystujący komunikację HTTP

import java.applet.*;

import java.awt.*;

import java.io.*;

import java.net.*;

import java.util.*;

import com.oreilly.servlet.HttpMessage;

public class ApletPogawHTTP extends Applet implements Runnable {

TextArea tekst;

Label etykieta;

TextField wpis;

Thread watek;

String uzyt;

public void init() {

// Sprawdzenie, czy aplet został pobrany bezpośrednio z systemu plików.

// Jeżeli tak, wyjaśnienie użytkownikowi, że musi on być pobrany z serwera w celu

// komunikacji z serwletami tego serwera

URL kodbazy = getCodeBase();

if (!"http".equals(kodbazy.getProtocol())) {

System.out.println();

System.out.println("*** Ups! ***");

System.out.println("Ten aplet musi być pobrany z serwera WWW.");

System.out.println("Proszę spróbować ponownie, tym razem ładując plik HTML");

System.out.println("zawierający ten serwlet jako");

System.out.println("\"http://serwer:port/plik.html\".");

System.out.println();

System.exit(1); // Działa jedynie z przeglądarką apletów

// Przeglądarki wyświetlają błąd i kontynuują

}

// Pobranie nazwy tego użytkownika z parametru apletu ustawionego przez serwlet

// Można o to po prostu spytać użytkownika, ale jest to przedstawienie formy

// komunikacji serwlet->aplet.

uzyt = getParameter("user");

if (uzyt == null) uzyt = "anonymous";

// konfiguracja interfejsu użytkownika...

// Na górze duże pole TextArea przedstawiające całą rozmowę.

// Poniżej opisane TextField przyjmujące wpisy tego użytkownika.

tekst = new TextArea();

tekst.setEditable(false);

etykieta = new Label("Powiedz coś: ");

wpis = new TextField();

wpis.setEditable(true);

setLayout(new BorderLayout());

Panel panel = new Panel();

panel.setLayout(new BorderLayout());

add("Centrum", tekst);

add("Dol", panel);

panel.add("Lewo", etykieta);

panel.add("Centrum", wpis);

}

public void start() {

watek = new Thread(this);

watek.start();

}

String pobierzNastepnaWiadomosc() {

String nastepnaWiadomosc = null;

while (nastepnaWiadomosc == null) {

try {

URL url = new URL(getCodeBase(), "/servlet/SerwletPogaw");

HttpMessage wiad = new HttpMessage(url);

InputStream in = wiad.sendGetMessage();

DataInputStream dane = new DataInputStream(

new BufferedInputStream(in));

nastepnaWiadomosc = dane.readLine();

}

catch (SocketException w) {

// Niemożliwe połączenie z komputerem macierzystym, zgłoszenie i oczekiwanie

// przed ponowną próbą

System.out.println("Połączenie z komputerem niemożliwe: " + w.getMessage());

try { Thread.sleep(5000); } catch (InterruptedException ignored) { }

}

catch (FileNotFoundException w) {

// Serwlet nie istnieje, zgłoszenie i oczekiwanie

System.out.println("Zasoby nie odnalezione: " + w.getMessage());

try { Thread.sleep(5000); } catch (InterruptedException ignored) { }

}

catch (Exception w) {

// Inny problem, zgłoszenie i oczekiwanie

System.out.println("Ogólny wyjątek: " +

w.getClass().getName() + ": " + w.getMessage());

try { Thread.sleep(1000); } catch (InterruptedException ignored) { }

}

}

return nastepnaWiadomosc + "\n";

}

public void run() {

while (true) {

tekst.appendText(pobierzNastepnaWiadomosc());

}

}

public void stop() {

watek.stop();

watek = null;

}

void nadajWiadomosc(String wiadomosc) {

wiadomosc = uzyt + ": " + wiadomosc; // Na początku dodanie nazwy użytkownika

try {

URL url = new URL(getCodeBase(), "/servlet/SerwletPogaw");

HttpMessage wiad = new HttpMessage(url);

Properties wlasc = new Properties();

wlasc.put("wiadomosc", wiadomosc);

wiad.sendPostMessage(wlasc);

}

catch (SocketException w) {

// Niemożliwe połączenie z komputerem macierzystym, zgłoszenie i zaprzestanie

// nadawania

System.out.println("Niemożliwe połączenie z komputerem: " + w.getMessage());

}

catch (FileNotFoundException w) {

// Serwlet nie istnieje, zgłoszenie i zaprzestanie nadawania

System.out.println("Zasoby nie odnalezione: " + w.getMessage());

}

catch (Exception w) {

// inny problem, zgłoszenie i zaprzestanie nadawania

System.out.println("Ogólny wyjątek: " +

w.getClass().getName() + ": " + w.getMessage());

}

}

public boolean obslugaWyjatek(Event wyjatek) {

switch (wyjatek.id) {

case Event.ACTION_EVENT:

if (wyjatek.target == wpis) {

nadajWiadomosc(wpis.getText());

wpis.setText("");

return true;

}

}

return false;

}

}

Powyższy aplet posiada dwie takie same metody robocze jak SerwletPogawpobierzNastepnaWiadomosc() i nadajWiadomosc(). Jego metoda pobierzNastepnaWiadomosc() pobiera następną wiadomość od serwletu. Jest ona wywoływana w celu uaktualnienia pola TextArea. Działa wykorzystując HttpMessage do wykonania żądania GET dla serwletu, po czym interpretuje pierwszą linie odpowiedzi jako następną nową wiadomość. Metoda apletu nadajWiadomosc() wysyła wiadomość do serwletu w celu udostępnienia jej pozostałym klientom. Metoda ta jest wywoływana w metodzie apletu obslugaWyjatek() za każdym razem, kiedy użytkownik naciśnie Enter w elemencie TextInput. Działa podobnie do pobierzNastepnaWiadomosc(). Wykorzystuje HttpMessage do wykonania żądania POST, przekazując tekst z TextInput jako parametr wiadomosc i nie zajmuje się odczytaniem odpowiedzi.

Aplet łączący się przez port

Jedyną różnicą pomiędzy łączącym się przez port ApletPogawPort i opartym na HTTP ApletPogawHTTP jest przeprojektowana metoda pobierzNastepnaWiadomosc(). Metoda ta przedstawiona jest w przykładzie 10.18.

Przykład 10.18.

Klient pogawędek wykorzystujący połączenie przez zwykły port

static final int PORT = 2428;

DataInputStream potokSerwer;

String pobierzNastepnaWiadomosc() {

String nastepnaWiadomosc = null;

while (nastepnaWiadomosc == null) {

try {

// Połączenie z serwerem jeżeli nie nastąpiło ono wcześniej

if (potokSerwer == null) {

Socket s = new Socket(getCodeBase().getHost(), PORT);

potokSerwer = new DataInputStream(

new BufferedInputStream(

s.getInputStream()));

}

// Odczytanie linii

nastepnaWiadomosc = potokSerwer.readLine();

}

catch (SocketException w) {

// Połączenie z komputerem niemożliwe, zgłoszenie i odczekanie przed kolejną próbą

System.out.println("Połączenie z komputerem niemożliwe: " + w.getMessage());

potokSerwer = null;

try { Thread.sleep(5000); } catch (InterruptedException ignored) { }

}

catch (Exception w) {

// Inny problem, zgłoszenie i odczekanie przed kolejną próbą

System.out.println("Ogólny wyjątek: " +

w.getClass().getName() + ": " + w.getMessage());

try { Thread.sleep(1000); } catch (InterruptedException ignored) { }

}

}

return nastepnaWiadomosc + "\n";

}

Powyższa metoda odczytuje nadane wiadomości z portu, który podłączony jest do serwletu pogawędek. Wykorzystuje ona prosty protokół portu — cała zawartość to zwykły tekst, jedna wiadomość na linię. Po pierwszym wywołaniu tej metody ustanawia ona połączenie z portem po czym wykorzystuje je do pobrania DataInputStream, przy pomocy którego może odczytywać z portu jedną linię za każdym razem. Przy każdym ponownym wywołaniu ponownie wykorzystuje ona ten sam potok i po prostu zwraca następną przeczytaną linię. Jeżeli kiedykolwiek wystąpi wyjątek SocketException, ponownie ustanawia ona połączenie.

Aplet RMI

Kod interfejsu KlientPogaw jest przedstawiony w przykładzie 10.19. Aplet pogawędek oparty na RMI, który wykorzystuje ten interfejs jest przedstawiony w przykładzie 10.20.

Przykład 10.19.

Interfejs KlientPogaw, wykorzystywany przez ApletPogawRMI

import java.rmi.Remote;

import java.rmi.RemoteException;

public interface KlientPogaw extends Remote {

public void pobierzNastepnaWiadomosc(String message) throws RemoteException;

}

Przykład 10.20.

Klient pogawędek wykorzystujący komunikację RMI

import java.applet.*;

import java.awt.*;

import java.io.*;

import java.net.*;

import java.rmi.*;

import java.rmi.registry.*;

import java.rmi.server.*;

import java.util.*;

public class ApletPogawRMI extends Applet implements KlientPogaw {

TextArea tekst;

Label etykieta;

TextField wpis;

Thread watek;

String uzyt;

SerwerPogaw serwerPogaw;

private int pobierzRejestrPort () {

try { return Integer.parseInt(getParameter("port")); }

catch (NumberFormatException ignored) { return Registry.REGISTRY_PORT; }

}

private String pobierzRejestrNazwa() {

String nazwa = getParameter("nazwa");

return (nazwa == null ? "SerwletPogaw" : nazwa);

}

// Zwraca odwołanie do zdalnej pogawędki serwer/serwlet

// Próbuje wyjść, jeżeli wystąpi problem

private SerwerPogaw pobierzSerwerPogaw() {

try {

Registry rejestr =

LocateRegistry.getRegistry(getCodeBase().getHost(), pobierzRejestrPort());

Object obi = rejestr.lookup(pobierzRejestrNazwa());

return (SerwerPogaw)obi;

}

catch (java.rmi.UnknownHostException w) {

// Nieznany komputer rejestru, próba wyjścia

System.out.println("Komputer nieznany, URL: " + w.getMessage());

System.exit(1);

}

catch (NotBoundException w) {

// Odnalezienie obiektu niemożliwe, próba wyjścia

System.out.println("Nazwa niedowiązana: " + w.getMessage());

System.exit(1);

}

catch (ClassCastException w) {

// Obiekt to nie SerwerPogaw, próba wyjścia

System.out.println(pobierzRejestrNazwa() + " to nie SerwerPogaw:" +

w.getMessage());

System.exit(1);

}

catch (RemoteException w) {

// Ogólny problem RMI, próba wyjścia

System.out.println("Wyjątek zdalny: " + w.getMessage());

System.exit(1);

}

catch (Exception w) {

// Inny problem, próba wyjścia

System.out.println("Ogólny wyjątek: " +

w.getClass().getName() + ": " + w.getMessage());

System.exit(1);

}

return null; // zwraca null, jeżeli exit() nie powiedzie się

}

// Dodanie siebie jako klienta serwera pogawędek

// Proszę zauważyć, że rejestr RMI nie jest potrzebny

private void rejestrujSerwerPogaw(SerwerPogaw serwer) {

try {

UnicastRemoteObject.exportObject(this);

serwer.dodajKlient(this);

}

catch (RemoteException w) {

// Ogólny problem RMI, próba wyjścia

System.out.println("Wyjątek zdalny: " + w.getMessage());

System.exit(1);

}

catch (Exception w) {

// Inny problem, próba wyjścia

System.out.println("Ogólny wyjątek: " +

w.getClass().getName() + ": " + w.getMessage());

System.exit(1);

}

}

public void init() {

// Sprawdzenie, czy aplet został pobrany bezpośrednio z systemu plików.

// Jeżeli tak, wyjaśnienie użytkownikowi, że musi on być pobrany z serwera w celu

// komunikacji z serwletami tego serwera

URL kodbazy = getCodeBase();

if (!"http".equals(kodbazy.getProtocol())) {

System.out.println();

System.out.println("*** Ups! ***");

System.out.println("Ten aplet musi być pobrany z serwera WWW.");

System.out.println("Proszę spróbować ponownie, tym razem ładując plik HTML");

System.out.println("zawierający ten serwlet jako");

System.out.println("\"http://serwer:port/plik.html\".");

System.out.println();

System.exit(1); // Działa jedynie z przeglądarką apletów

// Przeglądarki wyświetlają błąd i kontynuują

}

// Pobranie zdalnego serwera pogawędek

serwerPogaw = pobierzSerwerPogaw();

// Zarejestrowanie siebie jako jednego z klientów

rejestrujSerwerPogaw(serwerPogaw);

// Pobranie nazwy tego użytkownika z parametru apletu ustawionego przez serwlet

// Można o to po prostu spytać użytkownika, ale jest to przedstawienie formy

// komunikacji serwlet->aplet.

uzyt = getParameter("user");

if (uzyt == null) uzyt = "anonymous";

// konfiguracja interfejsu użytkownika...

// Na górze duże pole TextArea przedstawiające całą rozmowę.

// Poniżej opisane TextField przyjmujące wpisy tego użytkownika.

tekst = new TextArea();

tekst.setEditable(false);

etykieta = new Label("Powiedz coś: ");

wpis = new TextField();

wpis.setEditable(true);

setLayout(new BorderLayout());

Panel panel = new Panel();

panel.setLayout(new BorderLayout());

add("Centrum", tekst);

add("Dol", panel);

panel.add("Lewo", etykieta);

panel.add("Centrum", wpis);

}

String pobierzNastepnaWiadomosc() {

String nastepnaWiadomosc = null;

while (nastepnaWiadomosc == null) {

try {

nastepnaWiadomosc = serwerPogaw.pobierzNastepnaWiadomosc();

}

catch (RemoteException w) {

// Wyjątek zdalny, zgłoszenie i odczekanie przed następną próbą

System.out.println("wyjątek zdalny:" + w.getMessage());

try { Thread.sleep(1000); } catch (InterruptedException ignored) { }

}

}

return nastepnaWiadomosc + "\n";

}

public void ustawNastepnaWiadomosc(String wiadomosc) {

tekst.appendText(wiadomosc + "\n");

}

void nadajWiadomosc(String wiadomosc) {

wiadomosc = uzyt + ": " + wiadomosc; // Dodanie na początku nazwy klienta

try {

serwerPogaw.nadajWiadomosc(wiadomosc);

}

catch (RemoteException w) {

// Wyjątek zdalny zgłoszenie i zaprzestanie nadawania

System.out.println("Wyjatek zdalny: " + w.getMessage());

}

catch (Exception w) {

// Inny wyjątek, zgłoszenie i zaprzestanie nadawania

System.out.println("Ogólny wyjątek: " +

w.getClass().getName() + ": " + w.getMessage());

}

}

public boolean obslugaWyjatek(Event wyjatek) {

switch (wyjatek.id) {

case Event.ACTION_EVENT:

if (wyjatek.target == wpis) {

nadajWiadomosc(wpis.getText());

wpis.setText("");

return true;

}

}

return false;

}

}

Implementacje pobierzNastepnaWiadomosc() i nadajWiadomosc() są najprostsze z możliwych. Muszą one jedynie wywołać metody zdalnego serwletu o tych samych nazwach. Lecz ich prostota ma swoją cenę — bardziej skomplikowany kod konfiguracji. Konkretnie, metoda init() musi teraz wywołać stosunkowo długą (ale zrozumiałą w tym momencie) pobierzSerwerPogaw() w celu pobrania odwołania do zdalnego serwera pogawędek.

Po dokładniejszym przyjrzeniu się apletowi ApletPogawRMI można dostrzec, że właściwie nie wykorzystuje on swojej metody pobierzNastepnaWiadomosc(). Zamiast tego, prosi serwlet o wywołanie metody pobierzNastepnaWiadomosc() za każdym razem, kiedy nadana zostaje nowa wiadomość. ApletPogawRMI wykonuje to żądanie w swojej metodzie init(), kiedy wywołuje rejestrujSerwerPogaw(SerwerPogaw). Metoda ta eksportuje aplet jako zdalny obiekt, po czym wywołuje metodę serwletu addClient() przekazując jej odwołanie do siebie. Następnie metoda serwletu nadajWiadomosc() wysyła wywołanie zwrotne do apletu za każdym razem, kiedy pojawia się nowa wiadomość.

Podczas stosowania wywołań zwrotnych na własną rękę, należy pamiętać o opisanych wcześniej podstawach. Należy uruchomić kompilator RMI rmic na zdalnym aplecie w celu utworzenia jego klas końcówki i szkieletu. Należy również upewnić się, że serwer posiada pliki ApletPogawRMI.class i KlientPogaw.class gdzieś w swojej ścieżce klas.

Należy uważać także na fakt, ze współczesne przeglądarki są o wiele bardziej restrykcyjne w kwestii pozwalania na wywołania zwrotne niż przeglądarki wykorzystywane w przeszłości i często właściwość ta jest w nich wyłączona. W takim wypadku można zobaczyć jedynie zakodowane komunikaty o błędach, a czasami po prostu pusty ekran. Wywołania zwrotne mogą zostać włączone poprzez dołączenie do apletu cyfrowego podpisu (działanie to różni się w zależności od przeglądarki) i nadanie podpisanym apletom dodatkowych uprawnień lub przez uzyskanie dostępu do apletu przy pomocy HTTPS z zaufanego serwera. Generalnie, może to być uważane za kolejną wadę RMI i kolejny powód, by wykorzystywać HTTP do komunikacji aplet-serwer.

Dyspozytor

Ostatni przykład kodu w niniejszym rozdziale, serwlet DyspozytorPogaw przedstawiony jest w przykładzie 10.21. Serwlet ten posiada dwa obowiązki. Po pierwsze, kiedyn jest wywoływany bez żadnych parametrów żądania, wyświetla on przyjazną stronę powitalną, zapytującą użytkownika o wersje apletu, której chciałby on użyć, jak przedstawiono na rysunku 10.4. Po drugie, kiedy wywoływany jest z parametrem żądania, wyświetla stronę zawierającą właściwy aplet, jak przedstawiono wcześniej na rysunku 10.3. Proszę pamiętać, że URL wykorzystywany do uzyskania dostępu do serwletu-dyspozytora powinien zawierać prawdziwą nazwę serwera, nie localhost, w celu uniknięcia problemów bezpieczeństwa RMI.

Rysunek 10.4.

Strona powitalna dyspozytora pogawędek

Przykład 10.21.

Powitalny serwlet-dyspozytor

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class DyspozytorPogaw extends HttpServlet {

public void doGet(HttpServletRequest zad, HttpServletResponse odp)

throws IOException, ServletException {

odp.setContentType("text/html");

if (!zad.getParameterNames().hasMoreElements()) {

// Nie było parametrów żądania. Wyświetlenie strony powitalnej.

wyswietlStronaPowitalna(zad, odp);

}

else {

// Wystapił przynajmniej jeden parametr żądania.

// Wyświetl stronę apletu.

wyswietlApletStrona(zad, odp);

}

}

// Strona powitalna wita użytkownika i wyświetla formularz, w którym użytkownik może

// wybrać metodę komunikacji aplet-serwlet

private void wyswietlStronaPowitalna(HttpServletRequest zad,

HttpServletResponse odp)

throws IOException {

PrintWriter wyj = odp.getWriter();

String ja = zad.getServletPath();

wyj.println("<HTML>");

wyj.println("<HEAD><TITLE>");

wyj.println("Witamy w Absurdalnie Prostej Pogawędce");

wyj.println("</TITLE></HEAD>");

wyj.println();

wyj.println("<BODY>");

wyj.println("<H1> Witamy w Absurdalnie Prostej Pogawędce </H1>");

wyj.println();

wyj.println("Proszę wybrać formę komunikacji:");

wyj.println("<UL>");

wyj.println(" <LI><A HREF=\"" + ja + "?metoda=http\">http</A>");

wyj.println(" <LI><A HREF=\"" + ja + "?metoda=port\">port</A>");

wyj.println(" <LI><A HREF=\"" + ja + "?metoda=rmi\">rmi</A>");

wyj.println("</UL>");

wyj.println("</BODY></HTML>");

}

// Strona apletu wyświetla aplet pogawędek.

private void wyswietlApletStrona(HttpServletRequest zad,

HttpServletResponse odp)

throws IOException {

PrintWriter wyj = odp.getWriter();

wyj.println("<HTML>");

wyj.println("<HEAD><TITLE>Absurdalnie Prosta Pogawędka</TITLE></HEAD>");

wyj.println("<BODY>");

wyj.println("<H1> Absurdalnie Prosta Pogawędka </H1>");

String metoda = zad.getParameter("metoda");

String uzyt = zad.getRemoteUser();

String aplet = null;

if ("http".equals(metoda)) {

aplet = "ApletPogawHTTP";

}

else if ("port".equals(metoda)) {

aplet = "ApletPogawPort";

}

else if ("rmi".equals(metoda)) {

aplet = "ApletPogawRMI";

}

else {

// Brak podanej metody lub metoda nieprawidłowa.

// Wyjaśnienie użytkownikowi oczekiwań.

wyj.println("Przepraszamy, ten serwlet potrzebuje parametru <TT>metoda</TT> " +

"o jednej wartości jednej z poniższych: " +

"http, port, rmi");

return;

}

// Wyświetlenie kodu HTML wyświetlającego aplet.

// Wybranie kodu apletu oparte na parametrze metoda.

// Dostarczenie parametru użytkownik, jeżeli użytkownik jest znany.

wyj.println("<APPLET CODE=" + aplet + " CODEBASE=/ " +

"WIDTH=500 HEIGHT=170>");

if (uzyt != null)

wyj.println("<PARAM NAME=uzyt VALUE=\"" + uzyt + "\">");

wyj.println("</APPLET>");

wyj.println("</BODY></HTML>");

}

}

W powyższym kodzie nie występuje nic zaskakującego. Właściwie kod ten powinien wyglądać odświeżająco prosto po przykładzie SerwletPogaw. Przykład ten przedstawia jednak ostatnią formę komunikacji aplet-serwlet — generowane przez serwlet parametry apletu. Przy pomocy tej techniki serwlet generuje stronę zawierającą aplet i przekazuje apletowi informacje poprzez manipulację znacznikami apletu <PARAM>. Każda informacja, jaką serwlet chce przesłać nowemu apletowi, może być wysłana w ten właśnie sposób. W powyższym przykładzie, serwlet wysyła nazwę zwróconą przez odp.getRemoteUser(). W innym przykładzie serwlet mógłby przekazać apletowi jego typ przeglądarki poprzez wysłanie mu łańcucha zwróconego przez req.getHeader("User-Agent"). Lub, aby okazać się bardziej pomocnym, serwlet mógłby wykorzystać bazę danych w celu określenia możliwości przeglądarki i przekazać apletowi dokładne informacje. Mógłby nawet przekazać apletowi wiadomość z pytaniem, czy przeglądarka obsługuje komunikację RMI.

Można zastanawiać się, skąd sam serwer uzyskał informacje na temat cen. Dla celów tego przykładu należy przyjąć, że była to magia

Dokładny opis właściwości systemu potrzebnych aplikacji-klientowi RMI do przejścia przez firewalle znajduje się w 42 podpowiedzi Javy Johna D. Mitchela w JavaWorld pod adresem http://www.javaworld.com/javaworld/javatips/jw-javatip42.html (niewspomniane w artykule, a bardzo ważne są również własności socksProxySet, socksProxyHost i socksProxyPort konieczne dla proxy opartych na SOCKS). Wszystkie te właściwości systemu powinny być automatycznie ustawiane przez przeglądarkę WWW, lecz niestety obecnie niewiele przeglądarek to robi, pozostawiając swoje aplety bez sposobu określenia właściwych ustawień i bez możliwości wykorzystanie RMI przez firewall.

Tak właściwie można pozostawić wyłączenie buforowania serwletowi, przez ustawienie nagłówka serwletu Pragma na no-cache, Nie zaszkodzi jednak wyłączenie jej również w aplecie.

Zazwyczaj kiedy aplet wywoła nieobsługiwany wyjątek, przeglądarka zapisuje ścieżkę do wyjątku w konsoli Javy. Ten kod zawiera aplet naprawdę wyrzucający wyjątek na serwer, gdzie może być przechowywany w dzienniku zdarzeń. W dzienniku tym zapisywane są ścieżki, mogące być później przeglądane przez twórcę apletu

Nazwa daemon (demon) został wprowadzona w odniesieniu do demonów Uniksa, programów działających w tle i obsługujących konkretne zdarzenia. W jaki sposób otrzymały one nazwę demonów? Według słownika „New Hacker's Dictionary”, początkowo pochodziło „ze znaczenia mitologicznego, później zracjonalizowanego do akronimu 'Disk And Execution Monitor' (Monitor Dysku i Wykonania)”.

Zarządzanie wieloma plikami klas może stać się poważnym utrudnieniem przy tworzeniu oprogramowania. W systemie Uniksowym można wykorzystać łącza symboliczne w celu ułatwienia tego zadania. W każdym systemie, można zastosować bardziej ogólne rozwiązanie — zmienić ścieżkę klas serwera tak, aby zawierała ona katalog_macierzysty/webapps/ROOT/classes. (Proszę zauważyć, że nie jest to WEB_INF/classes). W tym miejscu należy pozostawić klasy końcówki i szkieletu. Następnie serwer może je odnaleźć w nowej ścieżce klas, a baza kodu apletu może zostać ustawiona na /classes, aby aplet również mógł je odnaleźć.

Aby być całkowicie poprawnym, należy wykonać jeszcze dodatkowe działania. Według dokumentacji java.rmi.remoteUnicastRemoteObject, „Jeżeli UnicastRemoteObject nie jest rozszerzana, klasa implementująca musi przejąć odpowiedzialność za poprawną semantykę metod hashCode, equals i toString dziedziczonych z klasy Object tak, aby zachowywały się one poprawnie w przypadku obiektów zdalnych”. Według dokumentacji java.rmi.remoteRef, „Metody te powinny zagwarantować, że końcówki obiektów zdalnych będą posiadać ten sam kod asocjacyjny (w celu obsługi obiektów zdalnych jako kluczy w tablicach asocjacyjnych).” Implementacja mechanizmów obsługujących tę gwarancję jest stosunkowo trudna i, według autorów, nie zawsze konieczna do komunikacji aplet-serwlet; w związku z tym podjęto ryzyko wykorzystania RemoteHttpServlet

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

2 C:\0-praca\Java Servlet - programowanie. Wyd. 2\r10-rysunki.doc

rys 10.1 — z płyty, ale uwaga do RedProw: kod spolszczony!!!!!



Wyszukiwarka

Podobne podstrony:
r12-05, Programowanie, ! Java, Java Server Programming
r20-05, Programowanie, ! Java, Java Server Programming
O Autorach-05, Programowanie, ! Java, Java Server Programming
r05-05, Programowanie, ! Java, Java Server Programming
r07-05, Programowanie, ! Java, Java Server Programming
r03-05, Programowanie, ! Java, Java Server Programming
rE-05, Programowanie, ! Java, Java Server Programming
r19-05, Programowanie, ! Java, Java Server Programming
r17-05, Programowanie, ! Java, Java Server Programming
r11-05, Programowanie, ! Java, Java Server Programming
rD-05, Programowanie, ! Java, Java Server Programming
rF-05, Programowanie, ! Java, Java Server Programming
r04-05, Programowanie, ! Java, Java Server Programming
r13-05, Programowanie, ! Java, Java Server Programming
r01-05, Programowanie, ! Java, Java Server Programming
r02-05, Programowanie, ! Java, Java Server Programming
rB-05, Programowanie, ! Java, Java Server Programming
r12-05, Programowanie, ! Java, Java Server Programming
05 Programowanie

więcej podobnych podstron