Programowanie za pomocą gniazd w Javie: samouczek

Ten samouczek jest wprowadzeniem do programowania gniazd w Javie, zaczynając od prostego przykładu klient-serwer, demonstrującego podstawowe funkcje we / wy Java. Zostaniesz wprowadzony zarówno do oryginalnego  java.io pakietu, jak i NIO, nieblokujących java.niointerfejsów API I / O ( ), które zostały wprowadzone w Javie 1.4. Na koniec zobaczysz przykład, który demonstruje obsługę sieci w Javie zaimplementowaną od wersji Java 7 w NIO.2.

Programowanie gniazd sprowadza się do dwóch systemów komunikujących się ze sobą. Ogólnie rzecz biorąc, komunikacja sieciowa występuje w dwóch odmianach: protokół kontroli transportu (TCP) i protokół datagramów użytkownika (UDP). TCP i UDP są używane do różnych celów i oba mają unikalne ograniczenia:

  • TCP jest stosunkowo prostym i niezawodnym protokołem, który umożliwia klientowi nawiązanie połączenia z serwerem i komunikację między dwoma systemami. W TCP każda jednostka wie, że jej ładunki komunikacyjne zostały odebrane.
  • UDP jest protokołem bezpołączeniowym i jest dobry w scenariuszach, w których niekoniecznie każdy pakiet musi dotrzeć do miejsca przeznaczenia, takich jak strumieniowe przesyłanie multimediów.

Aby docenić różnicę między TCP i UDP, zastanów się, co by się stało, gdybyś strumieniował wideo ze swojej ulubionej witryny i gubił klatki. Czy wolisz, aby klient spowolnił Twój film, aby otrzymać brakujące klatki, czy wolisz, aby wideo było nadal odtwarzane? Protokoły przesyłania strumieniowego wideo zazwyczaj wykorzystują UDP. Ponieważ TCP gwarantuje dostarczenie, jest to protokół z wyboru dla HTTP, FTP, SMTP, POP3 i tak dalej.

W tym samouczku przedstawię programowanie gniazd w Javie. Przedstawiam serię przykładów klient-serwer, które demonstrują funkcje z oryginalnego środowiska Java I / O, a następnie stopniowo przechodzę do korzystania z funkcji wprowadzonych w NIO.2.

Oldschoolowe gniazda Java

W implementacjach poprzedzających NIO kod gniazda klienta Java TCP jest obsługiwany przez java.net.Socketklasę. Poniższy kod otwiera połączenie z serwerem:

 Socket socket = new Socket (serwer, port); 

Po podłączeniu naszej socketinstancji do serwera możemy rozpocząć pobieranie strumieni wejściowych i wyjściowych do serwera. Strumienie wejściowe służą do odczytywania danych z serwera, podczas gdy strumienie wyjściowe służą do zapisywania danych na serwerze. Możemy wykonać następujące metody, aby uzyskać strumienie wejściowe i wyjściowe:

InputStream in = socket.getInputStream (); OutputStream out = socket.getOutputStream ();

Ponieważ są to zwykłe strumienie, te same, których używalibyśmy do odczytywania i zapisywania w pliku, możemy je przekonwertować do postaci, która najlepiej pasuje do naszego przypadku użycia. Na przykład moglibyśmy zawinąć element OutputStreama PrintStream, abyśmy mogli łatwo pisać tekst metodami takimi jak println(). Na przykład możemy zawinąć InputStreamznak a BufferedReader, poprzez an InputStreamReader, aby łatwo czytać tekst metodami takimi jak readLine().

pobierz Pobierz kod źródłowy Kod źródłowy programu „Programowanie za pomocą gniazd w Javie: samouczek”. Stworzone przez Stevena Hainesa dla JavaWorld.

Przykład klienta gniazda Java

