Check alle échte Black Friday-deals Ook zo moe van nepaanbiedingen? Wij laten alleen échte deals zien

[Java]Sockets - Chat en File transfer

Pagina: 1
Acties:

  • alex3305
  • Registratie: Januari 2004
  • Laatst online: 21-11 22:31
Speciaal op aanraden van o.a. Woy in Coffee Corner bij deze dan mijn vraag/vragen over Sockets en Java.

Anyway. voor een studieopdracht moet ik een soort Dropbox/remote file browser achtig client-server model bouwen in Java welke tevens cross-platform moet werken. Het vak waar deze opdracht voor geldt is Operating Systems 3 en het is dus de bedoeling dat ik aan kan tonen dat ik snap en begrijp hoe Sockets in een Operating System werken.

Allereerst ben ik begonnen met het maken van een relatief simpele chat client. Deze kon berichten overzenden door middel van een PrintWriter en uitlezen met een BufferedReader. Verder had ik een implementatie gebouwd die op basis van de eerste parameter (gescheiden op spaties) kijkt welk commando uitgevoerd moet worden; zoals CD, DIR, etc. Dat ging allemaal erg voorspoedig, totdat ik nu bij de FileTransfer kant aankom.

Het probleem waar ik vooralsnog mee zit is dat ik niet goed weet hoe ik een file transfer kan starten en het goed kan scheiden van de chat functionaliteit.

Nu naar de code.

Sowieso heb ik door middel van Maven modules onderscheid gemaakt in drie projecten, Client, Server en Shared. De Shared module heeft maar drie klassen, namelijk:
  • Constants, voor ... juist
  • FileTransferProtocol, welke een read() en write() methode heeft om buffered byte[]'s te kunnen verzenden en ontvangen.
  • SocketConnection, welke een basis heeft met een abstracte read(), maar geïmplementeerde write()-methoden om gemakkelijk tekst te verzenden en ontvangen.
Het FileTransferProtocol ziet er als volgt uit:
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class FileTransferProtocol {

    private static final int BUFFER_SIZE = 1024 * 16;

    private Socket connection;

    public FileTransferProtocol(Socket connection) {
        this.connection = connection;
    }

    public void sendFile(File file) throws IOException {
        try (ObjectOutputStream outputStream = new ObjectOutputStream(this.connection.getOutputStream())) {
            long fileSize = file.length();
            long sentBytes = 0;
            byte[] buffer = new byte[FileTransferProtocol.BUFFER_SIZE];

            try (FileInputStream fileStream = new FileInputStream(file)) {
                while (sentBytes <= fileSize) {
                    fileStream.read(buffer);
                    outputStream.write(buffer);
                    sentBytes += FileTransferProtocol.BUFFER_SIZE;
                }
            }
        }
    }

    public void receiveFile(String fileName) throws IOException  {
        try (ObjectInputStream inputStream = new ObjectInputStream(this.connection.getInputStream())) {
            try (FileOutputStream fileStream = new FileOutputStream(fileName)) {
                long bytesRead = 0;
                byte[] buffer = new byte[FileTransferProtocol.BUFFER_SIZE];

                while (bytesRead >= 0) {
                    bytesRead = inputStream.read(buffer);

                    if (bytesRead >= 0) {
                        fileStream.write(buffer);
                    }

                    if (bytesRead < FileTransferProtocol.BUFFER_SIZE) {
                        fileStream.flush();
                        break;
                    }
                }
            }
        }
    }
}


En welke volgens mij op deze manier goed moet zijn. Daarnaast is er dan nog de Server welke de SocketConnection implementeert als ClientConnection, welke dan in een thread loopt en er als volgt uitziet:

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private final FileBrowsingProtocol protocol;

private final BufferedReader input;

