<URL:http://www.docs.uu.se/~perg/course/datakom/dv96/dkomuppg.html>

Uppgift i datakommunikation

Uppdaterad 961203

Databas - klient och server

Handledning

Handledning på denna uppgift ges av Jesper Jonsson <jojo@docs.uu.se>. Jesper finns dagtid ofta i rum 1306 eller 1312.

Introduktion

Denna uppgift avser att göra dig bekant med datagram-tjänsten UDP/IP, hur man använder den i UNIX, samt med enklare Client/Server tekniker.

Förberedelser

Innan uppgiften påbörjas bör du ha läst:

Genomförande

Redovisning av uppgiften ska göras i grupper om två, och det krävs att båda deltagarna deltar aktivt i lösningen av uppgiften.

Ett separat dokument, kallat Manualer och material, innehåller:

Uppgiften i sig består av ett flertal delmoment, som bör lösas i tur och ordning. De första momenten är rena programmerings-uppgifter, medans de senare utgörs av test och analys av program och protokoll. Ni bör läsa igenom hela uppgiften innan ni börjar. Deluppgifterna är som följer:

  1. Konstruera en klient (eller varsin) och testa att den fungerar. Provkör den mot databasen bjorn.test.
    Om ni lägger in poster i denna databas, använd då ert användarnamn som prefix till nyckeln. Om ni vill stoppa in en nyckel test, kalla den då t96foo.test. Exempel:

    ./dbclient put bjorn.test t96foo.test "Hello world"

    Ni måste namnge nyckeln på detta vis eftersom alla kursdeltagare kommer använda samma databas. Om alla använder nyckeln test kommer kollisioner ofelbart att ske.

  2. Konstruera en server (eller varsin) och testa att den fungerar. Ni kan nu skapa egna databaser, och testa dessa med er klient.

    Att skriva en server, givet att man redan har en fungerande klient, torde vara ganska enkelt. Ni kan underlätta skrivandet av servern genom att redan då nu skriver klienten isolera de delar av koden som är gemensamma för server och klient.

  3. Dokumentera ert (era) system. (Den implicita dokumentationen är lika viktig som den explicita.)

    Inkludera information om hur man bygger ert system.
    (Tips: Använd make och utgå från den Makefile som är given. Då behövs bara en referens till katalogen där filerna finns.)

  4. Testkör ert program. Redovisningen innefattar att implementationen (dvs programmen) testas mot en annan implementation. Om ni har gjort varsin implementation, kan ni testa dessa mot varandra, annars måste ni kontakta en annan grupp och genomföra testet i samarbete med denna grupp.

    Modifiera testscriptet så att rätt program används, provkör det, och kontrollera resultatet.

    Skriv ut två kopior av utskriften från testet och addera följande:

  5. Analysera protokollet, och svara på de frågor om protokollet som finns i detta dokument.

Tentamen kan komma att innehålla åtminstone någon fråga som är direkt relaterad till denna uppgift, vilket gör det angeläget för er att slutföra den före tentamen.

Redovisning

En färdigställd uppgift kommer att bestå av ett flertal delar. Nedan följer en lista på alla delar som skall vara med i en färdig uppgift:

  1. Ett signerat försättsblad.
  2. En klient till en distribuerad databas, körbar på UNIX.
  3. En server för en distribuerad databas, körbar på UNIX.
  4. Dokumentation av programvaran och referenser till filerna.
  5. Redovisning av testkörningar.
  6. En analys av de brister som finns i det protokoll som du implementerat och svar på de frågor som ställts. (Se nedan.)

Den färdiga uppgiften skall lämnas i Jesper Jonssons fack på plan 4.

Klient och server - program plus dokumentation

Programmen ska vara skrivna i C för Solaris 2.4 (eller senare), och inte utnyttja några funktionsbibliotek andra än de som levereras med operativsystemet eller anvisas i uppgiftsmaterialet.

De krav vi ställer på programmen är att de ska återspegla god programmeringssed, vara adekvat dokumenterade och använda vettiga variabel- och funktionsnamn. Givetvis kräver vi också att de fungerar.

Detaljer hittar ni i kompendiet Krav på program och dokumentation.

Testkörningar

