Eine Anleitung zu Java Sockets

Eine Anleitung zu Java Sockets

1. Überblick

Der Begriffsocketprogramming bezieht sich auf das Schreiben von Programmen, die auf mehreren Computern ausgeführt werden, auf denen alle Geräte über ein Netzwerk miteinander verbunden sind.

Es gibt zwei Kommunikationsprotokolle, die für die Socket-Programmierung verwendet werden können:User Datagram Protocol (UDP) and Transfer Control Protocol (TCP).

Der Hauptunterschied zwischen den beiden besteht darin, dass UDP verbindungslos ist. Dies bedeutet, dass keine Sitzung zwischen dem Client und dem Server stattfindet, während TCP verbindungsorientiert ist. Dies bedeutet, dass zunächst eine exklusive Verbindung zwischen Client und Server hergestellt werden muss, damit die Kommunikation stattfinden kann.

In diesem Lernprogramm werden die Netzwerke vonan introduction to sockets programming over TCP/IPvorgestellt und das Schreiben von Client / Server-Anwendungen in Java veranschaulicht. UDP ist kein Mainstream-Protokoll und wird daher möglicherweise nicht häufig verwendet.

2. Projektaufbau

Java stellt eine Sammlung von Klassen und Schnittstellen zur Verfügung, die sich um die Kommunikation zwischen Client und Server auf niedriger Ebene kümmern.

Diese sind meistens im Paketjava.netenthalten, daher müssen wir den folgenden Import durchführen:

import java.net.*;

Wir benötigen auch das Paketjava.io, das uns Eingabe- und Ausgabestreams zum Schreiben und Lesen während der Kommunikation bietet:

import java.io.*;

Der Einfachheit halber führen wir unsere Client- und Serverprogramme auf demselben Computer aus. Wenn wir sie auf verschiedenen Netzwerkcomputern ausführen würden, würde sich nur die IP-Adresse ändern. In diesem Fall verwenden wirlocalhost auf127.0.0.1.

3. Einfaches Beispiel

Machen wir uns mit den meistenbasic of examples involving a client and a server die Hände schmutzig. Es wird eine bidirektionale Kommunikationsanwendung sein, bei der der Client den Server begrüßt und der Server antwortet.

Erstellen wir die Serveranwendung in einer Klasse namensGreetServer.java mit dem folgenden Code.

Wir enthalten diemain-Methode und die globalen Variablen, um die Aufmerksamkeit darauf zu lenken, wie alle Server in diesem Artikel ausgeführt werden. In den restlichen Beispielen in den Artikeln werden wir diese Art von sich wiederholendem Code weglassen:

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

Erstellen wir mit diesem Code auch einen Client namensGreetClient.java:

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
}

Let’s start the server; in Ihrer IDE tun Sie dies, indem Sie es einfach als Java-Anwendung ausführen.

Und jetzt senden wir eine Begrüßung mit einem Komponententest an den Server, der bestätigt, dass der Server tatsächlich eine Begrüßung als Antwort sendet:

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

Machen Sie sich keine Sorgen, wenn Sie nicht ganz verstehen, was hier passiert, da dieses Beispiel uns ein Gefühl dafür geben soll, was uns später in diesem Artikel erwartet.

In den folgenden Abschnitten werden wirsocket communication anhand dieses einfachen Beispiels analysieren und mit weiteren Beispielen tiefer in die Details eintauchen.

4. Wie Steckdosen funktionieren

Wir werden das obige Beispiel verwenden, um verschiedene Teile dieses Abschnitts durchzugehen.

Per Definition ist asocket ein Endpunkt einer bidirektionalen Kommunikationsverbindung zwischen zwei Programmen, die auf verschiedenen Computern in einem Netzwerk ausgeführt werden. Ein Socket ist an eine Portnummer gebunden, damit die Transportschicht die Anwendung identifizieren kann, an die die Daten gesendet werden sollen.

4.1. Der Kellner

In der Regel wird ein Server auf einem bestimmten Computer im Netzwerk ausgeführt und verfügt über einen Socket, der an eine bestimmte Portnummer gebunden ist. In unserem Fall verwenden wir denselben Computer wie der Client und haben den Server über Port6666 gestartet:

ServerSocket serverSocket = new ServerSocket(6666);

