Java Standard: Socket und ServerSocket (java.net)
Einleitung
[Bearbeiten]Zur Erzeugung von Socket-Verbindungen stellt Java die beiden Klassen java.net.Socket
und java.net.ServerSocket
zur Verfügung.
Der folgende Code soll das Vorgehen verdeutlichen. Er enthält einen extrem primitiven Client und Server. Beim Server kann sich nur ein Client anmelden und nur einmal eine (kurze) Nachricht senden. Der Server sendet diese Nachricht dann zurück und beendet sich. Hat der Client die zurückgesendete Nachricht empfangen, beendet auch er sich. Hinweise für intelligentere Server und Clients finden sich weiter unten.
Ein primitiver Server
[Bearbeiten] // Server.java
// import java.net.ServerSocket;
// import java.net.Socket;
import java.io.*;
public class Server {
public static void main(String[] args) {
Server server = new Server();
try {
server.test();
} catch (IOException e) {
e.printStackTrace();
}
}
void test() throws IOException {
int port = 11111;
java.net.ServerSocket serverSocket = new java.net.ServerSocket(port);
java.net.Socket client = warteAufAnmeldung(serverSocket);
String nachricht = leseNachricht(client);
System.out.println(nachricht);
schreibeNachricht(client, nachricht);
}
java.net.Socket warteAufAnmeldung(java.net.ServerSocket serverSocket) throws IOException {
java.net.Socket socket = serverSocket.accept(); // blockiert, bis sich ein Client angemeldet hat
return socket;
}
String leseNachricht(java.net.Socket socket) throws IOException {
BufferedReader bufferedReader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
char[] buffer = new char[200];
int anzahlZeichen = bufferedReader.read(buffer, 0, 200); // blockiert bis Nachricht empfangen
String nachricht = new String(buffer, 0, anzahlZeichen);
return nachricht;
}
void schreibeNachricht(java.net.Socket socket, String nachricht) throws IOException {
PrintWriter printWriter =
new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream()));
printWriter.print(nachricht);
printWriter.flush();
}
}
Ein primitiver Client
[Bearbeiten] // Client.java
// import java.net.Socket;
import java.io.*;
public class Client {
public static void main(String[] args) {
Client client = new Client();
try {
client.test();
} catch (IOException e) {
e.printStackTrace();
}
}
void test() throws IOException {
String ip = "127.0.0.1"; // localhost
int port = 11111;
java.net.Socket socket = new java.net.Socket(ip,port); // verbindet sich mit Server
String zuSendendeNachricht = "Hello, world!";
schreibeNachricht(socket, zuSendendeNachricht);
String empfangeneNachricht = leseNachricht(socket);
System.out.println(empfangeneNachricht);
}
void schreibeNachricht(java.net.Socket socket, String nachricht) throws IOException {
PrintWriter printWriter =
new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream()));
printWriter.print(nachricht);
printWriter.flush();
}
String leseNachricht(java.net.Socket socket) throws IOException {
BufferedReader bufferedReader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
char[] buffer = new char[200];
int anzahlZeichen = bufferedReader.read(buffer, 0, 200); // blockiert bis Nachricht empfangen
String nachricht = new String(buffer, 0, anzahlZeichen);
return nachricht;
}
}
Erklärung des Codes
[Bearbeiten]Die Klasse java.net.ServerSocket
dient in erster Linie zur Anmeldung von Clients. Sie hört nicht - wie man vielleicht erwarten könnte - auf die Nachrichten von Clients. Im Konstruktor wird der Port (im Beispiel 11111) übergeben (die IP-Adresse des Servers ist natürlich die IP-Adresse des Computers, auf dem der Server läuft). Die Methode accept()
wartet so lange, bis sich ein Client verbunden hat. Dann gibt sie einen java.net.Socket
zurück.
Die Klasse java.net.Socket
hat zwei Funktionen:
- im Server dient sie dazu, sich den angemeldeten Client zu merken, auf seine Nachrichten zu hören und ihm zu antworten. Erzeugt wird eine Instanz durch die Methode
accept()
, wenn sich ein Client angemeldet hat. - im Client erzeugt sie die Verbindung zum Server; dem Konstruktor wird die IP-Adresse des Servers und dessen Port übergeben. Mit dem Aufruf des Konstruktors im Client wird die Verbindung hergestellt - beim Server gibt
accept()
den verbundenen Client zurück.
Die Kommunikation läuft dann auf beiden Seiten gleich:
Mit java.net.Socket#getInputStream()
bzw. java.net.Socket#getOutputStream()
können die Nachrichten der anderen Seite gelesen werden bzw. dorthin gesendet werden. Die lesenden Methoden blockieren dabei so lange, bis eine Nachricht empfangen wurde.
Wie sähe ein intelligenter Server aus
[Bearbeiten]Probleme bereiten die blockierenden Methoden; sowohl die Methode java.net.ServerSocket#accept()
als auch die Methode read()
(oder seine Verwandten) warten solange, bis sich ein Client angemeldet hat bzw. bis die Gegenseite eine Nachricht gesendet hat.
Auf der Seite des Servers wird es deshalb in der Regel einen eigenen Thread geben, der auf Anmeldungen von Clients wartet. Für jeden Client, der sich angemeldet hat, wird dann wiederum ein neuer Thread gestartet, der auf Nachrichten des Clients wartet. Daneben wird man in aller Regel den Client in einer Liste speichern, damit es möglich ist, allen angemeldeten Clients eine Nachricht zu senden.
Auch im Client gibt es einen eigenen Thread, der auf Nachrichten des Servers wartet. Während man es sonst bei Methodenaufrufen gewohnt ist, dass man auf den Rückgabewert (und sei es void
) der Funktion wartet (synchron), verläuft die Kommunikation hier asynchron.
Eine weitere Schwierigkeit ist das Lesen von Nachrichten der Gegenseite. Im Beispiel werden Nachrichten der Länge 200 gelesen (das reicht für "Hello, world!"). Es hilft an dieser Stelle nicht wirklich, den Buffer deutlich größer zu machen (aus Performance-Gründen wird man ihn natürlich etwa auf 1024 Bytes vergrößern), Beim Lesen treten nämlich zwei Probleme auf:
- Es kommt vor, dass die Nachricht länger ist als der Buffer.
- Es kommt vor, dass mehrere Nachrichten (teilweise) im Buffer stehen.
Deshalb müssen Server und Client ein eigenes Protokoll vereinbaren. Denkbar (und üblich) sind dabei mehrere Varianten:
- Alle Nachrichten haben eine feste Länge
- Zu Beginn jeder Nachricht (z.B. in den ersten 8 Zeichen) wird die Länge der Nachricht angegeben; es werden dann zunächst diese 8 Zeichen eingelesen und ausgewertet und dann entsprechend viele Zeichen gelesen
- Jede Nachricht endet mit einem festen String, der sonst innerhalb der Nachrichten nicht vorkommt (z.B. "$END$"). Der InputStream wird solange gelesen, bis dieser String gefunden wurde.
Ein etwas komfortableres Client/Server-Beispiel
[Bearbeiten]Server und Client verwenden das TCP-Protokoll. - Server o Server-Threads 1. main-Thread 2. Thread zur Annahme von Client-Anforderungen. 3. Pro Anforderung wird in Thread 2 ein neuer Thread aus einem Thread-Pool gestartet, der mit seinem Client kommuniziert und letztlich die Anforderung bearbeitet. o Threadpool Die Client-Threads werden in einem Pool verwaltet und bei neuen Anforderungen wiederverwendet. - Client Client-Threads 1. main-Thread 2. Thread zur Kommunikation mit dem Server 3. (irrelevanter) Thread zur Veranschaulichung der Nebenläufigkeit Im main-Thread (Methode 'worker') kann jederzeit abgefragt werden, ob die Antwort vom Server eingetroffen ist.
TCP-Server
[Bearbeiten]Hinweis: Die Erklärung der Beispiele steht als Kommentar im Quelltext.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.io.*;
import java.net.*;
import java.util.Date;
import java.text.*;
public class ServerExample {
/*
- Server benutzt Port 3141,
liefert zu einem Datum selbiges mit dem Wochentag an den Client
- jede Client-Anforderung wird von einem Thread des erzeugten Thread-Pools
behandelt
- Server-Socket kann mit Strg+C geschlossen werden oder vom Client mit
dem Wert 'Exit'.
*/
public static void main(String[] args) throws IOException {
final ExecutorService pool;
final ServerSocket serverSocket;
int port = 3141;
String var = "C";
String zusatz;
if (args.length > 0 )
var = args[0].toUpperCase();
if (var == "C") {
//Liefert einen Thread-Pool, dem bei Bedarf neue Threads hinzugefügt
//werden. Vorrangig werden jedoch vorhandene freie Threads benutzt.
pool = Executors.newCachedThreadPool();
zusatz = "CachedThreadPool";
} else {
int poolSize = 4;
//Liefert einen Thread-Pool für maximal poolSize Threads
pool = Executors.newFixedThreadPool(poolSize);
zusatz = "poolsize="+poolSize;
}
serverSocket = new ServerSocket(port);
//Thread zur Behandlung der Client-Server-Kommunikation, der Thread-
//Parameter liefert das Runnable-Interface (also die run-Methode für t1).
Thread t1 = new Thread(new NetworkService(pool,serverSocket));
System.out.println("Start NetworkService(Multiplikation), " + zusatz +
", Thread: "+Thread.currentThread());
//Start der run-Methode von NetworkService: warten auf Client-request
t1.start();
//
//reagiert auf Strg+C, der Thread(Parameter) darf nicht gestartet sein
Runtime.getRuntime().addShutdownHook(
new Thread() {
public void run() {
System.out.println("Strg+C, pool.shutdown");
pool.shutdown(); //keine Annahme von neuen Anforderungen
try {
//warte maximal 4 Sekunden auf Beendigung aller Anforderungen
pool.awaitTermination(4L, TimeUnit.SECONDS);
if (!serverSocket.isClosed()) {
System.out.println("ServerSocket close");
serverSocket.close();
}
} catch ( IOException e ) { }
catch ( InterruptedException ei ) { }
}
}
);
//
}
}
//Thread bzw. Runnable zur Entgegennahme der Client-Anforderungen
class NetworkService implements Runnable { //oder extends Thread
private final ServerSocket serverSocket;
private final ExecutorService pool;
public NetworkService(ExecutorService pool,
ServerSocket serverSocket) {
this.serverSocket = serverSocket;
this.pool = pool;
}
public void run() { // run the service
try {
//Endlos-Schleife: warte auf Client-Anforderungen
//Abbruch durch Strg+C oder Client-Anforderung 'Exit',
//dadurch wird der ServerSocket beendet, was hier zu einer IOException
//führt und damit zum Ende der run-Methode mit vorheriger Abarbeitung der
//finally-Klausel.
while ( true ) {
/*
Zunächst wird eine Client-Anforderung entgegengenommen(accept-Methode).
Der ExecutorService pool liefert einen Thread, dessen run-Methode
durch die run-Methode der Handler-Instanz realisiert wird.
Dem Handler werden als Parameter übergeben:
der ServerSocket und der Socket des anfordernden Clients.
*/
Socket cs = serverSocket.accept(); //warten auf Client-Anforderung
//starte den Handler-Thread zur Realisierung der Client-Anforderung
pool.execute(new Handler(serverSocket,cs));
}
} catch (IOException ex) {
System.out.println("--- Interrupt NetworkService-run");
}
finally {
System.out.println("--- Ende NetworkService(pool.shutdown)");
pool.shutdown(); //keine Annahme von neuen Anforderungen
try {
//warte maximal 4 Sekunden auf Beendigung aller Anforderungen
pool.awaitTermination(4L, TimeUnit.SECONDS);
if ( !serverSocket.isClosed() ) {
System.out.println("--- Ende NetworkService:ServerSocket close");
serverSocket.close();
}
} catch ( IOException e ) { }
catch ( InterruptedException ei ) { }
}
}
}
//Thread bzw. Runnable zur Realisierung der Client-Anforderungen
class Handler implements Runnable { //oder 'extends Thread'
private final Socket client;
private final ServerSocket serverSocket;
Handler(ServerSocket serverSocket,Socket client) { //Server/Client-Socket
this.client = client;
this.serverSocket = serverSocket;
}
public void run() {
StringBuffer sb = new StringBuffer();
PrintWriter out = null;
try {
// read and service request on client
System.out.println( "running service, " + Thread.currentThread() );
out = new PrintWriter( client.getOutputStream(), true );
BufferedReader bufferedReader =
new BufferedReader(
new InputStreamReader(
client.getInputStream()));
char[] buffer = new char[100];
int anzahlZeichen = bufferedReader.read(buffer, 0, 100); // blockiert bis Nachricht empfangen
String nachricht = new String(buffer, 0, anzahlZeichen);
String[] werte = nachricht.split("\\s"); //Trennzeichen: whitespace
if (werte[0].compareTo("Exit") == 0) {
out.println("Server ended");
if ( !serverSocket.isClosed() ) {
System.out.println("--- Ende Handler:ServerSocket close");
try {
serverSocket.close();
} catch ( IOException e ) { }
}
} else { //normale Client-Anforderung
for (int i = 0; i < werte.length; i++) {
String rt = getWday(werte[i]); //ermittle den Wochentag
sb.append(rt + "\n");
}
sb.deleteCharAt(sb.length()-1);
}
} catch (IOException e) {System.out.println("IOException, Handler-run");}
finally {
out.println(sb); //Rückgabe Ergebnis an den Client
if ( !client.isClosed() ) {
System.out.println("****** Handler:Client close");
try {
client.close();
} catch ( IOException e ) { }
}
}
} //Ende run
String getWday(String s) { //Datum mit Wochentag
SimpleDateFormat sdf = new SimpleDateFormat("EEEE, dd.MM.yyyy");
String res="";
try {
//Parameter ist vom Typ Date
res=sdf.format(DateFormat.getDateInstance().parse(s));
} catch (ParseException p) {}
return res;
}
}
Es folgt ein passender Client. Aufrufbeispiel für 2 Datumsangaben: java ClientExample 24.12.1941 9.11.1989
TCP-Client
[Bearbeiten]Hinweis: Die Erklärung der Beispiele steht als Kommentar im Quelltext.
import java.io.*;
import java.net.*;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ClientExample {
/*
Im 'worker' des Hauptprogramms wird wie folgt verfahren:
o Bilde Instanz von 'FutureTask', gib ihr als Parameter eine Instanz von
'ClientHandler' mit, die das Interface 'Callable' (ähnlich 'Runnable')
implementiert.
o Übergib die 'FutureTask' an einen neuen Thread und starte diesen.
Im Thread wird nun die 'call'-Methode aus dem Interface 'Callable' des
ClientHandlers abgearbeitet.
o Dabei wird die komplette Kommunikation mit dem Server durchgeführt.
Die 'call'-Methode gibt nun das Ergebnis vom Server an die 'FutureTask'
zurück, wo es im Hauptprogramm zur Verfügung steht. Hier kann beliebig
oft und an beliebigen Stellen abgefragt werden, ob das Ergebnis bereits
vorliegt.
*/
String werte;
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Datum-Parameter fehlen !");
System.exit(1);
}
StringBuffer sb = new StringBuffer();
//alle Parameter zusammenfassen, getrennt durch Leerzeichen
for (int i = 0; i < args.length; i++) {
sb.append(args[i] + ' ');
}
String werte = sb.toString().trim();
try {
//Irrelevanter Thread zur Illustrierung der Nebenläufigkeit der Abarbeitung
Thread t1 = new Thread(new AndererThread());
t1.start();
ClientExample cl = new ClientExample(werte);
cl.worker();
} catch (Exception e) {
e.printStackTrace();
}
}
public ClientExample(String werte) {
this.werte = werte;
}
void worker() throws Exception {
System.out.println("worker:" + Thread.currentThread());
//Klasse die 'Callable' implementiert
ClientHandler ch = new ClientHandler(werte);
boolean weiter = false;
do { //2 Durchläufe
int j = 0;
//call-Methode 'ch' von ClientHandler wird mit 'FutureTask' asynchron
//abgearbeitet, das Ergebnis kann dann von der 'FutureTask' abgeholt
//werden.
FutureTask<String> ft = new FutureTask<String>(ch);
Thread tft = new Thread(ft);
tft.start();
//prüfe ob der Thread seine Arbeit getan hat
while (!ft.isDone()) {
j++; //zähle die Thread-Wechsel
Thread.yield(); //andere Threads (AndererThread) können drankommen
}
System.out.println("not isDone:" + j);
System.out.println(ft.get()); //Ergebnis ausgeben
if (werte.compareTo("Exit") == 0)
break;
weiter = !weiter;
if (weiter) {
//2. Aufruf für Client-Anforderung, letzten Wert modifizieren
ch.setWerte(werte.substring(0,werte.length()-4) + "1813");
}
} while (weiter);
}
}
//Enthält die call-Methode für die FutureTask (entspricht run eines Threads)
class ClientHandler implements Callable<String> {
String ip = "127.0.0.1"; //localhost
int port = 3141;
String werte;
public ClientHandler(String werte) {
this.werte = werte;
}
void setWerte(String s) {
werte = s;
}
public String call() throws Exception { //run the service
System.out.println("ClientHandler:" + Thread.currentThread());
//verlängere künstlich die Bearbeitung der Anforderung, um das Wechselspiel
//der Threads zu verdeutlichen
Thread.sleep(2000);
return RequestServer(werte);
}
//Socket öffnen, Anforderung senden, Ergebnis empfangen, Socket schliessen
String RequestServer(String par) throws IOException {
String empfangeneNachricht;
String zuSendendeNachricht;
Socket socket = new Socket(ip,port); //verbindet sich mit Server
zuSendendeNachricht = par;
//Anforderung senden
schreibeNachricht(socket, zuSendendeNachricht);
//Ergebnis empfangen
empfangeneNachricht = leseNachricht(socket);
socket.close();
return empfangeneNachricht;
}
void schreibeNachricht(Socket socket, String nachricht) throws IOException {
PrintWriter printWriter =
new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream()));
printWriter.print(nachricht);
printWriter.flush();
}
String leseNachricht(Socket socket) throws IOException {
BufferedReader bufferedReader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
char[] buffer = new char[100];
//blockiert bis Nachricht empfangen
int anzahlZeichen = bufferedReader.read(buffer, 0, 100);
String nachricht = new String(buffer, 0, anzahlZeichen);
return nachricht;
}
}
class AndererThread implements Runnable {
public void run() {
System.out.println(" AndererThread:" + Thread.currentThread());
int n = 0;
int w = 25000000;
//hinreichend viel CPU-Zeit verbrauchen
for (int i = 1; i <= 10; i++)
for (int j = 1; j <= w; j++) {
if (j % w == 0)
System.out.println(" n=" + (++n));
}
}
}