Chat Server - Clients

Hallo Leute.
Erstmal OT: vllt werden sich ein paar fragen, was mit meinem 3d projekt ist, aber das war kein projekt… ich hab nur versucht mich da ein bisschen
reinzulernen und das hab ich ja auch getan - das was ich bisher gemacht hab hab ich auch verstanden und gelernt.

Nun habe ich mit Java Sockets und dem Server Client prinzip angefangen, einerseits weil das schon lange auf meiner warteschlange steht und andererseits
weil mein Lehrer, der weiß das ich ein bisschen zu weit für die ef bin, das für eine gute sache für mich hielt ^^

Naja zu meinem ersten Problem: Die Kommunikation zwischen Server und einem clienthab ich bereits hinbekommen, auch wenn bislang nur über strings.
Ich hab das im grunde folgender maßen gemacht:

der server läuft in einer endlosschleife, speichert die ausgabe von bufferedReader.readLine() in einem string, guckt ob der “stop” ist, (um dann abzubrechen),
gibt ansonsten die erhaltene nachricht aus, und läuft wieder nach oben.

Der Client schickt bislang einfach nur 3 testnachrichten mit (outputstream) jeweils printWriter.print(irendwas), die kommen auch an, mit “stop” wird richtig beendet.
ABER: wenn eine nachricht geschickt wurde, bleibt der server natürlich bei reader.readLine() hängen - wartet also bis irgendetwas gesendet wird. ist ja logisch.
Das ganze sieht so aus:

