Cookies zijn vereist voor de werking van deze website. Schakel cookies in om deze website op de juiste manier te kunnen gebruiken. Lees meer hier.

Bestand synchronisatie protocol app

Toegevoegd op 2021-06-17 18:48:45 UTC


Project op GitHub.

Inhoudsopgave

Achtergrond

Voor het vak Netwerken op mijn opleiding kregen we de opdracht om een applicatie te ontwikkelen die bestond uit een server en client waarmee bestanden uitgewisseld en gesynchroniseerd konden worden. Denk hierbij aan een versimpelde variant van bijvoorbeeld Dropbox, Google Drive of Microsoft OneDrive.

Hierbij was één groep verantwoordelijk voor het bedenken van een protocol voor de uitwisseling van deze bestanden. De overige groepen, waaronder die van ons, waren verantwoordelijk voor het bouwen van de applicaties die volgens dit protocol werkten. Het doel daarmee was dat het niet uit moest maken of de software nu ontwikkeld was voor Windows, Mac of Linux. Zolang de applicaties zich aan het protocol hielden moesten deze met elkaar samen kunnen werken.

Het protocol

Specificaties

Communicatie tussen de client en server gaat over TCP d.m.v. requests en responses die in JSON formaat worden uitgewisseld met UTF-8 encoding. Hierbij luistert de server naar nieuwe requests om af te handelen. De client zelf stuurt alleen maar requests en luistert zelf niet naar requests van de server. Wijzigingen aan de server kant moeten door de client zelf opgevraagd worden.

De inhoud van een request wordt door de client als volgt opgebouwd:

VERB PROTOCOL_VERSION

JSON_BODY

Bijvoorbeeld:

GET idh14sync/1.0

{
    "filename": "ZXhhbXBsZS50eHQ="
}

De server leest dit request uit per karakter en detecteert wanneer de JSON uiteindelijk wordt afgesloten en het einde van de request content is bereikt. Vervolgens wordt de verb en de protocol versie gevalideerd en aan de hand van de verb wordt bepaald welke actie de server uit moet gaan voeren. Als de server een request heeft afgehandeld, met of zonder succes, stuurt deze een response terug met daarin het resultaat.

De inhoud van een response wordt door de server als volgt opgebouwd:

VERB PROTOCOL_VERSION
 
JSON_BODY

Bijvoorbeeld:

RESPONSE idh14sync/1.0
     
{
    "status": 200,
    "filename": "ZXhhbXBsZS50eHQ=",
    "checksum": "51182c5394952e8f6c52b6efcfde64259272a439",
    "content": "c29tZSBleGFtcGxlIGNvbnRuZXQ="
}

Requests

Het protocol kent de volgende soorten requests, die ook als verb worden meegegeven:

  • LIST
  • GET
  • PUT
  • DELETE

LIST vraagt om een lijst met alle bestanden op de server. Dit request stuurt een "lege" JSON mee.

Voorbeeld request JSON:

{}

GET vraagt een bestand op aan de hand van de naam. De UTF-8 bestandsnaam wordt als base64 meegestuurd in de JSON.

Voorbeeld request JSON:

{
    "filename": "ZXhhbXBsZS50eHQ="
}

PUT plaatst een bestand op de server. Hierbij wordt in de JSON het volgende meegestuurd:

  • De bestandsnaam in base64
  • De SHA-1 checksum van het bestand
  • De SHA-1 checksum van het bestand voordat deze is gewijzigd (als het bestand al bestond)
  • De content als base64

Voorbeeld request JSON:

{
    "filename": "ZXhhbXBsZS50eHQ=",
    "checksum": "51182c5394952e8f6c52b6efcfde64259272a439=",
    "original_checksum": "fbdbf2bafc9835d2267140e33a506ac424de17db",
    "content": "c29tZSBleGFtcGxlIGNvbnRuZXQ="
}

DELETE verwijderd het bestand aan de hand van de meegestuurde naam. Hierbij wordt ook de checksum meegeleverd om te controleren of het te verwijderen bestand voor zowel de client als server wel in dezelfde staat is. Zo niet, dan zorgt dit voor een conflict waardoor het bestand niet mag worden verwijderd.

Voorbeeld request JSON:

{
    "filename": "ZXhhbXBsZS50eHQ=",
    "checksum": "51182c5394952e8f6c52b6efcfde64259272a439="
}

Responses

Voor de responses kent het protocol alleen maar de verb genaamd "RESPONSE". De client weet het formaat van de JSON die wordt teruggestuurd, doordat deze overeenkomt met het type request. Ieder type response heeft altijd de een status die aangeeft of het request kon worden afgehandeld en zo niet, wat de oorzaak is.

Voor een LIST request wordt er een lijst met bestanden teruggestuurd die aanwezig zijn om de server. Per bestand wordt er een bestandsnaam en checksum mee teruggestuurd.

Voorbeeld response JSON:

{
    "status": 200,
    "files": [
        {
            "filename": "ZXhhbXBsZS50eHQ=",
            "checksum": "51182c5394952e8f6c52b6efcfde64259272a439"
        }            
    ]
}

Voor een GET request worden de naam, checksum en inhoud van een bestand teruggestuurd.

Voorbeeld response JSON:

{
    "status": 200,
    "filename": "ZXhhbXBsZS50eHQ=",
    "checksum": "51182c5394952e8f6c52b6efcfde64259272a439",
    "content": "c29tZSBleGFtcGxlIGNvbnRuZXQ="
}

Voor een PUT en DELETE request, of bij een fout, wordt alleen de status teruggestuurd.

