# SocketChannel ByteBuffer Bilder verschicken



## BaluEtDespi (12. Jan 2019)

Hallo an alle!
Ich habe ein Problem mit dem Senden von Bildern mithilfe von SocketChannels und ByteBuffern.
Da viele Bilder verschickt werden sollen, muss ich auf ein simples Protokoll zurückgreifen, das wie folgt funktionieren soll:
In ein zuvor gecleartes writeBuffer (ByteBuffer) füge ich die Größe der zu übertragenden Bilddatei ein: ByteBuffer.putLong();
Dann folgen die Daten des Bildes, die ebenfalls in das Buffer eingefügt werden. Dann wird geflippt und das writeBuffer über den SocketChannel gesendet.
Auf der Empfängerseite wird immer erst das readBuffer (ByteBuffer) gecleared. Dann wird zunächst die Länge der Bilddatei ausgelesen (ByteBuffer.getLong(). Dann wird das Limit des readBuffers auf DATEILAENGE+8 gesetzt schließlich das readBuffer in einen FileChannel gelesen. Nach diesem Vorgang wird das readBuffer wieder gecleared und auf ein weiteres Bild gewartet.
Nun das Problem: Bei der ersten Übertragung läuft alles einwandfrei. Doch beim zweiten Eintreffen von Daten im readBuffer ergibt ByteBuffer.getLong() plötzlich völlig absurde Werte, z.B. _Server erwartet ein Bild der Groesse: 3927928765588602075._
Als Quelltext sieht das ganze wie folgt aus:
Senderseite:

```
Path path = Paths.get(fileName);
            long fileSize = Files.size(path);
            System.out.println(fileName + " size: " + fileSize);
            socketChannelEntity.getWriteBuffer().putLong(fileSize);
            try{
                FileChannel fileChannel = FileChannel.open(path);
                int numRead = 0;
                int counter = 0;
               
                while((numRead = fileChannel.read(socketChannelEntity.getWriteBuffer())) > 0){
                    counter += numRead;
                    socketChannelEntity.getWriteBuffer().flip();
                    do {
                        numRead -= socketChannel.write(socketChannelEntity.getWriteBuffer());
                        socketChannelEntity.getWriteBuffer().clear();
                    } while (numRead > 0);
                }
                fileChannel.close();
                System.out.println("NIO_CLIENT: " + socketChannelEntity.getEntityName() + ": Image " + fileName + " sent: " + counter + " bytes long");
                socketChannelEntity.setCheckSum(fileSize);
                socketChannelEntity.setSendReceiveStatus(SendReceiveStatusClient.RECEIVE_CHECKSUM_IMAGE);
            } catch(IOException e) {
                e.printStackTrace();
            }
```

Empfängerseite:

```
Path path = Paths.get(outputFile);
            FileChannel fileChannel = FileChannel.open(path,
                    EnumSet.of(StandardOpenOption.CREATE,
                            StandardOpenOption.TRUNCATE_EXISTING,
                            StandardOpenOption.WRITE));
            int numRead = 0;
            int counter = 0;
            socketChannel.read(readBuffer);
            readBuffer.flip();
            checkSum = readBuffer.getLong();
            System.out.println("Server erwartet ein Bild der Groesse: " + checkSum);
            readBuffer.limit((int)checkSum+8);
            fileChannel.write(readBuffer);
            fileChannel.close();
            if(readBuffer.hasRemaining()){
                System.out.println("Ist noch was im ReadBuffer!");
            }
            prepareWriteBuffer(checkSum);
            System.out.println("NIO_SERVER: Received image.");
            sendReceiveStatus = SendReceiveStatusServer.SEND_CHECKSUM_IMAGE;
```

Hinweis: das sind jeweils die relevanten Codeausschnitte!
Ich bin für jede Hilfe dankbar!
Was mache ich falsch?


----------



## mihe7 (13. Jan 2019)

BaluEtDespi hat gesagt.:


> Was mache ich falsch?


Du ignorierst, wie viele Daten Dein Buffer tatsächlich zur Verfügung hat, indem Du das Limit auf einen "gewünschten" Wert setzt. Ich würde es mal nach dem folgendem Ansatz probieren.

Senderseite:

```
long size = fileChannel.size();
buf.putLong(size);
buf.flip();
do {
    fileChannel.write(buf);
while (buf.hasRemaining());

fileChannel.transferTo(0L, size, socketChannel);
```

Empfängerseite:

```
boolean expectLength = true;
long size = 0L;
while (socketChannel.read(buf) != -1) {
    if (expectLength) {
        if (buf.position() > 7) {
            buf.flip();
            size = buf.getLong();
            expectLength = false;
            buf.compact();
            fileChannel = ...
        }
    } else {
        buf.flip();
        while (size > 0 && buf.hasRemaining()) {
            size -= fileChannel.transferFrom(buf, 0, Math.min(buf.remaining(), size));
        }
        buf.compact();
        if (size == 0L) {
            fileChannel.close();
            expectLength = true;
        }
    }
}
```


----------



## BaluEtDespi (13. Jan 2019)

Danke für Deinen Code. Habe versucht, ihn umzusetzen:
Exception in thread "Thread-0" java.nio.channels.NonWritableChannelException
auf Senderseite. Des Weiteren verstehe ich nicht, warum Du nach einfügen der size den Buffer flipst, denn damit überschreibst Du doch diese Daten, wenn die Imagedaten aus dem FileChannel in den Buffer geschrieben werden!?
Und auf Senderseite schreibe ich doch nicht in den fileChannel, sondern möchte aus diesem LESEN, um die Daten dann in den Buffer zu wrappen.
Ändere ich die Operation in fileChannel.read(buf) werden die Daten übertragen. Das Problem ist nach wie vor die Empfängerseite, jedoch: FileChannel.transferFrom erwartet als erstes Argument einen ReadableByteChannel. Caste ich per Versuch einfach den ByteBuffer, ist dies natürlich nicht möglich.
Wie kann es anders funktionieren? Mir fehlen die Ideen.


----------



## mihe7 (13. Jan 2019)

Ach, Mist. Ich hatte das in der Nacht schon geschrieben und heute früh ungelesen reinkopiert 

1. Sender: in der do-while-Schleife musst es natürlich socketChannel heißen
2. Empfänger: da darf es kein transferFrom stehen sondern müsste ein write sein.

Theoretisch könnte man auch im Anschluss an die while-Schleife mit fileChannel.transferFrom(socketChannel, x, size); den kompletten Rest der Datei einlesen. Aber: wozu?


----------



## BaluEtDespi (13. Jan 2019)

Hey, bin damit einen Schritt weiter gekommen. Habe bei 30 Bildern (nur noch) einen Lost von 6 Bildern, alle anderen werden richtig übertragen. Habe mehrere Versuche repliziert, immer wird ein ganzes Bild geschluckt!? Ich arbeite morgen weiter dran, dann poste ich mal meinen Code.
Schon mal vielen Dank soweit!


----------



## BaluEtDespi (14. Jan 2019)

Also hier der Code der Empfängerseite.
Ergebnis: Die Filesizes werden jeweils richtig gelesen. Als Output werden einige Bilder exakt gespeichert, jedoch existieren zudem noch einige Bilder der Länge 0 ?!?? Was ist falsch? Deine letzte Condition (size == 0) tritt in einigen Fällen gar nicht ein, wenn von size insgesamt mehr Zeichen in den FileChannel geschrieben wurden, da mehr erreichbar waren.


```
boolean expectLength = true;
            boolean first = true;
            long size = 0L;
            while (socketChannel.read(readBuffer) > 0) {
                if (expectLength) {
                    if (readBuffer.position() > 7) {
                        readBuffer.flip();
                        size = readBuffer.getLong();
                        if(first){
                            first = false;
                            checkSum = size;
                        }
                        System.out.println("Server expects image of size: " + checkSum);
                        expectLength = false;
                        readBuffer.compact();
                    }
                } else {
                    readBuffer.flip();
                    while (size > 0 && readBuffer.hasRemaining()) {
                        size -= fileChannel.write(readBuffer);
                    }
                    readBuffer.compact();
                    if (size <= 0L) {
                        fileChannel.close();
                        expectLength = true;
                        System.out.println("Bei fileChannel.close angekommen.");
                    }
                }
                readBuffer.clear();
```


----------



## mihe7 (14. Jan 2019)

Du darfst nicht den ganzen Buffer in die Datei schreiben, sondern nur den Teil, der ggf. noch bis zum Dateieende fehlt. Aus dem Grund erreichst Du manchmal auch nicht den Fall size == 0.

```
size -= fileChannel.write(buf, 0, Math.min(buf.remaining(), size));
```


----------



## Xyz1 (14. Jan 2019)

mihe7 hat gesagt.:


> Math.min(buf.remaining(), size)


mihe7 , size für gewöhnlich kleiner als buf.remaining() dementsprechend würde ich schreiben: Math.min(size, buf.remaining()).


----------



## mihe7 (14. Jan 2019)

@horstiii2 wenn Du einen 4 KB Buffer verwendest, und 2 MB überträgst, ist size in aller Regel größer als remaining().


----------



## Xyz1 (14. Jan 2019)

Ja  Das wäre logisch. Aber 4 KB sind kleiner als 2 MB, damit ist size überwiegend viel kleiner als remaining().


----------



## mihe7 (14. Jan 2019)

size ist aber die Dateigröße, nicht die Buffergröße


----------



## Xyz1 (14. Jan 2019)

Sorry, dann habe ich Dich gerade falschverstanden.


----------



## mihe7 (14. Jan 2019)

Kein Problem.


----------



## BaluEtDespi (14. Jan 2019)

mihe7 hat gesagt.:


> ```
> size -= fileChannel.write(buf, 0, Math.min(buf.remaining(), size));
> ```



Hm, diese Form der FileChannel.write-Operation gibt es nicht.

FileChannel.write(ByteBuffer)
FileChannel.write(Bytebuffer, long)
Sonst noch eine Idee, wie ich begrenze, dass ich nicht den gesamten Buffer in die Datei lese?


----------



## mihe7 (14. Jan 2019)

Argh, das wird allmählich zum Ratespiel. Wenn jetzt wieder was nicht passt, muss ich es doch mal in Code umsetzen. Eine Möglichkeit sollte sein, das Limit zu setzen:

```
while (size > 0 && readBuffer.hasRemaining()) {
                        int oldLimit = readBuffer.limit();
                        int remaining = Math.min(buf.remaining(), size);
                        readBuffer.limit(readBuffer.position() + remaining);
                        size -= fileChannel.write(readBuffer);
                        readBuffer.limit(oldLimit);
                    }
```
Schön ist das nicht.


----------



## BaluEtDespi (15. Jan 2019)

Hm, so sieht der Code insgesamt aus. Aber 0Byte-Files kommen leider immer noch vor.



```
boolean expectLength = true;
            boolean first = true;
            long size = 0L;
            while (socketChannel.read(readBuffer) > 0) {
                if (expectLength) {
                    if (readBuffer.position() > 7) {
                        readBuffer.flip();
                        size = readBuffer.getLong();
                        if(first){
                            first = false;
                            checkSum = size;
                        }
                        System.out.println("Server expects image of size: " + checkSum);
                        expectLength = false;
                        readBuffer.compact();
                    }
                } else {
                    readBuffer.flip();
                    while (size > 0 && readBuffer.hasRemaining()) {
                        int oldLimit = readBuffer.limit();
                        int remaining = (int) Math.min(readBuffer.remaining(), size);
                        readBuffer.limit(readBuffer.position() + remaining);
                        size -= fileChannel.write(readBuffer);
                        readBuffer.limit(oldLimit);
                    }
                    readBuffer.compact();
                    if (size <= 0L) {
                        fileChannel.close();
                        expectLength = true;
                    }
                }
            }
```


----------



## mihe7 (15. Jan 2019)

Grr...


Spoiler: Server.java





```
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import static java.nio.file.StandardOpenOption.*;

public class Server {
    private SocketChannel channel;
    private ByteBuffer buf;


    public static void main(String[] args) throws Exception {
        new Server().run();
    }

    public void run() throws IOException {
        buf = ByteBuffer.allocateDirect(4096);
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress("localhost", 5000));
        channel = serverChannel.accept();
        receive();
    }

    private void receive() throws IOException {
        boolean expectLength = true;
        long size = 0L;
        FileChannel fc = null;
        int count = 0;
        while (channel.read(buf) != -1) {
            if (expectLength && buf.position() > 7) {
                buf.flip();
                size = buf.getLong();
                System.out.printf("Receiving %d bytes...\n", size);
                count++;
                Path file = Paths.get("out/file" + count + ".jpg");
                fc = FileChannel.open(file, WRITE, CREATE, TRUNCATE_EXISTING);
                expectLength = false;
                buf.compact();
            } else if (!expectLength) {
                buf.flip();
                while (size > 0L && buf.hasRemaining()) {
                    int oldLimit = buf.limit();
                    int remaining = (int) Math.min(buf.remaining(), size);
                    buf.limit(buf.position() + remaining);            
                    size -= fc.write(buf);
                    System.out.printf("%d bytes to go...\n", size);
                    buf.limit(oldLimit);
                }

                buf.compact();

                if (size == 0L) {
                    System.out.printf("done\n", size);
                    fc.close();
                    expectLength = true;
                }
            }
        }
    }
   
}
```






Spoiler: Client.java





```
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;

public class Client implements AutoCloseable {
    private SocketChannel channel;

    public void connect(SocketAddress addr) throws IOException {
        channel = SocketChannel.open(addr);
    }

    public void send(Path file) {
        try {
            FileChannel fc = FileChannel.open(file, StandardOpenOption.READ);
            ByteBuffer buf = ByteBuffer.allocate(8);
            long size = Files.size(file);
            buf.putLong(size);
            buf.flip();
            send(buf);
            int sent = 0;
            while (sent < size) {
                sent += fc.transferTo(sent, size - sent, channel);
            }
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    private void send(ByteBuffer buf) throws IOException {
        while (buf.hasRemaining()) {
            channel.write(buf);
        }
    }            

    public void close() throws IOException {
        if (channel != null) {
            channel.close();
        }
    }

    public static void main(String[] args) throws IOException {
        Path dir = Paths.get(args[0]);
        Client c = new Client();
        c.connect(new InetSocketAddress("localhost", 5000));
        Files.list(dir).filter(p -> p.toString().toLowerCase().endsWith(".jpg")).forEach(c::send);
        c.close();
    }
}
```


----------



## BaluEtDespi (15. Jan 2019)

Ich kann es kaum glauben!! Mit einer winzigen Änderung funktioniert es!
In der letzten if(size == 0)-Condition fehlt am Ende ein break, falls doch noch etwas aus dem Channel gelesen werden kann, != -1 also noch nicht eingetreten ist.
Ich werde noch ein paar Durchgänge replizieren, ob das Ergebnis immer das selbe ist (ist es natürlich nicht, da bei mehreren Entitäten immer anders selektiert werden kann, aber ob das Ergebnis immer ein erfolgreiches ist).
... (probiert)
Konnte erfolgreich 600 Bilder, insgesamt ca. 1,5 GB versenden.
Wow, DANKE!


----------