Der Server wartet nur und lauscht dem Socket, bis ein Client eine Verbindungsanfrage gestellt hat. Dies geschieht im nächsten Schritt:

Socket clientSocket = serverSocket.accept();

Wenn der Servercode auf die Methodeaccepttrifft, wird er blockiert, bis ein Client eine Verbindungsanforderung an ihn stellt.

Wenn alles gut geht, stellt der Serveracceptsdie Verbindung her. Bei der Annahme erhält der Server einen neuen Socket,clientSocket, der an denselben lokalen Port6666 gebunden ist, und dessen Remote-Endpunkt auf die Adresse und den Port des Clients festgelegt ist.

Zu diesem Zeitpunkt stellt das neueSocket-Objekt den Server in direkte Verbindung mit dem Client. Anschließend können wir auf die Ausgabe- und Eingabestreams zugreifen, um Nachrichten zum bzw. vom Client zu schreiben und zu empfangen:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Ab hier kann der Server endlos Nachrichten mit dem Client austauschen, bis der Socket mit seinen Streams geschlossen wird.

In unserem Beispiel kann der Server jedoch erst eine Begrüßungsantwort senden, bevor die Verbindung geschlossen wird. Wenn wir unseren Test erneut ausführen, wird die Verbindung abgelehnt.

Um Kontinuität in der Kommunikation zu ermöglichen, müssen wir aus dem Eingabestream innerhalb einerwhile-Schleife lesen und erst beenden, wenn der Client eine Beendigungsanforderung sendet. Dies wird im folgenden Abschnitt in Aktion gesehen.

Für jeden neuen Client benötigt der Server einen neuen Socket, der vom Aufruf vonacceptzurückgegeben wird. DasserverSocket wird verwendet, um weiterhin auf Verbindungsanfragen zu warten und sich gleichzeitig um die Anforderungen der verbundenen Clients zu kümmern. Dies haben wir in unserem ersten Beispiel noch nicht berücksichtigt.

4.2. Der Kunde

Der Client muss den Hostnamen oder die IP des Computers kennen, auf dem der Server ausgeführt wird, und die Portnummer, auf der der Server empfangsbereit ist.

Um eine Verbindungsanforderung zu stellen, versucht der Client, ein Rendezvous mit dem Server auf dem Computer und dem Port des Servers durchzuführen:

Socket clientSocket = new Socket("127.0.0.1", 6666);

Der Client muss sich auch gegenüber dem Server identifizieren, damit er sich an eine vom System zugewiesene lokale Portnummer bindet, die er während dieser Verbindung verwendet. Wir kümmern uns nicht selbst darum.

Der obige Konstruktor erstellt nur dann einen neuen Socket, wenn der Serveraccepted der Verbindung hat. Andernfalls wird eine Ausnahme für die verweigerte Verbindung angezeigt. Nach erfolgreicher Erstellung können wir dann Eingabe- und Ausgabestreams abrufen, um mit dem Server zu kommunizieren:

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

Der Eingabestream des Clients ist mit dem Ausgabestream des Servers verbunden, genauso wie der Eingabestream des Servers mit dem Ausgabestream des Clients verbunden ist.

5. Kontinuierliche Kommunikation

Unser aktueller Server blockiert, bis sich ein Client mit ihm verbindet, und blockiert dann erneut, um eine Nachricht vom Client abzuhören. Nach der einzelnen Nachricht wird die Verbindung geschlossen, da wir uns nicht um Kontinuität gekümmert haben.

Es ist also nur bei Ping-Anfragen hilfreich, aber stellen Sie sich vor, wir möchten einen Chat-Server implementieren. Eine kontinuierliche Kommunikation zwischen Server und Client ist auf jeden Fall erforderlich.

Wir müssen eine while-Schleife erstellen, um den Eingabestream des Servers für eingehende Nachrichten kontinuierlich zu beobachten.

Erstellen wir einen neuen Server mit dem NamenEchoServer.java, dessen einziger Zweck darin besteht, alle von Clients empfangenen Nachrichten wiederzugeben:

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

Beachten Sie, dass wir eine Beendigungsbedingung hinzugefügt haben, bei der die while-Schleife beendet wird, wenn wir ein Punktzeichen erhalten.