Voorbeeld response JSON:

{
    "status": 200
}

Status codes

De volgende status kunnen worden teruggestuurd:

  • 200 (OK, als alles goed is afgehandeld)
  • 400 (BadRequest, bijvoorbeeld bij ongeldig request formaat)
  • 404 (NotFound, bijvoorbeeld bij het ophalen van een bestand wat niet bestaat)
  • 412 (FileConflict, bijvoorbeeld bij het bijwerken van een bestand waarbij de originele checksum afwijkt van de server)
  • 500 (InternalServerError, als een overwachtte fout is opgetreden)

Beperkingen

Hoewel het protocol goed was uitgewerkt, was er in het beginstadium al een keuze gemaakt die uiteindelijk voor een hoop beperkingen zorgde. Bij het bedenken van het protocol is er namelijk bepaald om met requests en responses te werken die in JSON formaat worden uitgewisseld. De inhoud van het bestand wordt hier in base64 formaat omgezet.

Het grootste probleem wat je hier mee krijgt is dat de JSON eerst in zijn volledigheid moet worden ingeladen, waarna ook nog de inhoud van het bestand van base64 naar binaire data omgezet moet worden. Pas dan kan de data naar een bestand worden weggeschreven. Dit alles neemt natuurlijk een enorme hoeveelheid geheugen in beslag. Hierdoor was het protocol niet geschikt voor grote bestanden. Sterker nog, de parser die we gebruikten om de JSON om te zetten kreeg al problemen bij het uitwisselen van bestanden boven de 50 MB!

Mogelijke verbetering

Een beter idee zou zijn geweest om eerst headers over te sturen waar informatie in staat zoals de bestandsnaam en de grootte van het bestand. Deze headers zouden dan afgesloten kunnen worden met een speciaal karakter die het einde van de headers aangeeft. Alles wat na dit karakter komt is dan de binaire data van het bestand zelf, waarbij je weet hoeveel bytes je uit moet lezen doordat dit in de headers is aangegeven. Deze data zou je dan gebufferd uit kunnen lezen en direct naar een bestand weg kunnen schrijven.

De applicaties

Onze groep was verantwoordelijk voor het bouwen van een applicaties die volgens het bedachtte protocol moest werken.

De applicaties zijn gebouwd op het .NET Framework en bestaan uit een server en client console applicaties en twee bijbehorende libraries.

Protocol library

Deze library bevat de volledige implementatie van het protocol in de vorm van een SyncServer en SyncClient class.

De SyncServer gebruikt een TcpListener om naar nieuwe TCP verbindingen te luisteren die door andere clients kunnen worden gestart. Bij een nieuwe verbinding wordt deze op een nieuwe thread afgehandeld. Daar wordt het request uitgelezen uit de TCP stream volgens de protocol specificaties en wordt deze gevalideerd en omgezet naar een object waar mee gewerkt kan worden. Afhankelijk van de verb wordt een bijbehorende callback methode aangeroepen met het request object als argument. Deze callback methodes kunnen door een server applicatie geïmplementeerd worden om vervolgens de actie af te handelen en een response te creëren. Zodra een request is afgehandeld, of er een (validatie) fout is opgetreden, zal de server de response terugsturen naar de client door de data om te zetten en naar de TCP stream te schrijven.

De SyncClient bevat de methodes om data te sturen en op te vragen van en naar een server. Bij het uitvoeren van een van deze methodes zet de client een connectie op met de server middels een TcpClient. Dit gebeurt op dezelfde thread. De request data wordt opgebouwd volgens de protocol specificaties en verstuurd naar de server door deze naar de TCP stream te schrijven. Vervolgens wacht de client op reactie van de server en leest dan de TCP stream uit om de data vervolgens naar een response object om te zetten. Een client applicatie kan deze methodes gebruiken om bestanden uit te wisselen met een server.

Shared library

Deze library bevat gedeelde utilities voor de server en client console applicaties. In deze library is een ChecksumManager terug te vinden om checksums voor bestanden te genereren, op te slaan en uit te lezen. Ook bevat de library een FileManager om met de fysieke bestanden te werken die tussen client en server worden uitgewisseld.

Server console applicatie

De server console applicatie implementeert de SyncServer uit de Protocol library. De applicatie draait continu en maakt gebruik van de callbacks uit de SyncServer class om requests af te handelen en zo fysieke bestanden uit te kunnen lezen en synchroniseren. De applicatie accepteer alleen een commando op af te sluiten. Voor de rest draait deze op zichzelf zonder input nodig te hebben van een gebruiker.

Client console applicatie

De client console applicatie implementeert de SyncClient uit de Protocol library. Ook deze applicatie draait continu, maar deze accepteert meer commando's van de gebruiker.

Bij het starten van de applicatie wordt een initiële sync uitgevoerd tussen client en server. Deze sync kijkt naar de status van alle bestanden die tussen client en server gedeeld worden en roept de juiste methodes van de SyncClient aan om bestanden toe te voegen, te verwijderen of te overschrijven. Eerst worden wijzigingen van de server afgehandeld, daarna van de client.

Na de initiële sync wordt er een FileSystemWatcher gebruikt om wijzigingen aan bestanden van de client in de gaten te houden. Wanneer wijzigingen worden gedetecteerd wordt er opnieuw een sync uitgevoerd. Deze sync kan ook geforceerd worden uitgevoerd door het "sync" commando op te geven. Wil je alleen de status van alle bestanden zien zonder te synchroniseren, dan kun je het "list" commando uitvoeren.

Wesley Donker

Software Engineer

Nederland