Programowanie w języku JAVA (część I) 

Programowanie sieciowe w języku JAVA jest bardzo obszernym tematem i wiele jego aspektów daleko wykracza poza zakres tego kursu. Wśród znaczących technologii, którymi nie będziemy się zajmować, ale o których istnieniu warto wiedzieć są RMI (ang. Remote Method Invocation), CORBA (Common Object Request Broker Architecture) czy serwlety . Skoncentrujemy się na komunikacji klient-serwer i P2P przy wykorzystaniu zarówno dostępnych w JAVAie w postaci gotowych klas protokołów jak i własnych implementacji. Korzystając z JAVAy podepniemy się także do przykładowych usług sieciowych takich jak wyszukiwarki internetowe.

Przygotowanie środowiska pracy i pierwszy projekt

Do programowania wykorzystywane będzie środowisko Eclipse (Eclipse IDE for Java Developer – http://www.eclipse.org/downloads/). Wszystkie przykłady zostaną przetestowane z wykorzystaniem wersji JDK 6 Update 16. Ze względu na bardzo niewielkie zmiany w klasach sieciowych przykładowe programy powinny także działać na wcześniejszych wersjach, ale dobrym zwyczajem jest używanie w miarę nowej wersji środowiska. Po instalacji środowiska deweloperskiego i jego uruchomieniu pojawi się ekran pokazany na rys. 1.

Nowy projekt tworzy się wybierając z menu opcję File/New/Java Project. Następnie należy podać nazwę projektu, wybrać wersję wirtualnej maszyny, katalog roboczy i sposób organizacji plików i kliknąć Finish (rys.2). W ramach nowozałożonego projektu można już tworzyć nowe klasy. W tym celu należy kliknąć prawym klawiszem na nazwie pakietu (w lewym oknie), z dostępnych opcji wybrać New a następnie Class (rys.3). Po podaniu nazwy klasy, wyborze odpowiednich opcji i kliknięciu przycisku Finish Eclipse tworzy nową klasę i odpowiadający jej plik na dysku.

Model OSI

Rys.1. Główny ekran środowiska programistycznego Eclipse.


Model OSI

Rys.2. Tworzenie nowego projektu w środowisku Eclipse


Model OSI

Rys.3. Tworzenie nowej klasy.

Dostęp do zasobów internetowych z wykorzystaniem URLa

Praktycznie wszystkie klasy związane realizacją komunikacji sieciowej zostały umieszczone w pakiecie java.net, który jest dostępny jako część JDK (Java SE Development Kit). Jedną z klas dostępnych w tym pakiecie jest URL (ang. Unified Resource Locator). Służy ona do przechowywania i modyfikacji lokalizatora zasobów internetowych w JAVAie. Najprostszy konstruktor wymaga podania jako parametru obiektu typu String zawierającego poprawnie sformatowany adres URL.

try {
    URL url =  new URL("http://finanse.wp.pl/gid,11602751,galeria.html");
} catch (MalformedURLException e) {
    System.out.println(e.getMessage());
}

Jak można zauważyć konstruktor klasy URL w przypadku parametru, który spełnia wymogi jeśli chodzi o typ, ale już nie w kwestii formatu wyrzuca wyjątek java.net.MalformedURLException. Stworzony obiekt udostępnia wiele pożytecznych metod do manipulowania identyfikatorem zasobów. Wśród nich są getHost(), getFile(), getPort(), getQuery() czy getPath(), które zwracają w postaci obiektu typu String odpowiednie części adresu URL. Poszczególne częsci składowe pokazane są poniżej.

protocol://username@hostname:port/path/filename#fragment?query

Już stworzony obiekt typu URL umożliwia nawiązanie połączenia z zasobem znajdującym się na serwerze. W tym celu należy wykorzystać metodę openConnection() lub openConnection(Proxy proxy) jeśli chcemy nawiązać połączenie poprzez serwer Proxy.

URLConnection urlConn = url.openConnection();

W zależności od zasobu na który wskazuje lokalizator URL obiekt, który zostanie zwrócony po wywołaniu metody openConnection() może być instancją bardziej specjalizowanej klasy takiej jak java.net.HttpUrlConnection (dla protokołu HTTP) lub java.net.JarUrlConnection (dla plików JAR). Udostępniają one dodatkowe funkcje przydatne dla obsługi konkretnego typu zasobów. Wśród użytecznych metod klasy realizującej połączenie dla protokołu HTTP są: setFollowRedirects(boolean set) determinujący zachowanie się obiektu w przypadku napotkania kodu 3xx i żądania przekierowania i getResponseCode() zwracający kod uzyskanej odpowiedzi (same kody informacji i błędów są dostępne jako stałe zdefiniowane w klasie HttpUrlConnection).

Większość metod kluczowych dla obsługi połączenia jest jednak dziedziczona z klasy URLConnection. Wśród nich zanim nawiąże się połączenie warto przyjrzeć się następującym:

  1. setConnectTimeout(int timeout) – ustawianie limitu czasu w milisekundach, którego przekroczenie przy nawiązywaniu połączenia spowoduje wyrzucenie wyjątku,
  2. setReadTimeout(int timeout)> – ustawienie limitu czasu w milisekundach dla odczytywania danych,
  3. setUseCaches(boolean usecaches) – wybór, czy połączenie ma kożystać z cache’u,

Protokół HTTP pozwala (poza wysłaniem komunikatu GET) na ustawienie wielu parametrów zapytania takich jak akceptowane formaty, czy identyfikator systemy wysyłającego zapytanie (User-Agent). W JAVAie ustala się te wartości wykorzystując metodę setRequestProperty(String key, String value). Ustawienie User-Agent a także akceptowanego kontentu jest istotne ponieważ część serwerów WWW nie odpowiada na żądania odpowiednich wartości przypisanych do tych kluczy. Sposób ustawienia przykładowych wartości znajduje się poniżej:

urlConn.setRequestProperty("User-Agent", "Lynx/2.4 libwww/2.1.4");
urlConn.setRequestProperty("Accept", "text/html, text/plain");

Po ustawieniu odpowiednich wartości dla zapytania możemy otworzyć połączenie wywołując metodę connect() dla obiektu klasy HttpUrlConnection. Kolejnym krokiem jest wywołanie metody getInputStream() zwracającej obiekt typu InputStream. Umiejętność posługiwania się strumieniami przynajmniej na podstawowym poziomie jest niezbędna do sprawnego korzystania z funkcji sieciowych udostępnianych przez JAVAe. Dokładny i wyczerpujący opis zależności i specyfiki poszczególnych strumieni można znaleźć w [1]. Na potrzeby naszych aplikacji będziemy korzystać głównie z Buffered(InputOutput)Stream|, GZIP(InputOutput)Stream| i InputStreamReader.

Pierwszy z nich służy do wysyłania i odbierania ciągu bajtów przy jednoczesnym buforowaniu komunikacji w obie strony. Klasa ta czeka z wysłaniem danych do czasu aż uzna, że nazbierała się ich odpowiednia ilość. Dlatego przy jej wykorzystaniu należy pamiętać o wykonywaniu metody flush(), która wymusza wypchnięcie z bufora wszystkich danych. Druga klasa służy do kompresowania i rozkompresowywania przesyłanych danych (jeśli wśród akceptowanych formatów ustawimy zip to niektóre serwery prześlą nam dane w postaci skompresowanej). Ostatni strumień zajmuje się konwersją otrzymanych danych na tekst w formacie Unicode.

Poniżej pokazany jest prosty przykład programu łączącego się z serwerem WWW z wykorzystaniem protokołu HTTP ściągający stronę główną znajdującą się pod adresem www.onet.pl i wyświetlający ją na ekranie

try {
	URL url = new URL("http://www.onet.pl/");
	        URLConnection urlConn = url.openConnection();
	        BufferedReader in = new BufferedReader(
	                                new InputStreamReader(
	                                urlConn.getInputStream()));
  String inputLine;

	while ((inputLine = in.readLine()) != null) 
		System.out.println(inputLine);
	in.close();

} catch (Exception e) {
  System.out.println(e.getMessage());
}

Bardzo wiele usług sieciowych jest udostępnianych poprzez protokół HTTP i odpowiednio spreparowane wywołania GET (najczęściej poprzez podanie parametrów jako część adresu URL). Przykładowo chcąc uzyskać od serwera Google wyniki wyszukiwania dla frazy PJWSTK należy w przykładzie przedstawionym powyżej zamienić adres URL na:

http://www.google.pl/search?hl=pl&q=pjwstk&lr=&aq=f&oq=

Jeśli jednocześnie nie ustawi się poprawnych wartości zmiennych User-Agent i Accepted to serwer zwróci błąd 403. Natomiast poprawne ustawienie tych zmiennych skutkuje wyświetleniem strony HTML zawierającej wyniki z wyszukiwarki Google. Wykorzystywanie wyników z google’a uzyskanych w taki sposób w ramach swojej aplikacji jest sprzeczne z warunkami ich użycia publikowanymi na stronie Internetowej www.google.com ale istnieje wiele serwisów udostępniających wyniki wyszukiwania bez takich restrykcji. Jednym z ciekawszych jest udostępniany przez Yahoo silnik BOSS ( http://developer.yahoo.com/search/boss/).

Dane z obiektu URLConnection można nie tylko czytać, ale także do niego zapisywać. Ta funkcjonalność jest przydatna, gdy chcemy z poziomu JAVAy wypełnić jakiś formularz na stronie. Fragment kodu realizujący tą operację poniżej:

URLConnection connection = url.openConnection();
connection.setDoOutput(true);
OutputStreamWriter out = 
new OutputStreamWriter(connection.getOutputStream());
out.write("string=" + stringToReverse);
out.close();

Warto zwrócić także uwagę na to, że obiekty URL i URLConnection swoją użytecznością wykraczają poza protokół HTTP. Jeśli wiemy jaki plik chcemy ściągnąć z serwera FTP to po jego podaniu w URLu (wraz z nazwą użytkownika i hasłem) także możemy skorzystać z nich skorzystać. Przykładowy program znajduje się poniżej:

URL url = 
    new URL("ftp://username:password@ftp.server.com/file.zip;type=i");
URLConnection con = url.openConnection();
BufferedInputStream in = 
    new BufferedInputStream(con.getInputStream());
FileOutputStream out = 
    new FileOutputStream("C:\\file.zip");

int i = 0;
byte[] bytesIn = new byte[1024];
while ((i = in.read(bytesIn)) >= 0) {
	out.write(bytesIn, 0, i);
}
out.close();
in.close();

Wysyłanie poczty z poziomu języka JAVA

Rozszerzeniem standardowego pakietu klas J2SE jest JavaMail API (dostępne jako oddzielny pakiet http://java.sun.com/products/javamail/index.jsp lub jako część J2EE). Ten zbiór klas pozwala na wysyłanie i odbieranie emaili niezależnie (przynajmniej w teorii) od użytego serwera i protokołu. Podstawową klasą wykorzystywaną w większości operacji jest Session. Przykładowy sposób uzyskania obiektu tej klasy pokazany jest poniżej:

Session session = Session.getDefaultInstance(props,
  new javax.mail.Authenticator() {
    protected PasswordAuthentication getPasswordAuthentication() {
      return new PasswordAuthentication("mojemail@gmail.com", "moje_haslo");
    }
  }
);

Choć na pierwszy rzut oka wygląda to dość skomplikowanie to zaraz stanie się jasne co dzieje się poszczególnych linijkach. Metoda getDefaultInstance wymaga dwóch argumentów, z których jeden jest typu java.util.Properties a drugi javax.mail.Authentication. Pierwszy z nich zawiera wszelkie ustawienia dotyczące adresu serwera, sposobu autentykacji czy portów. Przykładowe ustawienia dla serwera gmail znajdują się poniżej.

Properties props = new Properties();
props.put("mail.smtp.host", “smtp.gmail.com”);
props.put("mail.smtp.auth", "true");
props.put("mail.debug", "true");
props.put("mail.smtp.port", “465”);
props.put("mail.smtp.socketFactory.port", “465”);
props.put("mail.smtp.socketFactory.class", “javax.net.ssl.SSLSocketFactory”);
props.put("mail.smtp.socketFactory.fallback", "false");

Drugi parametr odpowiada natomiast za autentykację wobec serwera. W podanym przykładzie tworzony jest nowy obiekt klasy Java.mail.Authenticator i jednocześnie przeciążana jest metoda getPasswordAuthentication(), która zwraca nazwę konta i hasło opakowane w klasę PasswordAuthentication. Kolejnym krokiem, który należy wykonać jest stworzenie nowej, pustej wiadomości:

Message msg = new MimeMessage(session);

Warto zwrócić uwagę na klasę MimeMessage, która jako parametr konstruktora przyjmuje obiekt typu Session. Pozawala ona na tworzenie dowolnych emaili zawierających dane kodowane zgodne ze standardem MIME (patrz wykład o protokołach sieciowych). Klasa MimeMessage posiada metody, które umożliwiają stworzenie wiadomości i jej wysłanie. Pierwszym polem, które należy ustawić jest nadawca:

InternetAddress addressFrom = new InternetAddress(“mojeemail@gmail.com”);
msg.setFrom(addressFrom);

Kolejnym krokiem jest wybranie odbiorcy, lub odbiorców. Odbiorcy powinni być tablicą typu InternetAddress.

msg.setRecipients(Message.RecipientType.TO, addressTo);

Możliwy jest wybór typu odbiorcy poprzez stałe zdefiniowane w Message.RecipientType. Poza TO użytą w przykładzie powyżej do wyboru są jeszcze CC i BCC. Kolejnym krokiem jest ustawienie tytułu wiadomości i jej treści:

msg.setSubject(subject);
msg.setContent(message, "text/plain");

Zarówno subject jak i message to obiekty typu String. Drugi parametr metody setContent jest typem (zgodnym z MIME) przesyłanych danych. Po przygotowaniu wiadomości można ją wysłać wywołując metodę send(message) obiektu Transport.

Transport.send(msg);

Niewiele bardziej skomplikowane od wysyłania jest odbieranie wiadomości. Klasy pakietu JavaMail zostały zaprojektowane w taki sposób, aby niezależnie od tego czy korzystamy z POP3 czy IMAP sposób postępowania był taki sam w związku z tym niektóre kroki przy połączeniach POP3 są trochę „na wyrost”. Jednak dzięki takiemu podejściu zyskujemy elastyczność. Aby odebrać wiadomość (lub kilka wiadomości) musimy najpierw stworzyć obiekt Session (tak samo jak przy wysyłaniu) a następnie uzyskać obiekt klasy Store.

Store store = session.getStore("pop3");

Odpowiednio dla protokołu IMAP4 podajemy “imap”. Kolejnym krokiem jest nawiązanie połączenia i pobranie folderu.

store.connect();
Folder folder = store.getFolder(„INBOX”);

Polecenie pobierające folder należy wykonać zarówno dla protokołu POP3 jak i IMAP choć w tym pierwszym przypadku wartość „INBOX” jest jedyną dopuszczalną wartością. Otrzymany folder należy otworzyć w jednym z dostępnych w JAVA trybów (na przykład do odczytu, lub do odczytu i zapisu). Otwarty folder po wywołaniu metody getMessages() zwróci nam listę dostępnych wiadomości.

folder.open(Folder.READ_WRITE);
Message message[] = folder.getMessages();

Obiekt klaasy Message udostępnia metody zwracające jego zawartość takie jak getContent() i getInputStream(). Ponadto umożliwia on modyfikowanie flag widomości.

message.setFlag(Flags.Flag.DELETED, true);

Wygodną metodą jest reply(toAll), która zwraca obiekty typu Message będący odpowiedzią na wiadomość wobec której metoda ta została wywołana. Jedyny parametr tej metody decyduje o tym, że wiadomość będzie wysyłana do wszystkich odbiorców czy tylko do jej nadawcy. Połączenie kończy się zamykają obiekt folder i store.

folder.close(boolean);
store.close();

Parametr typu boolean przy zamykaniu folderu decyduje czy ma on zostać uaktualniony o dokonane zmiany czy też nie. Przedstawione powyżej rozwiązanie będzie działać dla wiadomości bez załączników. Obsługa wieloczęściowych wiadomości wymaga korzystania z klasy MimeMultipart. Głównym problemem przy odbieraniu jest to, że nie znamy struktury wiadomości w związku z tym należy przetwarzać go po kawałku posiłkując się metodą isMimeType(mime_string) wywoływaną na obiekcie MimeBodyPart aby uzależnić podejmowane działania od typu tego kawałka wiadomości.





Zadania 

Zadania

  1. Znajdź ciąg znaków identyfikucjący Twój komputer, przeglądarkę i system operacyjny (User-Agent). Jedną z metod jest użycie WireSharka.
  2. Napisz program sprawdzający poprawność wprowadzonego URLa i jeśli jest on poprawny dzielący go na części zgodnie ze specyfikacją RFC 1738.
  3. Posługując się powszechnie dostępną (Internet) listą popularnych stringów User-Agent napisz program, który będzie łączył się z zadanym serwerem i ściągał z niego plik index.html przedstawiając się jako ktoś inny (do wyboru z listy przed nawiązaniem połączenia).
  4. Napisz program działający w trybie tekstowym umożliwiający wysyłanie emaili poprzez konto PJWSTK lub gmail. Spróbuj uzupełnić go o możliwość przesyłania plików w formacie GIF.

Słownik 



Pliki 

Ten dział zostanie uzupełniony wkrótce...

W sieci