System.out.println("#Server [connection_start]: STARTING");
try{
	loop:
	while(true){
		String input = s.readLine();
		if(input.equals("stop")){
		    break loop;
		}
		else{
			System.out.println("#Server [new_message]:  " + input + " [" + s.getLocalIp());
		}
	}
	System.out.println("#Server [connection_end]: EXIT");
}
catch(Exception e){
	e.printStackTrace();
}```

Für einen Client mag das ja klappen, weil der ja der einzige ist der etwas senden kann. 
Aber wenn ich nun mehrere clients haben will, die sich mit diesem server verbinden können sollen?
Ich dachte erstmal an folgendes:

```System.out.println("#Server [connection_start]: STARTING");
try{
	loop:
	while(true){
		for(ClientUser s : clientSockets){
			String input = s.readLine();
			if(input.equals("stop")){
				break loop;
			}
			else{
				System.out.println("#Server [new_message]:  " + input + " [" + s.getLocalIp());
			}
		}
	}
	System.out.println("#Server [connection_end]: EXIT");
}
catch(Exception e){
	e.printStackTrace();
}```

also das selbe, nur eben mit der for loop - die durch alle verbundenen sockets läuft. diese sockets werden einfach so aufgefüllt:

```public synchronized void run(){
	while(true){
		try{
			Socket incoming = serverSocket.accept();
			clientSockets.add(new ClientUser(incoming));
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}```

das klappt auch, die sockets verbinden sich und werden in der array list abgelegt. aber die for loop geht natürlich gar nicht. denn sobald ein client nichts sendet, geht der 
server ja trozdem zu readLine() und wartet auf nen signal eingang. Das Problem ist, if(readLine() != null) geht auch nicht, denn wenn nichts gesendet wird, ist readLine() nicht null
sondern eben ein "ich warte jetzt so lange bis was kommt". 
Wie soll ich denn bitte prüfen, ob etwas kommt, wenn ich das nur machen kann in dem ich etwas senden, aber nichts mehr senden will? o.O 
ich dachte vllt an eine for loop im client die zum beispiel "ichsendenichts" sendet, und dadurch der server ads einfach ignoriert. aber das ist doch totaler schwachsinn, allein wegem dem
traffic der dabei erzeugt wird.

Wie mach ich das also? Wie prüfe ich ob da nichts ankommt vom socket?

Vielen Dank!

Hier der ganze Code:


de.skysoldier.chat
[spoiler]
[SPOILER=ChatServer.java]```package de.skysoldier.chat;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class ChatServer implements Runnable {
	
	private Thread thread;
	private ServerSocket serverSocket;
	private UserManager userManager;
	private ArrayList<ClientUser> clientSockets;
	
	public ChatServer(int port){
		thread = new Thread(this);
		try{
			serverSocket = new ServerSocket(port);
		}
		catch(Exception e){
			e.printStackTrace();
		}
		clientSockets = new ArrayList<>();
		userManager = new UserManager();
		thread.start();
	}
	
	public String getLocalPort(){
		return serverSocket.getLocalPort() + "";
	}
	
	public String getLocalIp(){
		try{
			return InetAddress.getLocalHost().getHostAddress();
		}
		catch(Exception e){
			e.printStackTrace();
			return null;
		}
	}
	
	public void run(){
		System.out.println("#Server [connection_start]: STARTING");
		try{
			loop:
			while(true){
				for(ClientUser s : clientSockets){
					String input = s.readLine();
					if(input.equals("stop")){
						break loop;
					}
					else{
						System.out.println("#Server [new_message]:  " + input + " [" + s.getLocalIp());
					}
				}
			}
			System.out.println("#Server [connection_end]: EXIT");
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
	
	private class UserManager implements Runnable {
		
		Thread t;
		
		private UserManager(){
			t = new Thread(this);
			t.start();
		}
		
		public synchronized void run(){
			while(true){
				try{
					Socket incoming = serverSocket.accept();
					clientSockets.add(new ClientUser(incoming));
				}
				catch(Exception e){
					e.printStackTrace();
				}
			}
		}
	}
	
	private class ClientUser {
		
		private BufferedReader messageReader;
		private Socket clientSocket;
		
		private ClientUser(Socket clientSocket){
			try{
				messageReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
			}
			catch(Exception e){
				e.printStackTrace();
			}
			this.clientSocket = clientSocket;
		}
		
		public String readLine(){
			try{
				return messageReader.readLine();
			}
			catch(Exception e){
				e.printStackTrace();
				return null;
			}
		}
		
		public String getLocalIp(){
			return	clientSocket.getInetAddress().getHostAddress();
		}
	}
}```[/spoiler]


ServerGui.java
[spoiler]```package de.skysoldier.chat;

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class ServerGui {

	JFrame f;
	ChatServer server;
	
	public ServerGui(){
		init();
		buildGui();
	}
	
	private void init(){
		server = new ChatServer(5151);
	}
	
	private void buildGui(){
		f = new JFrame();
		f.setAlwaysOnTop(true);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setSize(400, 300);
		f.setLayout(new BorderLayout());
		f.add(new JLabel(server.getLocalIp() + " : " + server.getLocalPort()));
		f.setVisible(true);
	}
	
	public static void main(String[] args) {
		new ServerGui();
	}
}```[/spoiler]


ChatClient.java
[spoiler]```package de.skysoldier.chat;

import java.io.PrintWriter;
import java.net.Socket;

public class ChatClient {
	
	Socket s;
	PrintWriter writer; 
	
	public ChatClient(int port){
		try{ 
			s = new Socket("192.168.0.119", port);
			writer = new PrintWriter(s.getOutputStream(), true);
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
	
	public void send(String s){
		try{
			writer.print(s + "
");
			writer.flush();
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
```[/spoiler]


ChatGui
[spoiler]```package de.skysoldier.chat;

import java.awt.BorderLayout;

import javax.swing.JFrame;

public class ClientGui {
	
	JFrame f;
	ChatClient client;
	
	public ClientGui(){
		init();
		buildGui();
	}
	
	private void init(){
		client = new ChatClient(5151);
	}
	
	private void buildGui(){
		f = new JFrame();
		f.setAlwaysOnTop(true);
		f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		f.setSize(400, 300);
		f.setLocation(500, 0);
		f.setLayout(new BorderLayout());
		f.setVisible(true);
		client.send("test515151:::5151");
		client.send("ab zu bio");
		client.send("stC");
	}
	
	public static void main(String[] args) {
		new ClientGui();
	}
}```[/spoiler]

[/SPOILER]

Man benötigt für jeden Input Stream eines Clients einen eigenen Thread.

Das kann doch nicht das Ziel sein?
Sagen wir bei einem Chat mit 50 leuten, dann kann ich doch nicht 50 threads auf einmal laufen lassen, oder? o.O

Das ist zumindest der einfachste Weg so etwas umzusetzen.
Will man das etwas “professioneller” machen, dann nutzt man eine Lib, z.b. netty. Da hast du die Möglichkeit eine Menge x an Clienthandlern auf y Threads zu verteilen.

mir gehts erstens darum, das allein zu machen, und zweitens, wie macht diese library das denn dann?
das muss ich ja auch irgendwie hinbekommen können

Guck doch rein :wink:

und da soll ich jetzt was finden? o.O
welche klasse macht das denn bitte?

Klar bekommt man das auch selbst hin. Ich denke das wäre auch gar nicht so kompliziert.
Das Problem an deinem Code ist, dass die readLine() Methode den aktuellen Thread blockiert bis Daten kommen. Du müsstest alle Clients in einer Liste sammeln, und in regelmäßigen Abständen diese Liste durchlaufen und prüfen ob im jeweiligen Stream Daten vorhanden sind (bspw. mit der available()-Methode). Liegen bei irgendeinem Client Daten vor, muss sich dein Thread um diesen Client kümmern. Dann würdest du mit einem Thread auskommen.

ach, available liefert mir zurück ob was neues da ist??
sowas hatte ich als antwort erwartet, danke dir ^^
probier ich gleich mal aus :slight_smile:

NIO wäre hier ein Stichwort. Allerdings ist das relativ komplex. Wird einige Zeit dauern das Prinzip zu verstehen.

Edit: Man macht natürlich keine 50 Threads auf. Der Scheduler ist dann praktisch nur noch mit dem switching der Threads beschäftigt. In der Regel hat man ein ThreadPool und verschiedene Tasks. Hat der Pool ein freies Thread, wird dieses für den neuen Task verwendet.

Danke, aber ich glaube ich bleib erstmal bei Sockets. @EikeB : Danke erstmal, ich glaube das hat funktioniert.
nun das Problem: Ich erhalte eine concurrency (oder wie das heißt) exception, obwohl ich eigentlich alle methoden die
im entferntesten sinne was mit der ArrayList clientSockets zu tun haben syncronized gemacht hab.
Wie kann das sein? Genauer:

#Server [connection_start]: STARTING
#Server [new_message]: test515151:::5151 [127.0.0.1
#Server [new_message]: ab zu bio [127.0.0.1
#Server [new_message]: stC [127.0.0.1
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at de.skysoldier.chat.ChatServer.run(ChatServer.java:49)
at java.lang.Thread.run(Unknown Source)

Also beim joinen des zweiten Clients. Woran liegt das, einer ne idee?
(liegt es vielleicht daran das ich beide über localhost starte, also auf dem selben rechner? dürfte doch eigentlich nicht, oder?)

Synchronized hilft da nur bedingt. Poste doch mal die kritischen stellen. Ich vermute dass du während des loops über die Liste Einträge hinzufügen willst.

hier - also die loop, zeile 5 tritt der fehler auf

		try{
			loop:
			while(true){
				for(ClientUser socket : clientSockets){
					String input = socket.readLine();
					if(input != null){
						if(input.equals("stop")){
							break loop;
						}
						else{
							System.out.println("#Server [new_message]:  " + input + " [" + socket.getLocalIp());
							System.out.println("msg");
						}
					}
				}
			}
			System.out.println("#Server [connection_end]: EXIT");
		}
		catch(Exception e){
			e.printStackTrace();
		}```

wo ich der liste einträge hinzufüge:

```public synchronized void run(){
			while(true){
				try{
					Socket incoming = serverSocket.accept();
					clientSockets.add(new ClientUser(incoming));
				}
				catch(Exception e){
					e.printStackTrace();
				}
			}
		}```

das ganze passiert wie gesagt nur wenn ich das if bei readline habe, wie du gesagt hast:
(edit: ist ja logisch, davor ging das programm ja gar nicht weiter ^^)

public synchronized String readLine(){
try{
if(clientSocket.getInputStream().available() > 0){
return messageReader.readLine();
}
else{
return null;
}
}
catch(Exception e){
e.printStackTrace();
return null;
}
}

[ot][QUOTE=mymaksimus]
weil mein Lehrer, der weiß das ich ein bisschen zu weit für die ef bin, das für eine gute sache für mich hielt ^^[/QUOTE]

Meiner auch, nur gibt er mir ne 3 weil ich zu viel weiss und es ihm nicht passt -.- [/ot]

[OT]

[QUOTE=groggy][ot]

Meiner auch, nur gibt er mir ne 3 weil ich zu viel weiss und es ihm nicht passt -.- [/ot][/QUOTE]

dann schreib ihm nen virus und lass C löschen mit der meldung „Sie sind selbst schuld. Nun spüren sie meine Überlegenheit.“ oder so :smiley:
nein spass ^^
[/OT]

[ot]
Da faengt es schon an: Kp wie man nen Virus schreibt ;D

Was mich am meisten abfuckt iat aber dass die Leute im Kurs, die nicht mal ne Klasse deklarieren koennen 1- 2 stehen, da(und ich zitiere) “sie die bis jetzt nur erlernten Sachen anwenden(->google) und nicht Sachen die wir noch nicht hatten”.
[/ot]

Eine ConcurrentModificationException tritt auf wenn man den foreach loop “falsch” verwendet.

Bei deinem for-loop iterierst du über eine Liste clientSockets.

Wenn nun ein neuer clientSocket zu dieser Liste hinzukommt/wegfällt, während du über die Liste iterierst gibt es die genannte Exception.
Das selbe passiert auch, wenn du in dem for-loop einen neuen Client hinzufügst oder entfernst.

Abhilfe schaft hier z.B. das nutzen einer ganz einfachen for-Schleife bei der man den Index, meist i, selbst verwaltet und dann mit clientSockets.get(i) das entsprechende Element auswählt.

Hm das ist mir schon klar warum das passiert, aber ich dachte synchronized soll da helfen…
gut ich versuchs mal mit dem index, müsste ja klappen - blöd von mir. mal schaun…

Nee, das synchronized hat damit nichts zu tun.

Eine andere Möglichkeit ist folgendes.

  if(!clientSockets.isEmpty()) {
    Socket socket = clientSockets.get(0);
    if(socket!=null) {
      clientSockets.remove(socket);
      workWithSocketLikeBefore...
      clientsockets.add(socket);
    }
  } else {
    Thread.sleep(500);
  }
}```

Wenn sockets vorhanden sind, hole den ersten, entferne ihn aus der Liste. Arbeite ihn ab und füge ihn danach wieder hinten in der Liste ein.

Hierfür würde sich auch eine BlockingQueue statt der Liste eignen und dann mit take arbeiten.

da wäre die methode mit i aber besser, oder? o.O