List - Einträge durch subList abtrennen?

Hi,
ich versuche gerade eine Datei mithilfe von java NIO über TCP zu übertragen. Da man bei jnio nicht weiß, wo eine Nachricht, die man überträgt anfängt und wo sie aufhört, verwende ich die ersten zwei bytes der Nachricht jeweils zur Speicherung der Länge der Nachricht. Das funktioniert auch ganz gut, allerding brauche ich dazu auf der Seite des Clients eine Liste, die die erhaltenen Bytes abspeichert, da die Nachricht evtl. auf mehrere ByteBuffer aufgeteilt wird. um diese Nachricht vom Rest der Liste dann abzutrennen verwende ich subList() anstatt einer Schleife, die die einzelnen Elemente löscht, da mir diese Lösung sauberer und schneller erschien.
Mit fortschreitender Laufzeit wird die Datenübertragung aber (gefühlt, ich habs noch nicht genau getestet) langsamer. Bei einem Blick in den Debugger wurde auch der Grund klar: subList scheint die “Elternliste” von der sie abgespaltet wurde bei “parent” abzuspeichern, was dazu führt, dass mein Arbeitsspeicher wohl ziemlich voll wird, da diese Cacheliste immer, wenn eine Nachricht übertragen wird, den anfang durch subList() abtrennt und somit jeweils die vorige Liste noch vorhanden ist…
hier mal der Code:

	int messagelength = 0;

	private void receiveBuf(ByteBuffer buffer) {
		byte[] bytes = new byte[buffer.limit()];
		buffer.get(bytes);
		for(byte b:bytes) {
			receivedbytes.add(b);
		}
		buffer.clear();
		if(messagelength == 0) {
			if(receivedbytes.size() >= 2) messagelength = bytestoint(new byte[] {receivedbytes.get(0), receivedbytes.get(1)});
		}
		int l = 0;
		while((l = receivedbytes.size()) >= messagelength && l > 0) {
			ByteBuffer buf = ByteBuffer.allocateDirect(messagelength - 2);
			for(int p = 2; p < messagelength; p++) {
				buf.put(receivedbytes.get(p));
			}
			buf.flip();
			receive(buf);
			buf.clear();
			if(messagelength != l) {
				receivedbytes = receivedbytes.subList(messagelength, l); //<<< diese Stelle
			}
			else {
				receivedbytes.clear();
			}
			if(receivedbytes.size() >= 2) messagelength = bytestoint(new byte[] {receivedbytes.get(0), receivedbytes.get(1)});
			else messagelength = 0;
		}
	}```
MUSS ich also eine Schleife mit remove() einfügen oder gibt es eine saubere Lösung?

Eine Alternative, die mir noch eingefallen ist wäre:
```			if(messagelength != l) {
				List<Byte> newlist = receivedbytes.subList(messagelength, l);
				receivedbytes.clear();
				receivedbytes.addAll(newlist);
			}```
Das mündet bei mir aber immer in einer ConcurrentModificationException.

Ähmja, subList liefert erstmal eine Ansicht (“View”) auf einen Teil der Elternliste. Änderungen in der einen haben Auswirkungen auf die andere, und man sollte aufpassen, die subList nicht durch Änderungen an der Elternliste “ungültig” zu machen.

Aber… warum speicherst du die überhaupt in einer List? Buffer ist so schön praktisch und kompakt. Und er hat eine Methode “slice”, die ziemlich genau dem “subList” entspricht - einschließlich der o.g. Semantik in bezug auf parent/child-Relationen. Aber wenn ein Datenblock fertig gelesen ist, kann/sollte der ggf. als byte[]-Array weitergereicht werden - oder, vermutlich noch besser und sauberer und flexibler: Als neuer ByteBuffer. Hängt aber auch ein bißchen davon ab, wie der dann weiterverarbeitet werden soll. (Vielleicht machst du sowas ähnliches eigentlich schon, habe den Code auf die Schnelle nicht im Detail nachvollzogen)

Naja ich habe vor allem eine Liste gewählt, da es doch eine dynamischere Struktur ist und die Anzahl der Bytes in diesem “Zwischenspeicher” sehr Situationsabhängig ist. (was kommt rein? wie lang ist die Nachricht?) Ein ByteBuffer hat im Hintergrund ja ein Array.
Ich hab noch nicht so viel mit buffern gemacht und bin mir daher recht unsicher, ich werds aber mal probieren. (Alternativlösungen würden mich dennoch interessieren)

Eine Liste bzw. ArrayList hat im Hintergrund auch einen Array. Der ist nur versteckt, und das Umkopieren (wenn der Platz nicht reicht) wird automatisch gemacht.

Wenn ich das richtig sehe, geht es ja nur darum, den ByteBuffer für EINE komplette Message an die “receive”-Methode zu übergeben. Das ganze richtig einzuordnen ist auf Basis des geposteten Codes etwas schwierig (wo kommt der ‘buffer’ her, wo geht der ‘buf’ hin usw), vielleicht gibt’s dazu genauere Infos…?

Arbeite doch einfach nur mit dem Buffer den du wegen NIO eh schon hast. Erstell dir einfach einen ByteBuffer von 65538 bytes Größe. Jedes mal wenn du Daten empfangen hast liest du die ersten beiden Bytes für die Packetgröße aus und schaust dann ob schon genügend Daten im Buffer sind. Wenn nicht genug da ist kehrst du einfach wieder zum lesen zurück. Sobald der Buffer ein komplettes Packet enthält kannst du deine Daten verarbeiten und die Daten aus dem Buffer entfernen.

Schau dir am besten einfach mal die Methoden Buffer#mark(), Buffer#reset(), Buffer#flip() und Buffer#compact() an.

Ich glaube es ging auch um die Frage, was passiert, wenn ein Paket >65 ist (und meine Frage bezog sich auf die “Besitzrechte” der buffers - dass auf dem übergebenen ‘buffer’ z.B. ein ‘clrear’ aufgerufen wird, könnte in manchen Zusammenhängen schon fragwürdig sein…), aber vielleicht klärt sich das ja, wenn der Kontext klarer wird.

Also erstmal zum Thema Zusammenhang:
Mein größeres “Rahmenprojekt” ist eine eigene OpenGL basierte Gaming-sonstwas-Engine an der ich schon etwas länger herumprogrammiere. Dafür möchte ich mir jetzt einen Netzwerk-Teil für TCP und UDP schreiben (Multiplayer, Updater, etc.). Dafür habe ich mir für TCP 3 Klassen angelegt: Server, Connecter und Client. Server und Client erben beide von Connecter, der v.a. Methoden zum Verschicken der ByteBuffer enthält. Da ich versuche, in dieser Engine möglichst aktuelle Techniken verwenden und eine möglichst gute performance erreichen möchte habe ich mich an dieser Stelle für Java NIO entschieden.
Mein erster Ansatz war es, die ByteBuffer einfach einzeln zu verschicken, da das mit JNIO aber nicht so einfach funktioniert, habe ich mir überlegt, einen Trennbyte zu verwenden - das ist dann aber beim verschicken von Dateien gescheitert (dieser vordefinierte Trennbyte kann in den Dateien enthalten sein). Schließlich bin ich dann zu der Methode gekommen, die ersten zwei Bytes als Längenangabe der Nachricht zu verwenden.
So weit, so gut. Der Codeteil oben ist letztendlich der Teil des Codes, der die empfangenen ByteBuffer verarbeitet, wozu er sie erst in einer Liste (jetzt verwende ich einen ByteBuffer) zwischenspeichern musste, da die Nachrichten ja “irgendwie” ankommen können. Diesen Zwischenspeicher arbeitet die Methode dann ab, indem sie die ersten zwei Bytes ausliest und dadurch herausfindet, wie lang die nachfolgende Nachricht ist. Wenn dieser Zwischenspeicher >= die länge der Nachricht ist, liest die Methode den entsprechenden Teil aus und gibt es an die receive(ByteBuffer buffer); Methode weiter (abstract). Dieser TCPConnecter/-Client/-Server ist übrigens abstract wodurch je nach dem ob es sich z.B. um den Updater handelt der ByteBuffer verarbeitet verwden kann.

Und ja, es funktioniert jetzt - sogar ziemlich schnell. Ich verwende jetzt einen ByteBuffer als Zwischenspeicher und die Methode sieht jetzt so aus:

		byte[] bytes = new byte[buffer.limit()];
		buffer.get(bytes);
		setReceivedBufferCapacity(receivedbytes.capacity() + buffer.capacity());
		for(byte b:bytes) {
			receivedbytes.put(b);
		}
		buffer.clear();
		if(messagelength == 0) {
			calculateMessageLength();
		}
		int l = 0;
		while((l = receivedbytes.limit()) >= messagelength && l > 0 && messagelength > 0) {
			ByteBuffer buf = ByteBuffer.allocateDirect(messagelength - 2);
			for(int p = 2; p < messagelength; p++) {
				buf.put(receivedbytes.get(p));
			}
			buf.flip();
			receive(buf);
			buf.clear();
			if(messagelength != l) {
				receivedbytes.position(messagelength);
				receivedbytes.compact();
				setReceivedBufferCapacity(receivedbytes.capacity()-messagelength);
			}
			else {
				receivedbytes.clear();
			}
			calculateMessageLength();
		}
	}
	
	private ByteBuffer setReceivedBufferCapacity(int capacity) {
		ByteBuffer temp = receivedbytes;
		receivedbytes = ByteBuffer.allocateDirect(capacity);
		receivedbytes.put(temp);
		temp.clear();
		return receivedbytes;
	}
	
	private void calculateMessageLength() {
		if(receivedbytes.limit() >= 2) messagelength = bytestoint(new byte[] {receivedbytes.get(0), receivedbytes.get(1)});
		else messagelength = 0;
	}```

Ich hoffe, ich habe es jetzt halbwegs verständlich ausgedrückt. Weitere Verbesserungsvorschläge bzgl. der Performance sind natürlich immer willkommen, aber gefühlt ist es jetzt deutlich schneller als zuvor.