Programowanie gniazd sieciowych w języku JAVA

Nawiązywanie połączeń TCP (klient)

Do nawiązywania połączeń TCP służy klasa java.net.Socket.

import java.io.*;
import java.net.Socket;

public class ClientSocket {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket("google.pl", 80);
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bufferedWriter.write("GET / HTTP/1.0\n\n");
        bufferedWriter.flush();
        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = reader.readLine();
        while (line!=null){
            System.out.println(line);
            System.out.flush();
            line = reader.readLine();
        }
    }
}

Ważniejsze części programu:

W konstruktorze socketa podajemy adres i port na który się łączymy:

Socket socket = new Socket("google.pl", 80);

Z socketem powiązane są dwa strumienie, które Państwo znacie z przedmiotu PO Java:

  • OutputStream, pobierany za pomocą wywołania socket.getOutputStream, służy do wysyłania danych do docelowego hosta
  • InputStream, pobierany za pomocą wywołania socket.getInputStream, służy do odbierania danych

Strumienie te są strumieniami binarnymi (tj. przesyłającymi nie tekst a dane binarne). Ponieważ HTTP jest protokołem tekstowym (w zasadzie) opakowujemy te strumienie do klasy BufferedReader która pozwala wygodnie pracować na tekście:

BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));

Po wysłaniu linijki danych musimy wykonać polecenie flush które spowoduje wysłanie danych do hosta (implementacja TCP może czekać aż nie zbierze się dostateczna ilość danych by wysłać pełny pakiet:

bufferedWriter.flush();

Zadanie 2

Uruchomić powyższy program w środowisku eclipse. Polączyć się z serwerem www.wp.pl na porcie 80. Na ekran należy wypisać tylko te linijki które zawierają słowo sport.

Przyjmowanie połączeń TCP (serwer)

Do tworzenia serwerów w javie służy klasa java.net.ServerSocket.

Ten program stworzy serwer odpisujący na to co mu się wysłało:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerSocketExample {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(12347);
        while (true){
            Socket socket = serverSocket.accept();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bufferedWriter.write("Napisz: \"END\" by zakończyć połączenie.");
            String line = bufferedReader.readLine();
            while (!line.contains("END")){
                bufferedWriter.write("Sever says: ");
                bufferedWriter.write(line);
                bufferedWriter.write("\n");
                bufferedWriter.flush();
                line = bufferedReader.readLine();
            }
            socket.close();
        }
    }
}

Ciekawsze elementy programu:

Tworzymy serwer który będzie akceptował połączenia na porcie 12347:

ServerSocket serverSocket = new ServerSocket(12347);

Wywołanie serverSocket.accept() jest blokujące tj. metoda ta zakończy się w momencie w którym serwer otrzyma połączenie. Metoda ta zwraca zwykłego socketa pozwalającego odczytywać i zapisywać dane do zdalnego systemu.

Zadanie 3

Uruchomić powyższy program w środowisku eclipse. Przerobić program w taki sposób, by do tekstu wysyłanego przez użytkownika dopisywał !. Należy przetestować wprowadzone zmiany za pomocą narzędzia telnet. Proszę zasymulować połączenie się do powyższego programu dwóch różnych klientów (należy odpalić program telnet dwukrotnie, w różnych terminalach). Co się wtedy dzieje?

Wielowątkowy serwer w Javie

Serwer z poprzedniego przykładu jest jednowątkowy, tj. kiedy obsługuje jednego klienta nie akceptuje połączeń od innych klientów.

Oczywiście jest to niepożądana cecha, w praktyce serwery oprogramowuje się tak by każde połączenie było oprogramowane w oddzielnym wątku.

Ponższy przykład jest w stanie obsłużyć wiele wątków:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MultihreadedServerSocketExample {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(12347);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        while (true){
            final Socket socket = serverSocket.accept();
            Runnable connection = new Runnable() {
                @Override
                public void run() {

                    try {
                        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                        bufferedWriter.write("Napisz: \"END\" by zakończyć połączenie.");
                        bufferedWriter.flush();
                        String line = bufferedReader.readLine();
                        while (!line.contains("END")){
                            bufferedWriter.write("Sever says: ");
                            bufferedWriter.write(line);
                            bufferedWriter.write("\n");
                            bufferedWriter.flush();
                            line = bufferedReader.readLine();
                        }
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }
            };
            executorService.submit(connection);
        }
    }
}

Informacje na temat przykładu:

  • Żeby socket był widoczny wewnątrz instancji Runnable musimy poprzedzić go modyfikatorem final.
  • Pojawił się ExecutorService
  • Połączenie obsługiwane jest nie w wątku main a w wątku zarządzanym przez ExecutorService.
  • Musimy obsłużyć błędy w połączeniu, bo interfejs Runnable nie pozwala na propagację wyjątków z metody run.

Zadanie 4

Uruchomić powyższy program w środowisku eclipse. Przy użyciu narzędzia telnet przetestować możliwość łączenia się wielu klientów do powyższego programu.

Zadanie 5

Proszę w parach napisać prosty program typu “chat” do porozumiewania się pomiędzy dwoma użytkownikami.