public void read() {
    String message = "";

    try {
        while ((message = this.input.readLine()) != null) {
            if (message.startsWith(Constants.RECEIVE_FILE)) {
                this.transferProtocol.receiveFile(message.replace(Constants.RECEIVE_FILE, ""));
            } else if (message.startsWith(Constants.SEND_FILE)) {
                this.transferProtocol.sendFile(Paths.get(this.protocol.getCurrentWorkingDirectory().toString(),
                        message.replace(Constants.SEND_FILE, "")).toFile());
            } else {
                protocol.process(message);
                System.out.println(message); // debug.
            }
        }
    } catch (IOException e) {
        if (e.getMessage().equals("Connection reset")) {
            System.out.printf("Client disconnected <%s>.%n",
                    this.getConnection().getRemoteSocketAddress().toString());
        } else {
            System.err.printf("Issue when reading from client: %s%n", e.getMessage());
        }
    }
}


Zoals je al ziet gebruikt deze klasse het FileBrowsingProtocol, welke op basis van de input een process() methode aanroept en dan de input van de client verwerkt. Nu heb ik in het bovenstaande voorbeeld ook het FileTransferProtocol verwerkt, maar ik weet totaal niet of dat wel klopt, althans hoe ik het heb gedaan.

Op de client kant zou de read() methode er ongeveer hetzelfde uitzien als op de Server kant, aangezien ze allebei van SocketConnection afstammen met dezelfde input en output klassen. Verder gebeurt er op de client kant momenteel nog niet veel spannends.

Nu, wat is dan mijn vraag?!

Hoe kan ik op deze manier, of doe ik het gewoon helemaal fout, gemakkelijk en fatsoenlijk die verdomde file transfer werkend krijgen...? Ik weet namelijk totaal niet of ik op deze manier goed bezig ben of dat ik het beter anders kan aanpakken...

  • Herko_ter_Horst
  • Registratie: November 2002
  • Niet online
Om te beginnen heb ik twee vragen:
  • hoeveel sockets gebruik je op de server?
  • waarom gebruik je ObjectInput/OutputStream voor het versturen van de inhoud van een file?

"Any sufficiently advanced technology is indistinguishable from magic."


  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Op zich ben je best een eind op weg. Een van de problemen waar je zo te zien sowieso tegen aanloopt is dat je zo te zien je messages probeert op te splitsen d.m.v. CRLF ( Je doet telkens een readline ). Echter loop je daar tegen problemen aan omdat in de binary data van de file die characters ook voor kunnen komen, dus dan zou een byte stream halverwege afgebroken worden.

Voor het verzenden van messages over een stream moet je framing toepassen. Dat kun je simpel gezegd op drie manieren doen.

1. Gebruik maken van Fixed/Predictable messages. Je weet dan voordat je iets gaat ontvangen hoeveel bytes, of in wat voor formaat er moeten komen. Dit is erg inflexibel en zal je dus niet snel gebruiken.

2. Gebruik maken van delimeters, zoals jij nu eigenlijk ook doet met de CRLF. Het nadeel hierbij is dat er in de payload van de message niet de delimeter langs mag komen. Je zult alle data van de message dus moeten escapen als die mogelijkheid er wel is. Bij het ontvangen moet je dan ook weer rekening houden met deze escape sequences. ( Of het moet gegarandeerd zijn dat de delimeter niet voor kan komen, maar met binary transmissies is dat niet te garanderen ).

3. Gebruik van een header die de lengte van het bericht aangeeft. Hierdoor lees je altijd eerst een fixed format header uit, en aan de hand van de header weet je hoe je de message verder uit moet lezen.

