ServerSocket und Socket - Netzwerkgrundlagen
Inhaltsverzeichnis
Theorie, ServerSocket = Socket?
Ein Socket stellt einen logischen 'Sockel' für TCP- oder UDP-Netzwerkkommunikation dar. Hier wird nur TCP-Kommunikation behandelt.
TCP-Kommuniktion basiert auf sogenannten Ports, Ports benutzen Betriebssysteme um mehrere Verbindungen auseinanderhalten zu können. Jeder Port kann nur für eine Verbindung gleichzeitig benutzt werden.
Merke: Ports sind wie Sockets nur logische Konstrukte, in der Hardware sind weder Sockets noch Ports umgesetzt, beides sind lediglich Funktionen die das Betriebssystem zur Verfügung stellt.
Ein Socket kann benutzt werden um von einem lokalen Port eine Verbindung zu einem entferntem Port (dieser kann auf jedem über das Netzwerk erreichbaren Rechner sein, auch dem eigenen!) zu benutzen. Dazu muss ein neuer Socket mindestens mit zwei Angaben konstruiert werden: Zielrechner und Zielport. Der Quellport (auf dem eigenen Rechner, von dem die Kommunikation abgeht) wird vom Betriebssystem automatisch gewählt.
Es ist also nicht möglich auf eine neue Verbindung zu 'warten'. Diese Aufgabe übernimmt ein sog. ServerSocket, welcher automatisch an einen (anzugebenen) Port gebunden wird, auch hier gilt, nur ein Socket/Verbindung pro Port. Der ServerSocket selbst ist für keine wirkliche Kommunikation zuständig, er ist lediglich dafür zuständig neue Verbindungen anzunehmen. Diese werden dann automatisch auf einen neuen Port "umgeleitet" um den Server-Port wieder für neue Verbindungen freizumachen.
Zusammenfassung: Netzwerkkommunikation passiert über Ports die über Sockets angesprochen werden. Um auf neue Verbindungen zu warten wird ein ServerSocket benutzt, der für jede Verbindung einen neuen Socket liefert.
Socketverbindungen
Eine simple Verbindung zu einem anderen Rechner (hier allerdings der lokale Host) kann so aussehen:
[...]
// socket auf localhost port 1234 konstruieren
Socket s = new Socket("localhost",1234);
// etwas über den socket versenden
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
out.write("Hello World");
// zeilenumbruch senden
out.newLine();
out.flush();
Soweit noch ganz einfach, um den OutputStream den der Socket auf Anfrage liefert (über den auch sämtliche Kommunikation 'in' den Socket läuft) wird ein BufferedWriter konstruiert, damit wir ganze Zeilen senden können.
Zu beachten sind zwei Dinge:
- Damit auf der anderen Seite auch das Ende der Zeile erkannt werden kann, müssen wir explizit einen Zeilenumbruch mitschicken!
- Wenn Streams oder Writer mit Puffer (Buffer) benutzt werden, dann sollte, sofern es gewünscht wird das der gerade übergebene Text sofort gesendet wird _immer_ die Methode flush() aufgerufen werden, die alles was gerade im Puffer ist weiterleitet. Unter Umständen passiert sonst eine ganze Zeit lang keine Kommunikation da der Puffer noch nicht voll ist und so nicht weitergeleitet wird.
Der Socket kann natürlich nicht nur mit ausgehenden Daten umgehen, sondern auch mit eingehenden:
// BufferedReader konstruieren
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
// eine zeile lesen
String text = in.readLine()
// und ausgeben
System.out.print("Received: ");
System.out.println(text);
// am ende schliessen wir alle offenen Reader und Writer, der Socket wird dabei automatisch geschlossen
out.close();
in.close();
Hier ist eine bereits angesprochene Sache zu beachten:
- readLine() wartet (blockiert) so lange, bis ein Zeilenumbruch empfangen wird! Wird keiner gesendet, wartet die Methode ewig!
Socket-Server
Auf der anderen Seite brauchen wir nun natürlich einen Server, der mit unserem kleinen Client 'reden' kann. Dazu brauchen wir einen ServerSocket der sich an den entsprechenden Port bindet um dort auf Clients zu warten.
[...]
// Server starten
ServerSocket server = new ServerSocket(1234);
// warten auf eine neue Verbindung
Socket s = server.accept();
// neue Verbindung ist da, wir lesen einfach aus,
// was sie uns so schickt und schicken dann alles in grossbuchstaben wieder zurück
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
String text = in.readLine();
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
out.write(text.toUpperCase());
out.newLine();
out.flush();
// aufräumen
out.close();
in.close();
server.close();
An sich kein Problem, bis auf den ServerSocket tut das Programm genau dasselbe wie der Client.
Die Methode accept() blockiert das Programm solange bis eine neue Verbindung ankommt. Für diese Verbindung wird dann ein neuer Socket zurückgegeben der genauso wie ein normaler Socket funktioniert. Zu beachten ist, das accept() immer nur _eine_ Verbindung liefert, soll der Server auf weitere Verbindungen warten, so muss accept() wieder aufgerufen werden!
Zu bedenken ist auch, das in.readLine() zwar eine Zeile liest, der String aber _keinen_ Zeilenumbruch am Ende besitzt (also wieder explizit einen senden!)
Threads und Server
Es wird schnell klar das obiger Server nicht recht tauglich ist:
- Es wird nur eine Verbindung angenommen, danach beendet sich der Server
- Selbst wenn, zB. über eine while(true)-Schleife accept() wieder aufgerufen werden sollte, ist der Server für die Dauer der Bearbeitung einer Verbindung nicht erreichbar.
Sinnvoller wäre es, wenn der Server sich nur um neue Verbindungen kümmern würde und die Arbeit die eine eigentliche Verbindung macht an einen nebenläufigen Thread abgeben könnte.
Server
[...]
// Server aufbauen
ServerSocket server = new ServerSocket(1234);
Socket s;
while(true) {
// Auf verbindung warten
s = server.accept();
// kommunikation an einen nebenläufigen Thread abgeben
ServerThread t = new ServerThread(s);
t.start();
// und wieder auf neue Verbindungen warten
}
ServerThread
public class ServerThread extends Thread {
private Socket s;
public ServerThread(Socket s) {
this.s = s;
}
public void run() {
// lesen
BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
String text = in.readLine();
// schreiben
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));
out.write(text.toUpperCase());
out.newLine();
out.flush();
// aufräumen
out.close();
in.close();
}
}
Dies ist natürlich nur ein einfaches Konzept, echte Socketverbindungen können zB. über längere Zeit aufbleiben (damit sich die Threads auch lohnen), aber es macht deutlich wie ein Server mit nebenläufigen Threads funktionieren kann.
Beispiel: Chat
Server:
import java.net.*;
import java.io.*;
import java.util.*;
public class Server {
private Hashtable clients;
private int port;
public Server(int port) {
this.port=port;
clients = new Hashtable();
}
private void startServerListener() {
ServerSocket ss ;
try {
ss = new ServerSocket(port);
System.out.println("Server gestartet...");
while (true)
new ServerBody(ss.accept(), this).start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void addClient(String name, ServerBody body) {
clients.put(name, body);
}
public void removeClient(String name) {
clients.remove(name);
}
public String getUsers() {
String users;
users="users|";
for (Enumeration e = clients.keys();e.hasMoreElements();)
users+=(String) e.nextElement() + "|";
if (! users.equals("users|"))
users = users.substring(0, users.length() - 1);
return users;
}
public void broadcast(String name, String msg) throws Exception {
for (Enumeration e = clients.keys();e.hasMoreElements();)
((ServerBody) clients.get((String) e.nextElement())).send(name + ": " + msg);
}
public void send(String name, String targetname, String msg) throws Exception {
((ServerBody) clients.get(targetname)).send(name + ": " + msg);
}
public boolean isClient(String name) {
return clients.containsKey(name);
}
public static void main(String[] x) {
if (x.length != 1) {
System.out.println("#java Server <port>");
System.exit(0);
}
new Server(Integer.parseInt(x[0])).startServerListener();
}
}
class ServerBody extends Thread {
private Socket cs;
private Server server;
private PrintWriter out;
public ServerBody(Socket cs, Server server) {
this.cs=cs;
this.server=server;
}
public void run() {
BufferedReader in;
StringTokenizer str;
String name, msg, targetname;
int n;
try {
in = new BufferedReader (new InputStreamReader(cs.getInputStream()));
out = new PrintWriter(new DataOutputStream(cs.getOutputStream()));
name = in.readLine();
server.addClient(name, this);
server.broadcast(name, server.getUsers());
System.out.println("+ Client " + name + " hat sich angemeldet!");
for (String buffer;(buffer = in.readLine()) != null;) {
n = buffer.indexOf("|", 0);
targetname = buffer.substring(0, n);
msg = buffer.substring(n + 1, buffer.length());
if (targetname.equals("all")) {
server.broadcast(name, msg);
System.out.println(">Client " + name + " schreibt an alle: " + msg);
} else if (server.isClient(targetname)) {
server.send(name, targetname, msg);
System.out.println(">Client " + name + " schreibt an " + targetname + ": " + msg);
} else
this.send("Server: Client " + targetname + " existiert nicht!");
}
server.removeClient(name);
System.out.println("- Client " + name + " hat sich abgemeldet!");
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public void send(String msg) throws Exception {
out.println(msg);
out.flush();
}
}
Client:
import java.net.*;
import java.io.*;
import java.util.*;
public class Client {
private String ip;
private int port;
public Client(String ip, int port) {
this.ip=ip;
this.port=port;
}
private void startClient() throws Exception {
Socket s;
String buffer, name, targetname, msg;
PrintWriter out;
BufferedReader in;
int n;
s = new Socket(ip, port);
buffer = null;
out = new PrintWriter(new DataOutputStream(s.getOutputStream()));
in = new BufferedReader(new InputStreamReader(System.in));
System.out.println("\n\nClient gestartet...\n\n");
name = null;
while (name == null || name.equals("")) {
System.out.print("Client-Name eingeben: ");
name = in.readLine();
name = name.trim();
}
out.println(name);
out.flush();
new ClientBody(s.getInputStream()).start();
System.out.print("\nText eingeben -> <zielrechner> <message># ");
while (! (buffer = in.readLine().trim()).equals("stop")) {
if ((n = buffer.indexOf(" ", 0)) < 0) {
System.out.print("\nUngueltige Eingabe! Text eingeben -> <zielrechner> <message># ");
continue;
}
System.out.print("\nText eingeben -> <zielrechner> <message># ");
targetname = buffer.substring(0, n);
msg = buffer.substring(n + 1, buffer.length());
out.println(targetname + "|" + msg);
out.flush();
}
System.out.println("\n\nClient gestoppt...\n\n");
in.close();
out.close();
}
public static void main(String[] x) throws Exception {
if (x.length != 2) {
System.out.println("#java Client <server-ip> <port>");
System.exit(0);
}
new Client(x[0], Integer.parseInt(x[1])).startClient();
}
}
class ClientBody extends Thread {
private InputStream i;
public ClientBody(InputStream i) {
this.i=i;
}
public void run() {
String buffer;
BufferedReader in;
int n;
try {
in = new BufferedReader(new InputStreamReader(i));
while ((buffer = in.readLine()) != null) {
if ((n = buffer.indexOf("users|", 0)) > -1) {
buffer = buffer.substring(n + "users|".length(), buffer.length());
buffer = buffer.replace('|', ',');
System.out.println("\n\n==>Angemeldete User: " + buffer);
} else
System.out.println("\n\n==>Eingang von " + buffer);
System.out.print("\nText eingeben -> <zielrechner> <message># ");
}
} catch (Exception e) {}
}
}