Przeanalizujmy krótki przykład, który wykonuje HTTP GET na serwerze HTTP. Protokół HTTP jest bardziej wyrafinowany niż pozwala na to nasz przykład, ale możemy napisać kod klienta, aby obsłużyć najprostszy przypadek: zażądać zasobu z serwera, a serwer zwraca odpowiedź i zamyka strumień. Ten przypadek wymaga następujących kroków:

  1. Utwórz gniazdo do serwera WWW nasłuchującego na porcie 80.
  2. Uzyskaj PrintStreamdo serwera i wyślij żądanie GET PATH HTTP/1.0, gdzie PATHna serwerze znajduje się żądany zasób. Na przykład, gdybyśmy chcieli otworzyć katalog główny witryny internetowej, ścieżka byłaby taka /.
  3. Uzyskaj InputStreamdo serwera, zawiń go BufferedReaderi przeczytaj odpowiedź wiersz po wierszu.

Listing 1 przedstawia kod źródłowy tego przykładu.

Listing 1. SimpleSocketClientExample.java

pakiet com.geekcap.javaworld.simplesocketclient; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; public class SimpleSocketClientExample {public static void main (String [] args) {if (args.length <2) {System.out.println ("Użycie: SimpleSocketClientExample"); System.exit (0); } Serwer łańcuchowy = argumenty [0]; Ścieżka ciągu = argumenty [1]; System.out.println ("Ładowanie zawartości adresu URL:" + serwer); try {// Połącz się z serwerem Socket socket = new Socket (server, 80); // Utwórz strumienie wejściowe i wyjściowe do odczytu i zapisu na serwerze PrintStream out = new PrintStream (socket.getOutputStream ()); BufferedReader in = new BufferedReader (new InputStreamReader (socket.getInputStream ())); // Postępuj zgodnie z protokołem HTTP GET HTTP / 1.0, po którym następuje pusta linia out.println ("GET" + ścieżka + "HTTP / 1.0"); out.println (); // Odczytaj dane z serwera, aż skończymy czytać dokument String line = in.readLine (); while (linia! = null) {System.out.println (linia); line = in.readLine (); } // Zamknij nasze strumienie in.close (); out.close (); socket.close (); } catch (wyjątek e) {e.printStackTrace (); }}}

Listing 1 akceptuje dwa argumenty wiersza poleceń: serwer do połączenia (zakładając, że łączymy się z serwerem na porcie 80) i zasób do pobrania. Tworzy adres, Socketktóry wskazuje na serwer i wyraźnie określa port 80. Następnie wykonuje polecenie:

POBIERZ ŚCIEŻKĘ HTTP / 1.0 

Na przykład:

POBIERZ / HTTP / 1.0 

Co się stało?

Podczas pobierania strony internetowej z serwera WWW, na przykład www.google.com, klient HTTP używa serwerów DNS do znalezienia adresu serwera: zaczyna się od zapytania serwera domeny najwyższego poziomu o comdomenę, w której autorytatywny serwer nazw domen jest dla domeny www.google.com. Następnie prosi serwer nazw domen o adres IP (lub adresy) dla domeny www.google.com. Następnie otwiera gniazdo do tego serwera na porcie 80. (Lub, jeśli chcesz zdefiniować inny port, możesz to zrobić, dodając dwukropek, po którym następuje numer portu, na przykład :8080:) Na koniec klient HTTP wykonuje określony sposób HTTP, na przykład GET, POST, PUT, DELETE, HEAD, i OPTI/ONS. Każda metoda ma własną składnię. Jak pokazano na powyższych wycinkach kodu, GETmetoda wymaga ścieżki, po której następujeHTTP/version numberi pusty wiersz. Gdybyśmy chcieli dodać nagłówki HTTP, moglibyśmy to zrobić przed wejściem do nowej linii.

Na Listingu 1 pobraliśmy OutputStreami opakowaliśmy go w PrintStream, abyśmy mogli łatwiej wykonywać nasze polecenia tekstowe. Nasz kod uzyskał an InputStream, opakował go w an InputStreamReader, który przekonwertował go na a Reader, a następnie opakował w plik BufferedReader. Użyliśmy PrintStreamdo wykonania naszej GETmetody, a następnie użyliśmy BufferedReaderdo odczytania odpowiedzi wiersz po wierszu, aż otrzymaliśmy nullodpowiedź wskazującą, że gniazdo zostało zamknięte.

Teraz wykonaj tę klasę i przekaż jej następujące argumenty:

java com.geekcap.javaworld.simplesocketclient.SimpleSocketClientExample www.javaworld.com / 

Powinieneś zobaczyć wyjście podobne do tego, co poniżej:

Ładowanie zawartości adresu URL: www.javaworld.com HTTP / 1.1 200 OK Data: Sun, 21 września 2014 22:20:13 GMT Serwer: Apache X-Gas_TTL: 10 Cache-Control: max-age = 10 X-GasHost: gas2 .usw X-Cooking-With: Benzyna-Lokalny X-Benzyna-Wiek: 8 Długość treści: 168 Ostatnia modyfikacja: Wt, 24 stycznia 2012 00:09:09 GMT Etykieta: „60001b-a8-4b73af4bf3340” Typ treści : text / html Różne: Akceptuj-kodowanie Połączenie: zamknij stronę testową benzyny

Powodzenie

Te dane wyjściowe pokazują stronę testową w witrynie JavaWorld. Odpowiedział, że używa protokołu HTTP w wersji 1.1, a odpowiedź brzmi 200 OK.

Przykład serwera Java Socket

Omówiliśmy stronę klienta i na szczęście aspekt komunikacji po stronie serwera jest równie łatwy. Z uproszczonej perspektywy proces wygląda następująco:

  1. Utwórz ServerSocket, określając port do nasłuchiwania.
  2. Wywołać ServerSocket„s accept()metody słuchać od skonfigurowanego portu dla połączenia klienta.
  3. Gdy klient łączy się z serwerem, accept()metoda zwraca a, Socketza pośrednictwem którego serwer może komunikować się z klientem. Jest to ta sama Socketklasa, której użyliśmy dla naszego klienta, więc proces jest taki sam: uzyskanie pliku InputStreamdo odczytu od klienta i OutputStreamzapisu do klienta.
  4. Jeśli serwer musi być skalowalny, należy przekazać go Socketdo innego wątku w celu przetworzenia, aby serwer mógł kontynuować nasłuchiwanie dodatkowych połączeń.
  5. Zadzwoń do ServerSocket„s accept()metody znowu słuchać innego połączenia.

Jak wkrótce zobaczysz, NIO radzi sobie z tym scenariuszem nieco inaczej. Na razie jednak możemy bezpośrednio utworzyć a ServerSocket, przekazując mu port do nasłuchiwania (więcej o ServerSocketFactorys w następnej sekcji):

 ServerSocket serverSocket = nowy ServerSocket (port); 

A teraz możemy przyjmować połączenia przychodzące accept()metodą:

Gniazdo gniazda = serverSocket.accept (); // Obsłuż połączenie ...

Programowanie wielowątkowe z wykorzystaniem gniazd Java

Listing 2 poniżej zestawia cały dotychczasowy kod serwera w nieco bardziej niezawodny przykład, który wykorzystuje wątki do obsługi wielu żądań. Pokazany serwer jest serwerem echa , co oznacza, że ​​odsyła każdą otrzymaną wiadomość.

Chociaż przykład z Listingu 2 nie jest skomplikowany, przewiduje niektóre z tego, co pojawi się w następnej sekcji o NIO. Zwróć szczególną uwagę na ilość kodu wątkowego, który musimy napisać, aby zbudować serwer, który może obsłużyć wiele jednoczesnych żądań.

Listing 2. SimpleSocketServer.java

pakiet com.geekcap.javaworld.simplesocketclient; import java.io.BufferedReader; import java.io.I / OException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class SimpleSocketServer rozszerza Thread {private ServerSocket serverSocket; prywatny port int; prywatne działanie logiczne = false; publiczny SimpleSocketServer (port int) {this.port = port; } public void startServer () {try {serverSocket = new ServerSocket (port); this.start (); } catch (I / OException e) {e.printStackTrace (); }} public void stopServer () {running = false; this.interrupt (); } @Override public void run () {running = true; while (działa) {try {System.out.println ("Nasłuchiwanie połączenia"); // Wywołaj accept (), aby odebrać następne połączenie Socket socket = serverSocket.accept ();// Przekaż gniazdo do wątku RequestHandler w celu przetworzenia RequestHandler requestHandler = new RequestHandler (gniazdo); requestHandler.start (); } catch (I / OException e) {e.printStackTrace (); }}} public static void main (String [] args) {if (args.length == 0) {System.out.println ("Użycie: SimpleSocketServer"); System.exit (0); } int port = Integer.parseInt (args [0]); System.out.println ("Uruchom serwer na porcie:" + port); Serwer SimpleSocketServer = nowy SimpleSocketServer (port); server.startServer (); // Automatyczne zamknięcie za 1 minutę try {Thread.sleep (60000); } catch (wyjątek e) {e.printStackTrace (); } server.stopServer (); }} klasa RequestHandler rozszerza Thread {prywatne gniazdo Socket; RequestHandler (gniazdo gniazda) {this.socket = gniazdo; } @Override public void run () {try {System.out.println ("Otrzymano połączenie"); // Pobieranie strumieni wejściowych i wyjściowych BufferedReader in = new BufferedReader (new InputStreamReader (socket.getInputStream ())); PrintWriter out = new PrintWriter (socket.getOutputStream ()); // Wypisz nasz nagłówek do klienta out.println ("Echo Server 1.0"); out.flush (); // Echo wierszy z powrotem do klienta, dopóki klient nie zamknie połączenia lub nie otrzymamy pustej linii String line = in.readLine (); while (linia! = null && line.length ()> 0) {out.println ("Echo:" + linia); out.flush (); line = in.readLine (); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}// Pobieranie strumieni wejściowych i wyjściowych BufferedReader in = new BufferedReader (new InputStreamReader (socket.getInputStream ())); PrintWriter out = new PrintWriter (socket.getOutputStream ()); // Wypisz nasz nagłówek do klienta out.println ("Echo Server 1.0"); out.flush (); // Echo wierszy z powrotem do klienta, dopóki klient nie zamknie połączenia lub nie otrzymamy pustej linii String line = in.readLine (); while (linia! = null && line.length ()> 0) {out.println ("Echo:" + linia); out.flush (); line = in.readLine (); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}// Pobieranie strumieni wejściowych i wyjściowych BufferedReader in = new BufferedReader (new InputStreamReader (socket.getInputStream ())); PrintWriter out = new PrintWriter (socket.getOutputStream ()); // Wypisz nasz nagłówek do klienta out.println ("Echo Server 1.0"); out.flush (); // Echo wierszy z powrotem do klienta, dopóki klient nie zamknie połączenia lub nie otrzymamy pustej linii String line = in.readLine (); while (linia! = null && line.length ()> 0) {out.println ("Echo:" + linia); out.flush (); line = in.readLine (); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}// Wypisz nasz nagłówek do klienta out.println ("Echo Server 1.0"); out.flush (); // Echo wierszy z powrotem do klienta, dopóki klient nie zamknie połączenia lub nie otrzymamy pustej linii String line = in.readLine (); while (linia! = null && line.length ()> 0) {out.println ("Echo:" + linia); out.flush (); line = in.readLine (); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}// Wypisz nasz nagłówek do klienta out.println ("Echo Server 1.0"); out.flush (); // Echo wierszy z powrotem do klienta, dopóki klient nie zamknie połączenia lub nie otrzymamy pustej linii String line = in.readLine (); while (linia! = null && line.length ()> 0) {out.println ("Echo:" + linia); out.flush (); line = in.readLine (); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}Czytaj linię(); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}Czytaj linię(); } // Zamknij nasze połączenie in.close (); out.close (); socket.close (); System.out.println ("Połączenie zamknięte"); } catch (wyjątek e) {e.printStackTrace (); }}}