Als je de laatste optie neemt kom je bijvoorbeeld op iets als het volgende aan ( pseudo code )
Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class MessageProtocol
{
    Stream myStream = ...;

    public MessageProtocol( Stream input ) { myStream = input; }

    public void sendMessage(byte[] messageData)
    {
        myStream.send(messageData.Length);
        myStream.send(messageData);
    }

    public void start()
    {
        StartNewThread(messageLoop);
    }

    private byte[] readMessage()
    {
        int length = myStream.readInt();
        byte[] buffer = new byte[length];

        int bytesRead = 0;

        while (buffer.Length > bytesRead)
        {
            // read( buffer, startIndex, bytesToRead ) => Aantal Bytes gelezen
            bytesRead += myStream.Read(buffer, bytesRead, buffer.Length - bytesRead);
        }

        return buffer;
    }

    private void messageLoop()
    {
        while (running)
        {
            byte[] message = readMessage();

            onMessageReceived(message);
        }
            
    }
}

Nu krijg je alleen nog maar binary messages binnen, deze zul je dan nog moeten vertalen naar daadwerkelijk Commando's of Responses. Dat kun je doe d.m.v. serialisation/deserialization. Eventueel kun je in de header ook nog informatie stoppen over wat voor message het is, om je zo te helpen bij wat voor message het is.

[ Voor 9% gewijzigd door Woy op 17-06-2014 13:03 ]

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


  • alex3305
  • Registratie: Januari 2004
  • Laatst online: 21-11 22:31
Ik gebruik 1 Socket per client op de server én 1 Socket per client op de client. Deze pass ik netjes door wanneer nodig.

Geen idee eigenlijk. Het probleem nu is vooral dat ik, omdat het een schoolopdracht is, vooral moet begrijpen hoe het e.e.a moet werken zoals byte[]'s versturen, buffering en dergelijken.



@Woy, bedankt voor deze input. Zo'n voorbeeld had ik toevallig inmiddels ook op SO gevonden en daar wilde ik opnieuw mee beginnen. Aangezien het e.e.a niet lukt wil ik namelijk even back-to-basics voordat ik het weer verder ga bouwen. En dat is in dit geval binaire- en text-data kunnen verzenden over een Socket.

Ik ga er in ieder geval naar kijken meespelen en als het lukt, laat ik het weten :).

  • Herko_ter_Horst
  • Registratie: November 2002
  • Niet online
Een andere optie (naast die van Woy) is gebruik maken van een tweede socket voor het verzenden/versturen van de files.

"Any sufficiently advanced technology is indistinguishable from magic."


  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
alex3305 schreef op dinsdag 17 juni 2014 @ 13:03:
En dat is in dit geval binaire- en text-data kunnen verzenden over een Socket.
In dit geval verstuur ik puur binaire data. Afhankelijk van wat je wil versturen kun je daar natuurlijk extra betekenis aan hangen. Als je altijd string commando's verstuurd kun je deze byte array gewoon over zetten naar een string.

Wat je bijvoorbeeld kunt doen is in de header nog een tweede int meesturen die aangeeft welke message het is die je verstuurt.

Verder kun je het protocol natuurlijk ook nog verder uitbreiden dat er aan het eind van de message bijvoorbeeld nog een checksum over de data zit ( Voor TCP/IP is dit vaak niet nodig, aangezien die al voor error correctie zorgt, maar het kan wel helpen om fouten in bijvoorbeeld de versturende/ontvangende code te detecteren. )

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


  • Woy
  • Registratie: April 2000
  • Niet online

Woy

Moderator Devschuur®
Herko_ter_Horst schreef op dinsdag 17 juni 2014 @ 13:04:
Een andere optie (naast die van Woy) is gebruik maken van een tweede socket voor het verzenden/versturen van de files.
Dat wou ik inderdaad ook nog opperen, bijvoorbeeld het FTP protocol doet het op die manier. Die stuurt het commando om een file te downloaden, en als antwoord daarop krijgt hij terug op welke poort hij een nieuwe verbinding op kan zetten. Over die verbinding gaat dan alleen de file die gedownload moet worden, hierover hoef je dan niet perse meer aan message framing te doen, want de hele stream van begin tot eind is gewoon de file.

Voordeel hierbij is dat je het protocol over het command kanaal eenvoudiger kan houden, en later eventueel zelfs files paralel aan elkaar kan versturen. Maar het maakt je opzet in eerste instantie natuurlijk wel wat complexer.