Era program ska självklart testas innan de lämnas in. Tre test-omgångar ska redovisas:

  1. Klienten ska provas mot några av våra databaser. En av dessa beter sig som om det var fel på förbindelsen. Meddelanden fördröjs, dupliceras och försvinner emellanåt. Er programvara ska hantera detta i den mån protokollet tillåter, och de fall som inte kan hanteras skall redovisas i analysen.

  2. Din klient ska provköras mot en databas på din egen server.

  3. Klienten och servern ska testas med hjälp av en annan klient och server.

För att underlätta dessa test finns en prototyp till ett test-script som gör alla de nödvändiga testen och rapporterar resultaten. Ni kommer behöva modifiera detta för de klienter/servers som ni skall testa mot.

OBS! Test-scriptet startar inte era servers, utan dessa måste redan ha startats manuellt av er.

Som nämnts ovan skall redovisningen av testen inkludera information om när testen utördes, vem/vilka som konstruerat de testade klienterna, samt signaturer från de inblandade.

Analys av protokollet

Det protokoll ni får att jobba med är allt annat än perfekt. Det finns ett flertal brister som gör det vanskligt att använda detta protokoll "på riktigt". Det finns även utökningar man skulle vilja ha, t.ex "sekundering".

Er analys ska innhålla två delar:


Databasen

Uppgiften är att bygga en distribuerad databas. Uppgiften är delvis inspirerad av DNS (Domain Name System), vilket är den tjänst på Internet som översätter mellan namn och numeriska IP-adresser och vice versa. Det program eller den tjänst som gör denna översättning brukar kallas för en nameserver, och kan väl närmast jämföras med telefonnätets nummerupplysning.

Systemet ni ska bygga kommer att vara distribuerat i den meningen att för klienten kommer det inte att spela någon roll exakt på vilken maskin den server som har hand om en viss databas befinner sig. När man vill titta på en databas, slår man i en speciell nameserver upp på vilken maskin och port den sökta databasen för tillfället befinner sig. Det betyder att den enda statiska information som behövs i systemet är vart systemets nameserver befinner sig. Skulle någon server som hanterar flera databaser bli överlastad, kan man sprida ut denna servers databaser på flera olika servrar.

Fråga 1: Finns det något uppenbart problem med detta arrangemang? Beskriv hur systemet påverkas av sin design.
Ledning: Vad händer när systemet växer? Finns det flaskhalsar?