Wir werdenEchoServer mit der Hauptmethode starten, genau wie wir es fürGreetServer getan haben. Dieses Mal starten wir es an einem anderen Port wie4444, um Verwirrung zu vermeiden.

EchoClient ähneltGreetClient, sodass wir den Code duplizieren können. Wir trennen sie aus Gründen der Klarheit.

In einer anderen Testklasse erstellen wir einen Test, um zu zeigen, dass mehrere Anforderungen anEchoServer bearbeitet werden, ohne dass der Server den Socket schließt. Dies gilt, solange wir Anfragen vom selben Kunden senden.

Der Umgang mit mehreren Kunden ist ein anderer Fall, den wir in einem späteren Abschnitt sehen werden.

Erstellen wir einesetup-Methode, um eine Verbindung mit dem Server herzustellen:

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

Wir werden gleichermaßen einetearDown-Methode erstellen, um alle unsere Ressourcen freizugeben. Dies ist eine bewährte Methode für jeden Fall, in dem wir Netzwerkressourcen verwenden:

@After
public void tearDown() {
    client.stopConnection();
}

Testen wir dann unseren Echoserver mit einigen Anfragen:

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");

    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

Dies ist eine Verbesserung gegenüber dem ursprünglichen Beispiel, bei dem wir nur einmal kommunizieren würden, bevor der Server unsere Verbindung geschlossen hat. now we send a termination signal to tell the server when we’re done with the session.

6. Server mit mehreren Clients

So sehr das vorherige Beispiel eine Verbesserung gegenüber dem ersten war, so großartig ist die Lösung dennoch nicht. Ein Server muss in der Lage sein, viele Clients und viele Anforderungen gleichzeitig zu bedienen.

Der Umgang mit mehreren Kunden wird in diesem Abschnitt behandelt.

Eine weitere Funktion, die wir hier sehen werden, ist, dass derselbe Client die Verbindung trennen und erneut herstellen kann, ohne dass eine Ausnahme bezüglich einer abgelehnten Verbindung oder ein Zurücksetzen der Verbindung auf dem Server auftritt. Bisher konnten wir das nicht.

Dies bedeutet, dass unser Server über mehrere Anforderungen von mehreren Clients hinweg robuster und ausfallsicherer wird.

Dazu erstellen wir einen neuen Socket für jeden neuen Client und Service, den der Client in einem anderen Thread anfordert. Die Anzahl der Clients, die gleichzeitig bedient werden, entspricht der Anzahl der ausgeführten Threads.

Der Haupt-Thread führt eine while-Schleife aus, während er auf neue Verbindungen wartet.

Genug geredet, lassen Sie uns einen anderen Server namensEchoMultiServer.java. erstellen. Darin erstellen wir eine Handler-Thread-Klasse, um die Kommunikation jedes Clients auf seinem Socket zu verwalten:

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

Beachten Sie, dass wir jetztaccept innerhalb einerwhile-Schleife aufrufen. Jedes Mal, wenn diewhile-Schleife ausgeführt wird, blockiert sie den Aufruf vonaccept, bis ein neuer Client eine Verbindung herstellt. Anschließend wird der Handler-ThreadEchoClientHandler für diesen Client erstellt.

Was innerhalb des Threads passiert, ist das, was wir zuvor inEchoServer getan haben, wo wir nur einen einzelnen Client behandelt haben. DieEchoMultiServer delegieren diese Arbeit anEchoClientHandler, damit sie weiterhin auf mehr Clients in derwhile-Schleife warten können.

Wir werden weiterhinEchoClient verwenden, um den Server zu testen. Dieses Mal werden wir mehrere Clients erstellen, die jeweils mehrere Nachrichten vom Server senden und empfangen.

Starten wir unseren Server mit seiner Hauptmethode an Port5555.

Aus Gründen der Übersichtlichkeit werden wir weiterhin Tests in einer neuen Suite durchführen:

@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");

    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

Wir können beliebig viele dieser Testfälle erstellen, wobei jeder neue Client und der Server alle von ihnen bedienen.

7. Fazit

In diesem Tutorial haben wir uns aufan introduction to sockets programming over TCP/IPkonzentriert und eine einfache Client / Server-Anwendung in Java geschrieben.

Der vollständige Quellcode für den Artikel befindet sich - wie üblich - im ProjektGitHub.