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 zapoznany zarówno z oryginalnym
java.io
pakietem i NIO, nieblokującym We/Wy (java.nio
) API wprowadzone w Javie 1.4. Na koniec, zobaczysz przykład, który demonstruje sieci Java zaimplementowane z Java 7 forward, 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: Transport Control Protocol (TCP) i User Datagram Protocol (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, a oba systemy do komunikacji. W TCP każdy podmiot wie, że jego ładunki komunikacyjne zostały odebrane.,
- UDP jest protokołem bezpołączeniowym i jest dobry w scenariuszach, w których niekoniecznie każdy pakiet dociera do miejsca docelowego, takich jak przesyłanie strumieniowe multimediów.
aby docenić różnicę między TCP i UDP, zastanów się, co by się stało, gdybyś streamował wideo ze swojej ulubionej strony internetowej i upuszczał klatki. Czy wolisz, aby Klient zwolnił Twój film, aby otrzymać brakujące klatki, czy wolisz, aby film nadal był odtwarzany? Protokoły przesyłania strumieniowego wideo zazwyczaj wykorzystują UDP., Ponieważ TCP gwarantuje dostarczanie, jest to protokół z wyboru dla HTTP, FTP, SMTP, POP3 i tak dalej.
w tym tutorialu przedstawiam wam programowanie socket w Javie. Przedstawiam serię przykładów klient-serwer, które demonstrują funkcje z oryginalnego frameworka I/o Javy, a następnie stopniowo przechodzą do korzystania z funkcji wprowadzonych w NIO.2.
stare gniazda Java
w implementacjach poprzedzających NIO Kod gniazda klienta Java TCP jest obsługiwany przez klasęjava.net.Socket
., Poniższy kod otwiera połączenie z serwerem:
Socket socket = new Socket( server, port );
gdy nasza instancja socket
jest podłączona do serwera, możemy zacząć uzyskiwać strumienie wejściowe i wyjściowe do serwera. Strumienie wejściowe są używane do odczytu danych z serwera, podczas gdy strumienie wyjściowe są używane do zapisu danych na serwer., 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 strumienie, których używamy do odczytu i zapisu do pliku, możemy przekonwertować je do formy, która najlepiej służy naszemu przypadku użycia. Na przykład, możemy zawinąć OutputStream
z PrintStream
tak, że możemy łatwo pisać tekst za pomocą metod takich jak println()
., W innym przykładzie możemy zawinąć InputStream
z BufferedReader
, za pomocą InputStreamReader
, aby łatwo odczytać tekst za pomocą metod takich jak readLine()
.
przykład Java socket client
przejdźmy do krótkiego przykładu, który wykonuje HTTP GET na serwerze HTTP., 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:
- Utwórz gniazdo na serwerze sieciowym nasłuchującym na porcie 80.
- uzyskaj
PrintStream
do serwera i wyślij żądanieGET PATH HTTP/1.0
, gdziePATH
jest żądanym zasobem na serwerze., Na przykład, gdybyśmy chcieli otworzyć katalog główny strony internetowej, ścieżka byłaby/
. - uzyskaj
InputStream
na serwer, zawiń go za pomocąBufferedReader
i odczytaj odpowiedź linia po linii.
Listing 1 pokazuje kod źródłowy tego przykładu.
notowanie 1. SimpleSocketClientExample.java
Listing 1 akceptuje dwa argumenty wiersza poleceń: serwer do połączenia (zakładając, że łączymy się z serwerem na porcie 80) oraz zasób do pobrania., Tworzy on Socket
, który wskazuje na serwer i jawnie określa port 80
. Następnie wykonuje polecenie:
GET PATH HTTP/1.0
na przykład:
GET / HTTP/1.0
co się właśnie stało?
Po pobraniu strony internetowej z serwera www, takiego jak , klient HTTP używa serwerów DNS, aby znaleźć adres serwera: zaczyna się od zapytania serwera domeny najwyższego poziomu o
com
domeny, gdzie autorytatywny serwer nazwy domeny jest dla ., Następnie pyta, że serwer nazw domenowych dla adresu IP (lub adresów) dla
. Następnie otwiera gniazdo do tego serwera na porcie 80. (Lub, jeśli chcesz zdefiniować inny port, możesz to zrobić, dodając dwukropek, a następnie numer portu, na przykład:
:8080
.) Na koniec klient HTTP wykonuje określoną metodę HTTP, na przykład GET
, POST
, PUT
, DELETE
, HEAD
, lub OPTI/ONS
. Każda metoda ma swoją składnię., Jak pokazano w powyższym fragmencie kodu, metoda GET
wymaga ścieżki, po której następuje HTTP/version number
I pustej linii. Gdybyśmy chcieli dodać nagłówki HTTP, moglibyśmy to zrobić przed wejściem do nowej linii.
w liście 1 pobraliśmyOutputStream
I zawiniliśmy go wPrintStream
, abyśmy mogli łatwiej wykonywać nasze polecenia tekstowe., Nasz kod uzyskał InputStream
, zawinięty w InputStreamReader
, który przekonwertował go na Reader
, a następnie zawinięty w BufferedReader
. Użyliśmy PrintStream
aby wykonać naszą metodę GET
, a następnie użyliśmy BufferedReader
aby odczytać odpowiedź linia po linii, aż otrzymaliśmy odpowiedź null
, wskazując, że gniazdo zostało zamknięte.,
teraz wykonaj tę klasę i podaj jej następujące argumenty:
java com.geekcap.javaworld.simplesocketclient.SimpleSocketClientExample www.javaworld.com /
powinieneś zobaczyć wyjście podobne do tego, co jest poniżej:
to wyjście pokazuje stronę testową na stronie JavaWorld. Odpowiedział, że mówi HTTP w wersji 1.1, a odpowiedź to 200 OK
.
przykład Java socket server
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:
- Utwórz
ServerSocket
, określając port do nasłuchu. - wywołaj
ServerSocket
’saccept()
metodę nasłuchiwania skonfigurowanego portu dla połączenia klienta. - gdy klient łączy się z serwerem, metoda
accept()
zwracaSocket
za pomocą której serwer może komunikować się z klientem., Jest to ta sama klasaSocket
, której użyliśmy dla naszego klienta, więc proces jest taki sam: uzyskaćInputStream
do odczytu z klienta iOutputStream
do zapisu do klienta. - Jeśli serwer musi być skalowalny, będziesz chciał przekazać
Socket
do innego wątku do przetworzenia, aby twój serwer mógł kontynuować nasłuchiwanie dodatkowych połączeń. - wywołanie metody
ServerSocket
accept()
aby ponownie nasłuchać innego połączenia.,
jak już wkrótce się przekonacie, postępowanie NIO w tym scenariuszu byłoby nieco inne., Na razie możemy jednak bezpośrednio utworzyć ServerSocket
przekazując mu port do nasłuchu (więcej o ServerSocketFactory
s w następnej sekcji):
ServerSocket serverSocket = new ServerSocket( port );
i teraz możemy akceptować połączenia przychodzące za pomocą accept()
metoda:
Socket socket = serverSocket.accept();// Handle the connection ...
programowanie wielowątkowe za pomocą gniazd Java
listing 2, poniżej, umieszcza cały kod serwera do tej pory razem w nieco bardziej solidnym przykładzie, który używa wątków do obsługi wielu żądań., Wyświetlany serwer jest serwerem echo, co oznacza, że odbija On każdą otrzymaną wiadomość.
o ile przykład z listingu 2 nie jest skomplikowany, to jednak przewiduje niektóre z tego, co pojawi się w następnej sekcji na NIO. Zwróć szczególną uwagę na ilość kodu wątkowego, który musimy napisać, aby zbudować serwer, który może obsługiwać wiele jednoczesnych żądań.
notowanie 2. SimpleSocketServer.java
w liście 2 tworzymy nową instancję SimpleSocketServer
I uruchamiamy serwer., Jest to wymagane, ponieważSimpleSocketServer
rozszerzaThread
, aby utworzyć nowy wątek do obsługi blokowaniaaccept()
wywołanie, które widzisz wread()
. Metodarun()
znajduje się w pętli, akceptując żądania klientów i tworzącRequestHandler
wątki do przetworzenia żądania. Ponownie, jest to stosunkowo prosty kod, ale również wymaga sporej ilości programowania wątkowego.,
zauważ również, że RequestHandler
obsługuje komunikację z klientem, podobnie jak kod z listy 1: owija OutputStream
z PrintStream
w celu ułatwienia łatwego zapisu i podobnie owija InputStream
z BufferedReader
dla łatwego odczytu. Jeśli chodzi o serwer, odczytuje linie od klienta i wysyła je z powrotem do klienta. Jeśli klient wyśle pustą linię, rozmowa zostanie zakończona iRequestHandler
zamyka Gniazdo.
NIO i NIO.,2
dla wielu aplikacji, podstawowy model programowania gniazd Java, który właśnie zbadaliśmy, jest wystarczający. W przypadku aplikacji wymagających bardziej intensywnego wejścia/wyjścia lub asynchronicznego wejścia/wyjścia należy zapoznać się z nieblokującymi interfejsami API wprowadzonymi w Javie NIO i NIO.2.
pakiet JDK 1.4 NIO oferuje następujące kluczowe funkcje:
- kanały są zaprojektowane do obsługi masowych transferów z jednego bufora NIO do drugiego.
- bufory reprezentują sąsiadujący blok pamięci połączony prostym zestawem operacji.,
- Non-Blocking Input / Output to zestaw klas, które wystawiają kanały na wspólne źródła wejścia / wyjścia, takie jak pliki i gniazda.
podczas programowania z NIO otwierasz kanał do miejsca docelowego, a następnie wczytujesz dane do bufora z miejsca docelowego, zapisujesz dane do bufora i wysyłasz je do miejsca docelowego., W niedługim czasie zajmiemy się konfiguracją gniazda i uzyskaniem kanału do niego, ale najpierw przyjrzyjmy się procesowi używania bufora:
- zapis danych do bufora
- wywołanie metody bufora
flip()
aby przygotować go do odczytu - odczytanie danych z bufora
- wywołanie metody bufora
clear()
lubcompact()
metoda przygotowania go do odbioru większej ilości danych
Gdy dane są zapisane do bufora, bufor zna ilość danych do niego zapisanych., Zachowuje trzy właściwości, których znaczenia różnią się, jeśli bufor jest w trybie odczytu lub zapisu:
- Position: w trybie zapisu początkowa pozycja wynosi 0 i utrzymuje bieżącą pozycję zapisywaną w buforze; po odwróceniu bufora, aby umieścić go w trybie odczytu, resetuje pozycję do 0 i utrzymuje bieżącą pozycję w buforze, z którego jest odczytywany,
- Capacity: stały rozmiar bufora
- Limit: w trybie zapisu limit określa, ile danych można zapisać do bufora; tryb odczytu, limit określa ile danych można odczytać z bufora.,
Java i / o demo: Serwer Echo z NIO.2
NIO.2, który został wprowadzony w JDK 7, rozszerza nieblokujące Biblioteki We/Wy Javy, aby dodać obsługę zadań systemu plików, takich jak pakiet java.nio.file
I java.nio.file.Path
I udostępnia nowe API systemu plików. Mając to na uwadze, napiszmy nowy serwer Echo używając NIO.2 ' s AsynchronousServerSocketChannel
.
AsynchronousServerSocketChannel
zapewnia nieblokujący kanał asynchroniczny dla gniazd nasłuchowych zorientowanych strumieniowo., Aby go użyć, najpierw wykonujemy jego statyczną metodę open()
a następnie bind()
do określonego portu. Następnie wykonamy metodę accept()
, przekazując do niej klasę implementującą interfejsCompletionHandler
. Najczęściej znajdziesz ten handler stworzony jako anonimowa Klasa wewnętrzna.
Listing 3 pokazuje kod źródłowy naszego nowego asynchronicznego serwera Echo.
notowanie 3. SimpleSocketServer.,java
w liście 3 najpierw tworzymy nowy AsynchronousServerSocketChannel
a następnie bindujemy go do portu 5000:
final AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(5000));
z tego AsynchronousServerSocketChannel
wywołujemy accept()
aby powiedzieć mu, aby zaczął nasłuchiwać połączeń, przekazując mu niestandardową instancję CompletionHandler
. Gdy wywołujemy accept()
, zwraca natychmiast., Zauważ, że ten przykład różni się od klasy ServerSocket
w liście 1; podczas gdy metoda accept()
jest zablokowana do czasu podłączenia do niej klienta, metoda AsynchronousServerSocketChannel
accept()
obsługuje ją za nas.
obsługa zakończenia
naszym następnym zadaniem jest utworzenie klasyCompletionHandler
I dostarczenie implementacji metodcompleted()
Ifailed()
., Metodacompleted()
jest wywoływana, gdyAsynchronousServerSocketChannel
otrzymuje połączenie od klienta i zawieraAsynchronousSocketChannel
do klienta. Metoda completed()
najpierw przyjmuje połączenie z AsynchronousServerSocketChannel
I rozpoczyna komunikację z klientem. Pierwszą rzeczą, którą robi, jest wypisanie komunikatu „Hello”: buduje łańcuch znaków, konwertuje go na tablicę bajtów, a następnie przekazuje go do ByteBuffer.wrap()
, aby zbudować ByteBuffer
., MetodaByteBuffer
może być następnie przekazanaAsynchronousSocketChannel
write()
.
aby odczytać z klienta, tworzymy nowy ByteBuffer
wywołując jego allocate(4096)
(który tworzy bufor 4K), następnie wywołujemy AsynchronousSocketChannel
’sread()
metoda. read()
zwraca Future<Integer>
na którym możemy wywołać get()
aby pobrać liczbę bajtów odczytanych z klienta., W tym przykładzie przekazujemy get()
wartość limitu czasu 20 sekund: jeśli nie otrzymamy odpowiedzi w ciągu 20 sekund, to get()
metoda rzuci TimeoutException
. Naszą zasadą dla tego serwera echo jest to, że jeśli zachowamy 20 sekund ciszy, zakończymy rozmowę.
następnie sprawdzamy pozycję bufora, która będzie lokalizacją ostatniego bajtu otrzymanego od klienta. Jeśli klient wyśle pustą linię, to otrzymamy dwa bajty: powrót karetki i kanał linii., Kontrola zapewnia, że jeśli klient wyśle pustą linię, przyjmujemy ją jako wskaźnik, że klient zakończył rozmowę. Jeśli mamy sensowne dane, to wywołujemy metodę ByteBuffer
’s flip()
aby przygotować je do odczytu. Tworzymy tymczasową tablicę bajtów, która przechowuje liczbę bajtów odczytanych z klienta, a następnie wywołujemyByteBuffer
’sget()
aby załadować dane do tej tablicy bajtów. Na koniec konwertujemy tablicę bajtów na ciąg znaków, tworząc nową instancję String
., Możemy echo linii z powrotem do klienta, konwertując łańcuch znaków do tablicy bajtów, przekazując go do metody ByteBuffer.wrap()
I wywołując AsynchronousSocketChannel
’s write()
. Teraz clear()
ByteBuffer
, co oznacza, że repozycjonuje position
do zera i umieszczaByteBuffer
w trybie zapisu, a następnie odczytujemy następną linię od klienta.,
należy pamiętać tylko o tym, że metodamain()
, która tworzy serwer, ustawia również 60 sekund timera, aby aplikacja działała. PonieważAsynchronousSocketChannel
’saccept()
metoda zwraca natychmiast, jeśli nie mamy Thread.sleep()
wtedy nasza aplikacja zatrzyma się natychmiast.,
aby to przetestować, uruchom serwer i połącz się z nim za pomocą klienta telnet:
telnet localhost 5000
wyślij kilka ciągów do serwera, obserwuj, że są one odbite od ciebie, a następnie wyślij pustą linię, aby zakończyć rozmowę.
podsumowując
w tym artykule przedstawiłem dwa podejścia do programowania gniazd w Javie: tradycyjne podejście wprowadzone w Javie 1.0 oraz nowsze, nieblokujące NIO i NIO.2 podejścia wprowadzone odpowiednio w Javie 1.4 i Javie 7., Widziałeś kilka iteracji klienta gniazd Java i przykładu serwera gniazd Java, demonstrując zarówno użyteczność podstawowych We/Wy Java, jak i niektóre scenariusze, w których nieblokujące We/Wy ulepszają model programowania gniazd Java. Za pomocą nieblokujących We/Wy można zaprogramować aplikacje sieciowe Java do obsługi wielu jednoczesnych połączeń bez konieczności zarządzania wieloma kolekcjami wątków. Możesz również skorzystać z nowej skalowalności serwera wbudowanej w NIO i NIO.2.