(För den som vill veta mer om DNS rekommenderas ISC BIND Homepage <URL:http://info-sys.home.vix.com/isc/bind/index.html> som beskriver den troligen vanligaste implementationen av DNS, och dessutom har många länkar till mer information.)

Den som så önskar kan säkert dra paralleller även med World Wide Web, för vad är väl det om inte en distribuerad databas.

Protokollspecifikation

[Picture of client lookup]

Bilden ovan föreställer en uppslagning med nyckeln foo i databasen t94foo.test. De numrerade operationerna är som följer:

  1. Klienten kontaktar den maskin som har hand om databasen nameserver för att ta reda på vart databasen t94foo.test befinner sig.

  2. Servern som har hand om databasen nameserver tar fram informationen från databasfilen för nyckeln t94foo.test.
  3. Servern skickar tillbaka svaret till klienten.

  4. Klienten kontaktar maskinen/porten som specifierats i svaret från föregående förfrågan till databasen nameserver.

  5. Servern tar fram informationen från databasfilen.

  6. Servern returnerar svaret på förfrågan.

Förfrågningar
För förfrågningar och svar används samma datatyp, message. Denna består av två delar, ett huvud och en buffert. Innehåll i bufferten specifieras i huvudet, som består av fem fält: Bufferten består av max BUFSIZE tecken, och i denna skall nyckel och data läggas in. De första entrylen tecknen utgörs av nyckeln, och de därpå följande datalen tecknen är data. I alla diskussioner om bufferten kommer strängavslutande '\0' att skrivas ut explicit. I exemplet ovan skulle förfrågan till databasen nameserver se ut som följer:
head.machine  = "Hund"
head.database = "nameserver"
head.op       = DB_GET
head.entrylen = 12
head.datalen  = 0

buf           = "t94foo.test\0"
Svar på förfrågningar
Ett svar på en förfrågning består av en kopia av förfrågan med följande modifikationer: Därefter skickas svaret tillbaka till avsändaren. Svaret på förfrågan till nameservern blir i exemplet:
head.machine  = "Hund"
head.database = "nameserver"
head.op       = DB_GET | ACK
head.entrylen = 12
head.datalen  = 12

buf           = "t94foo.test\0Kamel:32500\0"
Operationerna
De tre operationernas effekt på paket har tidigare beskrivit, men nu skall vi titta på deras semantik.
DB_PUT

Denna operation skall lägga in en ny post i databasen. Argument är en nyckel, samt en sträng som skall associeras med denna nyckel. Summan av storleken av nyckel plus post får ej överstiga 1000 tecken.

I svaret returneras ej den inlaga strängen, endast nyckeln.

Operationen får ej ersätta en existerande post. Dvs, om en post med nyckel 'foo' existerar, skall DB_PUT misslyckas.

Fråga 2: Hade det spelat någon roll om man tillåtit DB_PUT att ersätta poster direkt? Jämför DB_PUT med och utan automatisk ersättning i scenariona där:

  1. Två klienter båda försöker ändra en existerande post.
  2. En klient försöker ändra en post, och en annan försöker lägga in samma post.
  3. Två klienter båda försöker lägga in samma post.

DB_GET

Denna operation hämtar den sträng, om någon, som associerats med en viss nyckel. Argument är den sökta nyckeln. Summan av storleken av nyckel och post får ej överstiga 1000 tecken.

I svaret returneras dels nyckeln, dels den sträng som associerats med den.

Operationen måste misslyckas om den sökta nyckeln ej existerar i databasen.

DB_DEL

Denna operation tar bort en post i databasen. Argument är nyckeln till den post som skall raderas.

I svaret returneras den raderade postens nyckel.

Operationen måste misslyckas om den sökta nyckeln ej existerar i databasen. Posten måste raderas så att efterföljande DB_GET-operationer misslyckas, och en DB_PUT operation kan lyckas.

Systemet

Givet protokollet ovan, har vi nu ett sätt att utföra operationer på en databas, givet att vi vet var den finns. För att underlätta detta problem skall vi införa några regler till ovanpå det givna protokollet. Man kan se dessa som ett högre nivås protokoll.

För att slippa behöva hålla reda på var alla databaser befinner sig, skapar vi en databas nameserver i vilken vi lagrar information om på vilka servrar olika databaser befinner sig. Dvs, om vi är intresserade av var databasen t94foo.test befinner sig, gör vi en förfrågan till databasen nameserver med den sökta databasens namn som nyckel. Svaret skall då bli en sträng som beskriver var den sökta databasens server finns.

I och med detta behöver vi i fortsättningen bara veta på vilken server databasen nameserver befinner sig, från denna kan vi sedan ta reda på var alla andra databaser finns.

Vi skall i nameserver-databasen lagra maskinnamn samt portnummer för den server som för tillfället hanterar den sökta databasen. Vi lagrar detta som "<maskinnamn>:<portnummer>", dvs maskinnamn och portnummer separerade av ett kolon, ":".

Exempel: Databasen t94foo.test finns på maskinen "Kamel" på port 32500, så det betyder att "Kamel:32500" skall läggas in i databasen nameserver med nyckeln t94foo.test.

Förfrågningat till en databas blir nu en tvåstegs-process, först slås den sökta databasens server-adress upp i nameserver databasen. Därefter kontaktas denna server med den verkliga förfrågan.

Namnrymd

Viktigt att hålla i minnet är att vi i praktiken har en stor databas. Om du skapar en databas kallad test, och startar en server som har hand om den, så kan alla kursdeltagare ställa frågor och modifiera den. Varje kursdeltagare kommer alltså ha konstruerat en komponent i ett större system, precis som att en enstaka WWW-server inte är hela webben, så är ingen enskild persons server hela databasen.

Sett på ett annat sätt så delar hela kursen på en namnrymd, dvs alla namn i systemet är globala. Detta har en mycket viktig implikation: Alla i systemet måste kunna modifiera namndatabasen.

Var extremt försiktig då du ändrar i nameservern. Om du ändrar på adressen till någon av test-databaserna kommer ingen att kunna testa sina program.

Kommunikationsproblem

Den underliggande tjänst vi ska utnyttja, UPD/IP, är i grund och botten opålitligt. Vad menar vi då med detta?

Eftersom det inte går att skilja på dessa fall, måste man själv ta hand om omsändning. Man får helt enkelt anta att om man inte fått svar inom en viss tid, TIMEOUT, så kom antingen förfrågan eller svaret bort.

Vad man kan göra då är att sända om förfrågan. Men detta leder till flera nya problem, bland annat:

Man kan heller inte skilja på upprepade förluster och en server som är nere eller saknar förbindelse med omvärlden. Om vi inte får svar efter RETRIES omsändningar, så antar vi att servern vi försökte kontakta är nere.

Dessa är några av de problem vi ställs inför på grund av den underliggande tjänstens otillförlitlighet. I ett protokoll som bygger på tjänsten UDP/IP måste dessa problem lösas, men vårt protkoll säger ganska lite om detta, så man får göra vad man kan. Dock får era program inte ge ett felaktigt svar till användaren. Det enda fel som accepteras är de fall då RETRIES omsändningar gjorts, och inget svar anlänt.

Fråga 3: Hur kan man hantera dessa problem inom detta protokoll, dvs genom att ändra hur man använder protokollet, snarare än att ändra protokollet i sig? Finns det några fall då man kommer misslyckas trots dessa ändringar? Skulle man ha kunnat lösa dem på ett bättre sätt om man fick ändra i protokollet? Redovisa gärna med hänvisningar till er analys av protokollet. (Tips på ett problem att börja med: Fundera på konsekvenserna av ett förlorat svar på en DB_PUT-förfrågan.)

Blockerande I/O och förlorade paket

I UNIX blockerar normalt I/O-operationer, dvs de väntar tills de lyckas, oavsett hur lång tid detta tar. Eftersom paket kan försvinna, kan vi alltså hamna i den oacceptabla situationen att vi väntar för evigt på ett svar som aldrig kommer.

Vi måste alltså kunna antingen avbryta en I/O-operation efter en viss tid, eller låta bli att utföra den tills det finns något att läsa. Hur länge ska man vänta på svar? I detta protokoll är väntetiden fördefinierad, och finns som värdet TIMEOUT i filen message.h (se Definitionsfiler för C). Om inget svar erhållits på denna tid efter en förfrågan, ska den sändas om.

Dock kan det ju vara så att servern, maskinen eller nätet mellan klienten och servern inte fungerar, så oavsett hur många omsändningar vi gör, kommer vi inte att få något svar. Därför begränsar vi oss till RETRIES omsändningar innan vi ger upp.

Nameserver

Det har ett flertal gånger nämnts att det ska finnas en nameserver i detta system, och det gör det. Information om hur man hittar den finns i nameserver.h (se Definitionsfiler för C). Maskinen ni ska koppla er till är NAMESERVER, porten är NSPORT och namnet på databasen som innehåller informationen är NSDB.

Nameservern beter sig precis som vilken annan databas som helst i systemet, förutom att den inte byter vare sig adress eller portnummer.

Sekundering

Det vore i ett sånt här system önskvärt med sekundering av databaser. Vad innebär då detta? Jo, att någon server håller en flitigt uppdaterad kopia av en databas som hanteras av en annan server. Skulle denna server gå ner, ska klienter kunna rikta sina frågor till den sekunderande servern istället. Resultat: Ett system som överlever att enstaka servrar går ner, utan att resultatet blir mer än en kort fördröjning.

Ni behöver inte implementera sekundering, det är en frivillig extrauppgift. Däremot måste ni i er analys svara på denna fråga:

Fråga 4: Hur skulle man kunna utöka protokollet med sekundering av databaser? Beskriv de nödvändiga mekanismerna. Finns det risk för baklås i din lösning? (Scenario: Server 1 hanterar databas A och en sekundär till databas B, server 2 hanterar databas B och en sekundär till databas A. Vad händer databas A och B uppdateras samtidigt?) Hur kan man undvika ett ev. baklås?


Programmen

En del manualer och material finns tillgängliga via WWW, via länkar från detta dokument.

Det material ni måste ha tillgång till, som definitionsfiler och testprogram, finns i katalogen "/stud/docs/kurs/datakom/dbuppg", tillsammans med en Makefile som troligen kan underlätta kompileringen.

Klienten

Klienten är det program med vars hjälp man använder servern. Klienten skall kunna göra uppslagningar i databasen i enlighet med protokollet. Den skall implementera följande fem kommandon:

  1. get database key
    Hämta posten med nyckel key från databasen database och skriv ut resultatet. Exempel:
    # ./dbclient get man fprintf

  2. put database key "data"
    Lägg in data i databasen database med nyckeln key. (Citat-tecknen kring data rekommenderas eftersom det gör att allt mellan dem placeras i ett argument, komplett med alla mellanslag osv.) Exempel:
    # ./dbclient put t94foo.test foo "Allan tar en kaka."

  3. del database key
    Radera posten med nyckel key från databasen database . Exempel:
    # ./dbclient del t94foo.test foo

  4. copy database1 key1 database2 key2
    Kopiera posten med nyckel key1 i databasen database1 till databasen database2 och lägg in den med nyckeln key2. Man kan givetvis kopiera från och till samma databas, om nycklarna är olika, eller med samma nyckel, om databaserna är olika. Eftersom copy lägger in en ny post, så krävs att ingen post redan har den nyckel man tänkt använda. Exempel:
    # ./dbclient copy t94foo.test foo t94foo.prov bar
    # ./dbclient copy t94foo.prov bar t94foo.prov foo
    # ./dbclient copy t94foo.prov bar t94foo.test bar
    # ./dbclient copy t94foo.prov foo t94foo.test foo
    - Detta ska misslyckas.

  5. move database1 key1 database2 key2
    Flytta posten med nyckel key1 i databasen database1 till databasen database2 och lägg in den med nyckeln key2. Man kan givetvis flytta från och till samma databas, om nycklarna är olika, eller med samma nyckel, om databaserna är olika. Eftersom move lägger in en ny post, så krävs att ingen post redan har den nyckel man tänkt använda. Skulle så vara fallet, får man givetvis inte radera den post man flyttar från. Exempel:
    # ./dbclient move t94foo.test foo t94foo.prov bar
    # ./dbclient move t94foo.prov bar t94foo.prov foo
    # ./dbclient copy t94foo.prov foo t94foo.test foo
    - OBS, copy, inte move
    # ./dbclient move t94foo.prov foo t94foo.test foo - Detta ska misslyckas
    # ./dbclient move t94foo.prov foo t94foo.test bar

För att testprogrammen ska fungera rekommenderas att ni låter er klient använda exakt den syntax som visas här.

Om fel uppstår skall ert program rapportera detta. Om servern returnerar ett fel, så bör ni skriva ut vilken operation ni försökte utföra, samt felmeddelandet servern skickade till er. Om felet upptäcktes av er klient, bör ett lämpligt felmeddelande skrivas ut.

Notera att de två sista kommandona inte har en motsvarande operation i protokollet, utan måste implementeras (med viss försiktighet i fallet move) av klienten med hjälp av de operationer som finns.

Servern

Servern är den delen av systemet som implementerar databasen. Den databas vi ska använda är extremt enkel: Givet en nyckel, som är en sträng tecken, ska en post, som är en annan sträng tecken, returneras. Jämför med en ordbok, där man givet ett uppslagsord kan få tillbaka definitionen av ordet.

Implementationen av databasen lämnas öppen, ni får alltså välja själva hur ni ska implementera den. Vill ni spara tid rekommenderar vi att ni använder ett paket kallat "ndbm" som brukar finnas på de flesta modernare UNIX-system. Mer info om detta finns i Manualer och material.

Uppstart

Servern ska ta ett eller flera argument, där varje argument är en databas som ska hanteras av databasen.

Om vi t.ex har en server kallad "dbserver" i den aktuella katalogen som vill vill ska hantera databasen "t94foo.test" så skulle vi starta den på detta vis:

Om vi dessutom vill sköta en databas "t94foo.namn" skulle vi skriva:

Servern ska när den startar:

I princip kommer din server att bete sig som en klient under uppstarten.

I drift

Er server ska lyssna efter förfrågningar på den port den annonserat genom nameservern och svara på dessa i enlighet med protokollet. Förslagvis rapporterar ni eventuella felaktiga förfrågningar.

Servern behöver inte kunna stoppas - dvs en oändlig loop som svarar på förfrågningar duger fint.