“Build a man a fire, and he'll be warm for a day. Set a man on fire, and he'll be warm for the rest of his life.”


  • alex3305
  • Registratie: Januari 2004
  • Laatst online: 21-11 22:31
Nogmaals bedankt voor alle input. Alhoewel ik het nog niet compleet uitgewerkt heb, is het mij inmiddels wel gelukt om wat mooie klassen te maken welke de basisfunctionaliteit leveren, namelijk het verzenden en ontvangen van byte-streams (array's). Daar ben ik al heel blij mee, want nu kan ik verder naar het uitwerken van tekst (wat er al voor een gedeelte in zit) én het verzenden en ontvangen van binaire data.

Aangezien het wat veel code is om hier direct te plaatsen, heb ik een GitHub repo aangemaakt die door middel van een Maven project een client en server bouwt zodat het zelf ook te testen is. Hieronder heb ik wel de essentiële code toegevoegd (ter referentie, zonder commentaar). Momenteel is het nog niet meer dan een veredelde echo-dienst, maar dat is natuurlijk vrij gemakkelijk uit te breiden. Commentaar of opmerkingen hoor ik natuurlijk graag, maar ik ben vrij zeker van mijn zaak nu. Het enige waar ik nog over twijfel is de sendBytes(byte[], int, int) methode, volgens mij ondersteund dat ding namelijk nu geen fatsoenlijke buffered versturen. Maar dat zou natuurlijk ook verder in de server/client kunnen.

Overigens was jouw code Woy erg sterk en heeft mij veel geholpen, naast de StackOverflow link die ik in de README van de repo heb gezet.

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class BasicByteStream {

    private DataInputStream inputStream;

    private DataOutputStream outputStream;

    private Socket socket;

    public BasicByteStream(Socket socket) {
        this.socket = socket;
    }

    public byte[] read() throws IOException {
        this.checkAndCreateStreams();

        int length = this.inputStream.readInt();
        byte[] buffer = new byte[length];
        int bytesRead = 0;

        while (length > bytesRead) {
            bytesRead += this.inputStream.read(buffer, bytesRead, length - bytesRead);
        }

        return buffer;
    }

    public void sendBytes(byte[] bytes) throws IOException {
        this.sendBytes(bytes, 0, bytes.length);
    }

    public void sendBytes(byte[] bytes, int start, final int length)
            throws IOException, IllegalArgumentException, IndexOutOfBoundsException {
        if (length <= 0) {
            throw new IllegalArgumentException("Negative length not allowed.");
        }

        if (bytes == null || bytes.length < 0) {
            throw new IllegalArgumentException("Bytes to be send cannot be null or empty.");
        }

        if (start < 0 || start >= bytes.length) {
            throw new IndexOutOfBoundsException("Start index out of bounds: " + start + ".");
        }

        this.checkAndCreateStreams();

        this.outputStream.writeInt(length);
        if (length > 0) {
            this.outputStream.write(bytes, start, length);
        }
    }

    private void checkAndCreateStreams() throws IOException {
        if (this.inputStream == null) {
            this.inputStream = new DataInputStream(socket.getInputStream());
        }

        if (this.outputStream == null) {
            this.outputStream = new DataOutputStream(socket.getOutputStream());
        }
    }

}

  • Johnnei
  • Registratie: Augustus 2011
  • Laatst online: 24-08 18:54

Johnnei

Uuhmmm....

Voor de byte[] read() methode zou ik gebruik maken van de readFully(byte[]) methode van DataInputStream. Die functie bespaard het zelf moeten controleren of alle bytes zijn gelezen. Het voorkomt ook een mogelijke oneindige loop doordat de read functie van InputStream -1 returned als de "End of File" is bereikt.

[ Voor 0% gewijzigd door Johnnei op 18-06-2014 22:45 . Reden: typo ]

They blur the lines and lead the way, their way!

Pagina: 1