Distribuerat generiskt ramverk för simultan textredigering via HTTP(S)/JSON

Full text

(1)

Institutionen för datavetenskap

Department of Computer and Information Science

Examensarbete

Distribuerat generiskt ramverk för simultan

textredigering via HTTP(S)/JSON

av

David Granqvist

LIU-IDA/LITH-EX-A—11/030—SE

2011-09-10

(2)

Examensarbete

Distribuerat generiskt ramverk för

simultan textredigering via

HTTP(S)/JSON

av

David Granqvist

LIU-IDA/LITH-EX-A—11/030—SE

Handledare: Emil Styrke

Examinator: Christoph Kessler

(3)

Denna rapport visar hur man kan designa ett generiskt ramverk över HTTP/JSON för textredigerings-/programmerings-samarbete över internet. Ramverket implementeras i form av en server, en webbklient och ett insticksprogram i en textredigerare. Prestandan samt användarupplevelsen av implementering-en utvärderas och vi kommer fram till att implemimplementering-entationimplementering-en fungerar och känns till stor del naturlig att använda. Prestandan skulle kunna blir bättre genom att minska belastningen på servern genom att optimera samt flytta delar av den designade algoritmen ut till klienterna. Ramverket gick att använda men saknade många hjälpmedel för att användaren bättre skulle kunna kommunicera genom samarbets-verktyget. Designen är kort sagt mer generisk än andra liknade samarbetssystem, men samtidigt saknas många väsentliga funktioner, vilket öppnar för framtida utbyggnader av verktyget.

Nyckelord: Samarbete, textredigering, parprogrammering, HTTP, JSON, operationstransform, COT, GOT, dOPT, distribuerade system

(4)

Innehåll

1 Inledning 4 1.1 Språk . . . 4 1.2 Problembeskrivning . . . 4 1.3 Syfte . . . 4 1.4 Kravspecifikation . . . 5

1.5 Översikt över rapporten . . . 6

2 Bakgrund 7 2.1 Överensstämmelse mellan textredigerings-klienter . . . 7

2.1.1 Konvergens . . . 8

2.1.2 Kausalitetsbevarande . . . 8

2.1.3 Intentionsbevarande . . . 10

2.2 Samtidighetskontroll, olika lösningssätt . . . 10

2.2.1 Turas om . . . 10 2.2.2 Låsning . . . 11 2.2.3 Serialisering . . . 11 2.2.4 Kausal ordning . . . 11 2.2.5 Transformation . . . 13 2.2.6 WOOT . . . 13 2.3 Operationstransformer . . . 13

2.3.1 Distribuerad Operationstransform (dOPT) . . . 16

2.3.2 OT för strängar . . . 17

2.3.3 Kontext-baserad operationstransform (COT) . . . 20

(5)

3 Design 26 3.1 Gränssnitt . . . 26 3.1.1 Definitioner . . . 26 3.1.2 Klientens gränssnitt . . . 27 3.1.3 Serverns gränssnitt . . . 30 3.2 Nätverk/Protokoll . . . 33 3.3 Algoritmer . . . 37 3.3.1 Övergripande operationshanteringsalgoritm . . . 37 3.3.2 Reviderade operationstransformer . . . 38 4 Implementation 41 5 Resultat 43 5.1 Användarutvärdering . . . 43 5.2 Prestanda . . . 47 6 Diskussion 49 6.1 Användarperspektiv . . . 49 6.2 Prestanda . . . 50

6.3 Jämförelse med andra system . . . 50

7 Slutsatser 52

A Utvärderingsuppgift 54

(6)

Kapitel 1

Inledning

Detta är en rapport för examensarbetet ”Distribuerat generiskt ramverk för simultan textredigering via HTTP(S)/JSON”. Rapporten börjar med en problembeskrivning och metod som beskriver vilket problem som ska lösas respektive på vilket sätt lösningen ska utvärderas. Därefter ges en bakgrund som är en viktig grund för att förstå lösningen på problemet som kommer i design-kapitlet. Sist men inte minst kollar vi på resultaten som vi utvärderar och drar slutsatser av.

1.1 Språk

Denna rapport är skriven på svenska. All refererad litteratur är däremot skriven på engelska och språket är ibland mycket domänspecifikt, det har därför ibland varit svårt att översätta uttryck till svenska. För att inte meningen av uttryck ska gå förlorade har fotnoter med engelsk översättning1 lagts till på

lämpliga ställen. De delar av rapporten som är programkod är för enkelhets skull och av tekniska skäl på engelska.

1.2 Problembeskrivning

Vid parprogrammering på distans (över Internet) kan det för programmerarna vara användbart att kunna redigera koden man arbetar med samtidigt. Det finns idag en hel del produkter på marknaden som är gjorda för just detta. Dessa lösningar har däremot begränsningen att de är specifika implementationer i form av ett insticksprogram till ett visst programmeringsverktyg, ett separat verktyg, eller som en webbtjänst på en speciell sida. Samarbetslösningar som är designade för att användas både på webben och som insticksprogram i kodredigeringsprogram finns inte.2Lösningar som har insticksmoduler för fler

än ett kodredigeringsverktyg har heller ej påträffats.

1.3 Syfte

Syftet är att designa ett generiskt ramverk för simultan textredigering över Internet. Ett ramverk som som ska gå att implementera på webben och som insticksprogram i textredigerare, med en klient/server-modell som grund. Designen ska prioritera liten implementationsinsats framför funktionsrikedom, för att det ska

1Eng. Footnotes with English translation

(7)

vara så enkelt som möjligt att implementera nya klienter. Ramverket riktar sig till de mjukvaruutvecklare som har ett behov av att parpogrammera och redigera samma kod på distans.

1.4 Kravspecifikation

Som ett konceptbevis ska ramverket implementeras i två klienter, ett i form av insticksprogram till en texteditor, och en klient i webbläsare. Därefter ska implementationen utvärderas ur användar- och prestanda-perspektiv.

Ramverket ska vara en specifikation för vilket gränssnitt som krävs av textredigerings-applikationen, vilken samarbetsalgoritm som behöver implementeras, samt hur protokollet som ska användas för kom-munikationen mellan klienter och server ska var utformat. Det ska vara möjligt att enkelt skapa klient-implementationer utifrån dessa specifikationer som både insticksprogram och i webbläsare med Javascript-stöd. Designen av ramverket ska vara gjord på ett sådant sätt att så stor del som möjligt av konstruktionen sitter på serversidan vilket då minimerar implementationskraven per klient.

Protokollet som ska användas för kommunikationen mellan klient och server ska för enkelhets skull vara densamma för både webbklienter och insticksprogram och byggas ovanpå HTTP3 alternativt HTTPS4

med JSON5[1] för att strukturera upp datautbytet. Då HTTP- och JSON-stöd ofta finns tillgängligt i de

flesta programmeringsspråk genom något programbibliotek så borde det inte vara problem att ha dessa som underliggande middleware i insticksprogram. Dagens webbläsare har stöd för JSON och trafiken skickas normalt över HTTP(S), för andra protokoll än HTTP(S) skulle exempelvis Adobe Flash6 eller

Java Applets7 krävas. Ovanpå HTTP(S)/JSON ska det designas ett lämpligt protokoll för att hantera

kommunikationen som krävs av ramverket.

Varför vi redan nu begränsar oss till att köra en klient/server-modell och inte ska utreda alternativ så som en klient/klient-modell är då vi ska använda oss av webbläsare som klienter och dessa inte har stöd för direkt kommunikation mellan klienter, utan kommunikation måste alltid gå via en webbserver då det är så HTTP-protokollet är uppbyggt.

Det existerar samarbetsbibliotek (libinfinity8, Apache Wave9), så det skulle vara möjligt att utgå från

dessa. Däremot så är dessa bibliotek implementationer för specifika plattformar och inte en definition som går att implementera på vilket plattform där man har behovet. Därför vill vi istället skapa samarbets-ramverket från grunden så att det blir så minimalt som möjligt och så att det går utifrån definitionerna implementera ramverket mot valfri textredigerare med tillräckligt insticksprogramsstöd och på valfri plattform.

För att utvärdera hur de implementerade klienterna fungerar och upplevs för en programmerare ska en grupp användare få testa verktyget genom att genomföra en mindre programmeringsuppgift tillsammans. Därefter ska användarnas intryck och åsikter sammanställas och slutsatser om hur användarna upplever verktyget ska göras.

Långa fördröjningar mellan uppdateringar är något som kan skapa frustration för användaren. Dessa fördröjningar ska mätas när en eller fler klienter är anslutna och gör förändringar samtidigt. En jämförelse mellan antal klienter och fördröjningarnas storlek ska göras, detta för att avgöra om ett stort antal klienter kan försämra användarupplevelsen eller om den stannar på samma nivå.

För att utvärdera att den text man redigerar stämmer överens när alla klienter inte har några ändringar

3HTTP står för Hypertext Transfer Protocol, protokollet som används för webbtrafik 4HTTPS är krypterad HTTP

5JavaScript Object Notification, lättviktat format för datautbyte 6http://en.wikipedia.org/wiki/Adobe_Flash

7http://en.wikipedia.org/wiki/Java_applet 8http://infinote.org

(8)

kvar att utföra, ska manuella tester göras samt automatiska tester skrivas. De automatiska testerna ska täcka upp så många testfall som möjligt.

1.5 Översikt över rapporten

Rapporten kommer börja med en bakgrund (kapitel 2) som kommer gå igenom den nödvändiga teorin som sedan används i designen av ramverket i kapitel 3. Därefter går vi kort igenom hur designen im-plementerades (kapitel 4) och resultaten (kapitel 5) av den prestanda- och användar-utvärdering som genomfördes. Resultaten diskuteras i kapitel 6 och slutsatser från diskussionen dras till sist i kapitel 7.

(9)

Kapitel 2

Bakgrund

Detta kapitel berättar om teorin bakom lösningarna till problemet som denna rapport handlar om. Då det är ett samarbetsverktyg mellan textredigerare som designats, kommer största fokus i detta kapitel vara på just samarbetsalgoritmer för text.

Först ska vi i avsnitt 2.1 definiera tre nödvändiga villkor för att anse att samarbetsklienterna stämmer överens. Dessa tre egenskaper kommer vi sedan använda i en jämförelse mellan olika sätt att lösa samtida textredigeringar, som presenteras i avsnitt 2.2, för att ge läsaren en helhetsbild över vilka alternativ som finns för samtidighetskontroll. Avsnitt 2.2 är inte nödvändig för att förstå designen, men kan vara intressant för att få en helhetsbild.

Avsnittet 2.3 om handlar om operationstransformer, vilket är den typen av samtidighetskontroll vi valt att utgå från för designen av samarbetsramverket. Vi kommer att bygga vidare på de algoritmer som finns i denna sektion, så läsaren bör skaffa sig en förståelse för hur algoritmerna fungerar.

Sist avslutar vi bakgrundskapitlet med att beskriva hur datautbytesformatet JSON fungerar, vilket vi kommer bygga nätverks-protokollet på i kapitel 3.

2.1 Överensstämmelse mellan textredigerings-klienter

Enligt Sun m.fl. [11] finns det tre viktiga delproblem att lösa för att uppnå överensstämmelse1 mellan

klienter, nämligen divergens2, brott mot kausaliteten3 och brott mot användares avsikt4. Omvänt, för

att lösa problemen vill man uppnå konvergens5, kausalitetsbevarande6 och intentionsbevarande7.

Dessa tre egenskaper utgör tillsammans en överensstämmelsemodell8. Denna modell brukar förkortas

CCI-modellen. Det finns även andra modeller så som CC-modellen [3].

1Eng. consistency 2Eng. divergens problem 3Eng. causality violation problem 4Eng. intention violation problem 5Eng. convergence

6Eng. causality preservation 7Eng. intention preservation 8Eng. consistency model

(10)

2.1.1 Konvergens

Konvergens innebär att alla klienters text stämmer överens när alla textoperationer från alla klienter utförts på alla klienter. När användarna inte längre gör nya textoperationer, och dessa operationer har nått och utförts av alla klienter så ska den resulterande texten vara densamma. För enkelhets skull antar vi att vi har följande operationer på teckennivå (vi kommer senare även ha operationer med strängar):

• INS[P,X], sätter in tecknet X på position P i textsträngen. • DEL[P,X], tar bort tecken X på position P i textsträngen.

Position P utgår i båda fallen från att strängen är nollindexerad, dvs att första positionen i strängen är 0.

Figur 2.1: Konvergens uppnås efter att alla operationer utförts.

Ett enkelt exempel som visar när konvergens uppnås ses i figur 2.1, där både Klient A och B har samma textsträng efter att båda har kört både operation OPA och OPB. Om däremot textsträngarna skiljer

sig åt som i figur 2.2 så har vi istället fått divergens, vilket i det här fallet beror på att OPB inte tagit

hänsyn till den förskjutning som OPA skapat.

2.1.2 Kausalitetsbevarande

Kausalitetsbevarande innebär att operationer ska utföras i sin naturliga ordning. På grund av icke-deterministiska kommunikationsfördröjningar skulle nätverkspaket och därmed operationer kunna hamna i oordning. Om denna egenskap inte skulle gälla skulle användare t.ex. kunna uppleva att ett svar på en fråga lades till före själva frågan. Som exempel har vi figur 2.3, där Klient C får svaret ”Jag är döden” före frågan ”Vem är du?”. För att skapa en ordning mellan operationer kan Lamport-tidsstämplar [6] eller vektorklockor [4] användas. Observera att operationerna i figur 2.3 med flit saknar position, då detta texternas position inte ger något mervärde för i diskussionen att illustrera kausalitetsbevarande.

(11)

Figur 2.2: Konvergens uppnås inte efter att alla operationer utförts.

(12)

2.1.3 Intentionsbevarande

Intentionsbevarande innebär att användarnas avsikter ska bevaras. Kravet på konvergens är nödvändigt men inte tillräckligt för att bevara användares intentioner. I figur 2.4 har klient A för avsikt att lägga till X mellan A och B och klient B har för avsikt att sätta in Y mellan B och C. I exemplet hamnar Y dock mellan X och B, vilket i för sig är på position 2, men inte alls vad användaren på klient B hade i åtanke. Som vi ser uppnås dock konvergens. I figur 2.1 utförs samma operationer som i figur 2.4 men användarnas intentioner är bevarade.

Figur 2.4: Konvergens uppnås men alla användares intentioner bevaras ej.

2.2 Samtidighetskontroll, olika lösningssätt

När flera användare redigerar dokument samtidigt från flera ställen behövs någon form av mjukvara som förmedlar användares ändringar och ser till att alla användare hela tiden ser en uppdaterad, om än något fördröjd, version av dokumentet. För att hantera synkroniseringen mellan klienter behövs någon form av samtidighetskontroll9. Vi kommer nedan gå igenom några vanliga angreppssätt för att skapa

samtidighetskontroll.

2.2.1 Turas om

Enklaste formen av samtidighetskontroll är att låta användarna turas om. Alla användare ser ändringarna som sker, men enbart en användare i taget kan redigera dokumentet, användaren som har stafettpinnen är den som får redigera. När en annan användare vill redigera måste stafettpinnen lämnas över innan användaren får påbörja redigering. Sun m.fl. [11] nämner att de tre överensstämmelseproblemen aldrig uppstår då enbart en användare åt gången redigerar dokumentet, men å andra sidan har man aldrig riktig simultan redigering.

(13)

2.2.2 Låsning

Greenberg och Marwood [5] nämner låsning som ett alternativ. Man kan använda sig av icke-optimistisk låsning10, vilket skulle innebära att en klient/användare begär att få ensamrätt för ett objekt och därefter

om den får tillåtelse får börja redigera objektet. Vid textredigering skulle ett objekt lämpligtvis kunna vara en rad eller ett stycke i ett dokument, för att illustrera principen låter vi varje objekt vara rader. Låsningen skulle då separera användares ändringar så att konflikter inte kan uppstå. Varje gång en användare vill redigera en rad så måste denne invänta att erhålla låset innan den kan börja redigera, vilket skapar en fördröjning då användaren inte kan göra några ändringar. Om användaren inte erhåller låset så kommer den inte heller kunna redigera raden förrän låset är tillgängligt.

Ett annat alternativ är optimistisk låsning11 vilket är likt icke-optimistisk låsning men med skillnaden

att en användare kan börja redigera raden innan användaren erhållit låset. Låt oss säga att vi har två användare, användare A och användare B. Användare A upplever inte någon fördröjning och kan påbörja redigering direkt och om användare A erhåller låset fortsätta redigeringen. Om användare A däremot inte erhåller låset, eftersom användare B erhållit det, så uppstår en konflikt. En konflikt som antingen kan lösas genom att användarens ändringar görs ogjorda eller att användare A:s och användare B:s ändringar sammanfogas på lämpligt sätt. Om man antar att användare inte begär låsning för samma rad särskilt ofta kan optimistisk låsning vara lämplig.

Sun [10] anser att låsning inte avhjälper något av överensstämmelseproblemen (om inte låsningen gäller hela dokumentet, dvs man turas om), men att en optimistisk valbar låsning kan vara ett bra komplement om användare vill göra delar av ett dokument exklusiva för ett tag. Den intresserade läsaren får gärna läsa artikel[10] där det förklaras varför han anser det.

2.2.3 Serialisering

Serialisering är ytterligare ett sätt att skapa samtidighetskontroll [11][5]. Serialisering bygger på att när klienterna skickar händelser till varandra så ser serialiseringen till att händelser sker i en korrekt ordning. Vanligtvis används Lamport-tidsstämplar [6] eller vektorklockor [4] för att skapa en total ordning mellan händelser. Serialisering kan även den vara mer eller mindre optimistisk.

Greenberg och Marwood [5] nämner att en icke-optimistisk serialiseringsteknik ser alltid till att händelser är seriella genom att invänta föregående händelse innan nästa skickas, se figur 2.5. Detta skulle kunna innebära en rätt långsam exekveringstid för en lång sekvens av händelser.

En optimistisk serialisering klarar av att ta emot händelser i oordning, figur 2.6, vilket gör den snabbare än den icke-optimistiska varianten men den måste då upptäcka och reparera inkonsekvensen som uppstår. Reparation av inkonsekvensen skulle t.ex. kunna ske genom att systemet backar till tillståndet precis innan oordningen uppstod och därefter exekvera händelserna i ordning. Optimistisk serialisering är bra om konflikterande händelser sällan kommer i oordning.

Sun m.fl. [11] anser att serialisering löser divergensproblemet men att de andra två problemen inte går att lösa genom serialisering.

2.2.4 Kausal ordning

Samtidighetskontrollen ser till att kausal ordning12upprätthålls genom att använda t.ex. vektorklockor[4].

Operationer genereras och exekveras samtidigt, men ordningen de exekveras i är bestämd av deras naturliga kausala ordning.

10Eng. non-optimistic locking 11Eng. optimistic locking 12Eng. Causal Ordering

(14)

Figur 2.5: Två icke-optimistiskt serialiserade händelser.

(15)

Sun m.fl. [11] nämner att detta enbart löser kausala ordningsproblemet. Detta är en ”klassisk” teknik som används av många distribuerade datorsystem, men de gruppkommunikationstjänsterna dessa system tillhandahåller är för tunga för att användas i realtidssamarbetsapplikationer.

2.2.5 Transformation

Ellis och Gibbs [3] föreslår att operationer genereras och exekveras samtidigt, men operationer trans-formeras innan de körs. Exekvering av samma uppsättning korrekt transformerade operationer ska ge samma dokumenttillstånd (samma text) oavsett ordningen de exekveras i.

Sun m.fl. [11] nämner att transformation är tillräckligt för att bevara användares intentioner, och till-sammans med kausal ordning uppnås både konvergens och bevarande av kausalitet. Reaktionstiden för användaren är också god då lokala operationer kan köras direkt efter användaren genererat dem. Närmare detaljer om hur transformation fungerar finns i sektion 2.3.

2.2.6 WOOT

Oster m.fl. [7] har skapat en modell, WOOT, anpassad för P2P-nätverk. Jämfört med andra decentrali-serade angreppssätt använder inte WOOT vektorklockor [4] för att säkerhetsställa konvergens, kausalitet och användares avsikter. WOOT är mer skalbart än tekniker som använder vektorklockor, då vektorkloc-kor hela tiden ökar i storlek för varje ny användare som läggs till.

WOOT använder sig av en teknik där man låter varje tecken i ett dokument få ett unikt ID-nummer. Varje klient får också ett unikt id. Varje tecken består av en fem-tupel <tecknets id, tecknets alfabetiska värde, synlighetsflagga, id av föregående tecken, id av nästa tecken>.

När ett nytt tecken läggs in i dokumentet, så bestäms det nya tecknets position genom att operationen talar om mellan vilka två tecken det nya tecknet ska placeras. Id av nästa och föregående värde uppdateras därefter för de tre tecknena till lämpliga värden.

Om två operationer samtidigt begär att få sätta in ett nytt tecken mellan samma tecken i dokumentet, bestäms ordningen av dessa tecken beroende på vilken klient operationerna kommit från, detta kräver en totalordning mellan klienterna.

När ett tecken ska raderas sätts istället synlighetsflaggan till falskt. På så sätt kan teckeninsättningar som skett ovetandes om att radering gjorts ändå placeras korrekt, dvs på samma sätt som om tecknet inte skulle vara raderat.

Då operationerna kan utföras lokalt direkt så är responstiden låg. Mer detaljer går att läsa i artikeln [7].

2.3 Operationstransformer

Som beskrivet i avsnitt 2.2.5, så är transformationer ett bra sätt att lösa samtidighetskontroll på då de både ger bra respons och bevarar överensstämmelse mellan klienterna på ett bra sätt. I detta avsnitt kommer vi koncentrera oss på grunderna för operationstransformerna och gå igenom de varianter som har använts i implementationen för detta examensarbete.

Den noggranne läsaren kan se att även WOOT skulle ge bra respons och hanterar överensstämmelsevill-koren på ett tillfredsställande sätt. Varför vi väljer OT (Operationstransform) framför WOOT är främst

(16)

då WOOT kräver extra minne i form av att en fem-tupel för varje tecken måste lagras. Fem-tuplarna blir förmodligen mer komplicerade för att på ett bra sätt koppla mot den textbuffer man redigerar i, där OT inte kräver några extra data mot de tecken som finns i textbuffern. OT kräver bara att vissa operationer sparas för en kortare tid tills de inte behövs längre. OT är även mer beprövat och ofta använts i liknande samarbetsverktyg (ex. SubEthaEdit13, CoWord14, ACE15, Gobby16, Etherpad17 m.fl.).

Vid operationstransformering så transformerar man en operation mot en annan operation. Dessa ope-rationer kommer från olika klienter och transformeringen är ett sätt att anpassa en operation från en klient med hänsyn till en operation som exekverats på en annan klient.

Ett OT-system består typiskt av tre lager:

• Transformationskontroll-algoritmer18, vilka bestämmer vilka operationer som ska transformeras mot vilka.

• Transformationsfunktioner, vilka bestämmer hur två primitiva operationer ska transformeras mot varandra.

• Transformationsegenskaper och villkor, vilka delar upp ansvar mellan transformationskontroll-algoritmer och transformationsfunktioner.

Vi kommer nedan gå igenom en enkel transformationskontroll-algoritm, som hämtar inspiration från dOPT-algoritmen [3] och COT-algoritmen [12], men där vi tagit bort mycket komplexitet för att lättare kunna få en förståelse för själva grundprincipen. I kommande avsnitt kommer vi gå igenom transfor-mationsfunktioner från dOPT (teckennivå) och GOT-algoritmerna (strängnivå). Sist men inte minst kommer vi introducera transformationskontroll-algoritmen COT vilken hanterar flera efterföljande ope-rationer bättre än den från dOPT-algoritmen.

Grunden för operationstransformer fungerar som följande: Anta att vi har x antal klienter som kom-municerar med varandra över nätverk, för enkelhets skull låter vi x vara två men principen för fler är densamma. När en användare skriver något i sin textredigerare skapas en operation som representerar användarens redigering, denna operation skickas sedan till den andra klienten som exekverar operationen så att även denna klient ska ha samma text som användaren som skapade operationen, dvs de har då samma dokumenttillstånd. Detta fungerar bra så länge ingen av klienterna skapar operationer samtidigt, då detta innebär att klienterna inte vet vilken ordning de ska utföras. Ex. om användare A skriver X (Op1) och användare B skriver Y (Op2), båda i ett tomt dokument, så skulle resultatet kunna bli XY

eller YX beroende på vilket ordning operationerna utförs på de båda klienterna.

För att råda bot på problemet med ordningen inför vi nu operationstransformen. Det betyder att när en klient får operationer från en annan klient så måste dessa transformeras innan de utförs. Transformatio-nen ska se till att oavsett om klient 1:s operationer eller klient 2:s operationer körs först så ska resultatet bli detsamma, dvs:

{Op1, Op2‘} ≡ {Op2, Op1‘}

Vi noterar här en ordnad mängd med operationer som {Op1, Op2}, där operation Op1sekventiellt kommer

före Op2. Vi noterar att exekvering av två olika ordnade mängder av operationer ger samma resultat

genom notationen {Op1, Op2} ≡ {Op3, Op4}, dvs exekvering av Op1 och därefter Op2ger samma effekt

som Op3 följt av Op4. 13http://www.codingmonkeys.de/subethaedit/ 14http://www.codoxware.com/codoxword 15http://sourceforge.net/projects/ace 16http://gobby.0x539.de/trac/ 17http://etherpad.org

(17)

Operationerna är följande:

Op1= INS[0, X]

Op2= INS[0, Y ]

Op1‘ = IT (Op1, Op2) = IT (INS[0, X], INS[0, Y ] = INS[0, X]

Op2‘ = IT (Op2, Op1) = IT (INS[0, Y ], INS[0, X]) = INS[1, Y ]

Om vi ersätter Op-operationerba med deras värden, får vi följande två ordnade mängder av operationer vilka vid exekvering från vänster till höger ger ekvivalent resultat:

{INS[0, X], INS[1, Y ]} ≡ {INS[0, Y ], INS[0, X]}

IT är ett vanligt funktionsnamn i litteraturen för (framåt-)transform. Till vänster ser vi klient 1 som först utför Op1 och därefter Op2‘ som är Op2 transformerad mot Op1 och tvärtom för klient 2 som är

representerad till höger i ekvivalensen. Resultatet kommer därmed bli XY i båda klienter. Vi har än inte definierat hur IT-funktionen ser ut men vi har sett principen, IT kan definieras olika beroende på tillämpning etc., t.ex. skulle man kunna definiera den så att resultatet blir YX istället för XY, huvudsaken är att IT med en transformationskontroll-algoritm ser till att resultat är samma på alla klienter. Anledningen till att det inte fungerade för den ena klienten att köra den andra klientens operation direkt beror på att den andra klientens operation inte kommer köras i samma dokumenttillstånd som när den skapades av den andra klienten. Låt oss representera Dokumenttillståndet med den mängd operationer som utförts på dokumentet ({Op1, Op2, ...}) samt för visualisering den sträng som dokumentet innehåller

när operationerna utförts (”Strängen”). Vi börjar då med det tomma dokumentets tillstånd, vilket vi noterar som ({} / ””), vilket de båda klienterna utgår från.

Klient 1 utför sin operation lokalt och ändrar då sitt dokumenttillstånd från ({} / ””) till ({Op1} / ”X”),

dvs Op1 utfördes på Dokumenttillståndet ({} / ””) vilket gjorde att Dokumenttillståndet ändrades till

({Op1} / ”X”).

Klient 2 utför sin operation lokalt och ändrar då sitt dokumenttillstånd från ({} / ””) till ({Op2} / ”Y”),

dvs Op2 utfördes på Dokumenttillståndet ({} / ””), dvs samma procedur som i klient 1.

Både Op2och Op1är skapta för att köras på det tomma dokumentets tillstånd ({} / ””). Därför kan inte

Op1 köras direkt i Dokumenttillståndet ({Op2} / ”Y”) och vice versa. För att Op1 ska kunna köras på

klient två, med Dokumenttillståndet ({Op2} / ”Y”), måste Op1 därför transformeras för att exekveras

i ({Op2} / ”Y”) istället för ({} / ””). En illustration av exemplet finns i figur 2.7. Den läsare som är

intresserad av att fördjupa sig operationstransformer hänvisas till källorna i kommande undersektioner. En enkel transformationskontroll-algoritms flöde för en klient kan beskrivas i följande punkter:

1. Låt Q vara en tom kö.

2. När operationer skapas, utför dem lokalt, samt lägg dem på kön Q. 3. Skicka/ta emot operationerna till/från andra klienter.

4. Transformera inkommande operationer mot operationerna i kön Q. 5. Exekvera de transformerade operationerna.

6. Töm kön Q.

(18)

Figur 2.7: Operationstransform med en operation från vardera klient.

2.3.1 Distribuerad Operationstransform (dOPT)

En av de första operationstransformerna som skapades var den distribuerade operationstransformen19

(dOPT) av Ellis och Gibbs [3]. Denna är en av de simplaste och är därför bra för att skapa förståelse för operationstransformer, däremot finns det brister i den som vi kommer förklara i slutet av denna undersektion.

Anta att vi har de två typerna av teckenoperationer Op=INS[P,X] och Op=DEL[P,X] som nämndes i avsnitt 2.1.1. Anta också att vi har metoderna Op.position för att plocka ut en operations position, Op.string för att plocka ut operationens sträng samt Op.length för att plocka ut längden av operationens sträng. Metoderna Op.string och Op.length kommer nu till en början bara vara ett tecken respektive all-tid 1, men vi kommer ha nytta av dem när vi går över till att hantera operationer med strängar senare. Anta också att en operation har följande egenskap Op.userid som plockar fram den unika användariden-tifikationen, vilket är ett heltal, för den användare/klient som skapat operationen. I litteraturen används ofta funktionerna exempelvis P(Op), S(Op) och L(Op) istället för egenskaperna Op.position, Op.string och Op.length, tanken att använda dessa egenskaper istället för de korta funktionsnamnen är för att de ska ge en mer lättläst och modern tolkning av algoritmerna från litteraturen.

I figur 2.8 ser vi dOPT:ens algoritmer för att transformera operationer. Den noggranne läsaren kan se att dOPT-algoritmen i litteraturen inte ordagrant ser ut som den i figuren, utan att algoritmen är justerad för att matcha resten av stilen i denna rapport, principen är dock densamma. Algoritmen för transformen är uppdelad i de olika kombinationer av operationer som kan förekomma:

dOPT_II en INS-operation (OpA) transformeras mot en annan INS-operation (OpB). dOPT_DD en DEL-operation (OpA) transformeras mot en annan DEL-operation (OpB). dOPT_ID en INS-operation (OpA) transformeras mot en DEL-operation (OpB).

dOPT_DI en DEL-operation (OpA) transformeras mot en INS-operation (OpB).

Om vi återgår till exemplet med två klienter där den ena skriver X och den andra Y, så kan vi följa funktionen dOPT_II och se vad som händer. Vi inspekterar transformationen som sker i klient 1:

(19)

• Klient 1 har utfört INS[0,’X’] och får därefter INS[0,’Y’] från Klient 2.

• INS[0,’Y’] ska då transformeras mot INS[0,’X’], dvs dOPT_II(OpA=INS[0,’Y’], OpB=INS[0,’X’]) anropas.

• Då båda operationerna har samma position och inte skriver likadana tecken kommer transforma-tionen bero på användaridentiteten. Anta att klient 1 har användaridentiteten 1 och klient 2 har användaridentiteten 2, dvs OpA.userid < OpB.userid vilket medför att den transformerade ope-rationen blir INS[0,’Y’]. Kontrollen OpA.userid < OpB.userid då vi har samma position medför att vi alltid har en totalordning mellan klienter och operationer. Totalordning krävs för att vi deterministiskt ska kunna transformera operationerna.

• Resultaten blir därmed YX i klient 1.

Vi överlåter till läsaren att verifiera att resultatet även blir YX i klient 2, och att den transformerade operationen blir INS[1,’X’] i det fallet.

dOPT:ens transformationsfunktioner tillsammans med dess transformationskontroll-algoritm har några brister. Transformationskontroll-algoritmen har vi inte introducerat här utan lämnar till läsaren att fördjupa sig i litteraturen [3]. Den hanterar bara operationer på tecken-nivå. Om man exempelvis klistrar in text i ett dokument så kommer textsträngen styckas upp på i tecken innan operationerna skickas. Detta kommer förmodligen kräva mer bandbredd vid kommunikationen. En lösning på detta, genom att införa operationer för strängar istället för bara tecken, hittar vi i avsnitt 2.3.2. Om en användare gör flera operationer i sekvens utan en synkronisering mellan klienter kommer dOPT:s transformationskontroll-algoritm inte att hantera detta på ett korrekt sätt, vilket medför att fel kommer uppstå i form av divergens mellan klienter, detta benämns som dOPT-puzzle [12] [11] [8]. I [8] kan den intresserade läsaren på sida 290 se ett konkret exempel på när dOPT:s transformationskontroll-algoritm fallerar.

2.3.2 OT för strängar

Sun m.fl. [11] har konstruerat ett OT-system (transformationskontroll-algoritmer samt transformations-funktioner) med namn GOT (Generic Operational Transform) som hanterar operationer med strängar, till skillnad från dOPT som bara hanterar operationer på teckennivå. I figur 2.9 och 2.10 ser vi hälften av transformationsfunktionerna för GOT algoritmen. Som vi ser blir dessa funktioner lite mer komplexa än de från dOPT-algoritmen.

I GOT-algoritmen definieras DEL-operationen som DEL[position, längd] istället för DEL[position, sträng-/tecken] som tidigare. Vi kommer i designkapitlet återgå till att använda DEL[position, strängsträng-/tecken] tillsammans med denna algoritm, men för stunden låter vi DEL-operatorns andra parameter vara rade-ringens längd.

Detta är som nämnt bara hälften av transformationsfunktionerna som GOT använder. GOT använder sig av en transformationskontroll-algoritm som använder sig av både framåt- och bakåttransformationer. Framåttransformationer (IT) är vad dOPT använder sig av, bakåttransformationer (ET) är som framåt transformationer fast med invers effekt, vilket ger följande krav på ET/IT-funktionerna:

• OpA = ET (IT (OpA, OpB), OpB) om OpA har samma kontext/dokumenttillstånd som OpB. • OpA = IT (ET (OpA, OpB), OpB) om OpB har utförts direkt före OpA.

Vi kommer inte fördjupa oss med i bakåttransformationer då vi inte kommer använda oss av det, däremot kommer vi använda oss av GOT:s IT-funktioner med vissa modifikationer. Bl.a. kommer vi plocka bort Save_LI-funktionsanropet då den bara är nödvändig vid användning tillsammans med ET-transformer, mer om det kommer i designkapitlet.

(20)

dOPT_II(OpA=INS , OpB=INS )

i f (OpA. p o s i t i o n < OpB. p o s i t i o n )

return INS [OpA. p o s i t i o n , OpA. s t r i n g ] else i f (OpA. p o s i t i o n > OpB. p o s i t i o n )

return INS [OpA. p o s i t i o n +1, OpA. s t r i n g ] else

i f (OpA. s t r i n g == OpB. s t r i n g )

return 0 # empty operation , do nothing else

i f (OpA. u s e r i d < OpB. u s e r i d )

return INS [OpA. p o s i t i o n , OpA. s t r i n g ] else

return INS [OpA. p o s i t i o n +1, OpA. s t r i n g ] dOPT_DD(OpA=DEL, OpB=DEL)

i f (OpA. p o s i t i o n < OpB. p o s i t i o n )

return DEL[OpA. p o s i t i o n , OpA. s t r i n g ] else i f (OpA. p o s i t i o n > OpB. p o s i t i o n )

return DEL[OpA. p o s i t i o n −1, OpA. s t r i n g ] else

return 0 # empty operation , do nothing dOPT_ID(OpA=INS , OpB=DEL)

i f (OpA. p o s i t i o n < OpB. p o s i t i o n )

return INS [OpA. p o s i t i o n , OpA. s t r i n g ] else

return INS [OpA. p o s i t i o n −1, OpA. s t r i n g ] dOPT_DI(OpA=DEL, OpB=INS )

i f (OpA. p o s i t i o n < OpB. p o s i t i o n )

return DEL[OpA. p o s i t i o n , OpA. s t r i n g ] else

return DEL[OpA. p o s i t i o n +1, OpA. s t r i n g ]

(21)

IT_II (OpA=INS , OpB=INS )

i f (OpA. p o s i t i o n < OpB. p o s i t i o n )

return INS [OpA. p o s i t i o n , OpA. s t r i n g ] else

return INS [OpA. p o s i t i o n+OpB. length , OpA. s t r i n g ] IT_DD(OpA=DEL, OpB=DEL)

OpA‘ = 0

i f (OpA. p o s i t i o n + OpA. l e n g t h <= OpB. p o s i t i o n ) OpA‘ = DEL[OpA. p o s i t i o n , OpA. l e n g t h ]

else i f (OpA. p o s i t i o n >= OpB. p o s i t i o n + OpB. l e n g t h ) OpA‘ = DEL[OpA. p o s i t i o n −OpB. length , OpA. leng th ] else

i f OpB. p o s i t i o n <= OpA. p o s i t i o n

i f OpA. p o s i t i o n + OpA. l e n g t h <= OpB. p o s i t i o n + OpB. l e n g t h # OpA t o t a l l y i n s i d e OpBs range

OpA‘ = DEL[OpA. p o s i t i o n , 0 ]

else #(OpA. p o s i t i o n + OpA. l e n g t h > OpB. p o s i t i o n + OpB. l e n g t h ) # OpA i s p a r t l y i n s i d e OpB ’ s range , t h e r e s t i s to r i g h t o f B OpA‘ = DEL[OpB. p o s i t i o n , OpA. p o s i t i o n

+ OpA. l e n g t h − (OpB. p o s i t i o n − OpB. le ngth ) ] else # (OpB. p o s i t i o n > OpA. p o s i t i o n )

i f OpB. p o s i t i o n + OpB. l e n g t h >= OpA. p o s i t i o n + OpA. l e n g t h

# OpA i s p a r t l y i n s i d e OpB ’ s range , t h e r e s t i s to t h e l e f t o f B OpA‘ = DEL[OpA. p o s i t i o n , OpB. p o s i t i o n −OpA. p o s i t i o n ]

else

# OpB i s t o t a l l y i n s i d e OpA ’ s range

OpA‘ = DEL[OpA. p o s i t i o n , OpA. l e n g t h − OpB. le ngth ] Save_LI (OpA‘ , OpA, OpB)

return OpA‘

Figur 2.9: GOT:s operationstransform för strängar, del 1 (pseudokod), adapterad från [11]

Kort ska vi förklara skillnaden mellan en bakåt- och en framåt-transform. En bakåt-transform anpassar en operation för att den ska kunna exekveras innan den operation den från början exekverades efter se-kventiellt. En framåt-transform anpassar en operation för att den ska kunna exekveras efter en operation som skapats parallellt.

I figur 2.9 ser vi att funktionerna IT_II och IT_DD. IT_II tar hand om transformationen mellan två INS-operationer. Den förskjuter fram den operation som transformeras (OpA) med längden av den operation som transformationen sker mot (OpB) mot om OpA ligger till höger om OpB i strängen. IT_DD tar hand om transformationer mellan två DEL-operationer. OpA ska transformeras mot OpB. OpA:s position och längd kommer justeras olika beroende på om OpA:s raderings-område är helt till vänster eller höger om OpB:s område, OpA:s område är delvis inom OpB:s område med en del utanför till vänster eller höger, OpA:s område helt omsluter OpB:s raderings-område eller tvärtom. Totalt finns det sex olika fall, vilka parvis är motsatser till varandra, IT_DD ser till att dessa motsatser transformeras rätt mot varandra så att resultatet blir detsamma för {Op1, Op�2}

och {Op2, Op�1} där Op1 och Op2 är DEL-operationer och Op�1 = IT _DD(Op1, Op2) och Op�2 =

IT_DD(Op2, Op1).

I figur 2.10 ser vi funktionerna IT_ID och IT_DI. IT_ID transformerar INS-operationen OpA mot DEL-operationen OpB och förskjuter OpA till vänster med OpB:s längd om OpB ligger till vänster om OpA. Om OpA är inom OpB:s raderings-område förflyttas OpA till OpB:s position.

IT_DI transformerar DEL-operationen OpA mot INS-operationen OpB och förskjuter OpB till höger när OpA ligger höger om OpB. Om OpB:s insättnings-område omfamnar OpA:s position så kommer OpA vara tvungen att delas i två DEL-operationer för att inte radera något av den insättning som OpB gör, då returneras en lista av två del-operationer istället för en som det normalt är.

(22)

IT_ID(OpA=INS , OpB=DEL) OpA‘ = 0

i f (OpA. p o s i t i o n <= OpB. p o s i t i o n )

OpA‘ = INS [OpA. p o s i t i o n , OpA. s t r i n g ]

else i f (OpA. p o s i t i o n > OpB. p o s i t i o n + OpB. l e n g t h ) OpA‘ = INS [OpA. p o s i t i o n −OpB. length , OpA. s t r i n g ] else

OpA‘ = INS [OpB. p o s i t i o n , OpA. s t r i n g ] Save_LI (OpA‘ , OpA, OpB)

return OpA‘

IT_DI(OpA=DEL, OpB=INS )

i f OpB. p o s i t i o n >= OpA. p o s i t i o n + OpA. l e n g t h return DEL[OpA. p o s i t i o n , OpA. l e n g t h ] else i f OpA. p o s i t i o n >= OpB. p o s i t i o n

return DEL[OpA. p o s i t i o n + OpB. length , OpA. l e n g t h ] else

# D e l e t e needs to be s p l i t t e d i n t o two s p l i t o p e r a t i o n s s i n c e # t h e r e i s an i n s e r t i n s i d e t h e d e l e t e range .

return [

DEL[OpA. p o s i t i o n , OpB. p o s i t i o n − OpA. p o s i t i o n ] , DEL[OpB. p o s i t i o n + OpB. length ,

OpA. l e n g t h − (OpB. p o s i t i o n − OpA. p o s i t i o n ) ) ] ]

Figur 2.10: GOT:s operationstransform för strängar, del 2 (pseudokod), adapterad från [11]

2.3.3 Kontext-baserad operationstransform (COT)

Sun och Sun [12] har skapat en transformationskontroll-algoritm som klarar transformera mer än två sekventiella operationer åt gången, vilket inte dOPT algoritmen klarar av. Vi ser nedan algoritmen i korthet. Alla formella krav/förklaringar till varför algoritmen fungerar etc. har utelämnats, dessa återfinns i artikeln för den nyfikne läsaren.

Låt oss först deklarera viktiga begrepp och funktioner som används i COT-algoritmen:

Dokumenttillståndet Dokumenttillståndet i algoritmen är det dokumenttillstånd klient X är i för tillfället. Dokumenttillståndet representeras som en mängd av operationer som utförts, ex. {Op1, Op2}

Original(Operation) Tar fram en icke transformerad version av Operation, detta för att jämförelse mellan operationer ska vara smidig.

Kontext(Operation) Dokumenttillståndet/Kontexten där operationen skapades. När operation A trans-formeras mot operation B läggs operation B:s original till i operation A:s kontext. Kontexten re-presenteras som en mängd av operationer som utförts.

”-”-operatorn Beräknar differensen mellan två mängder, ex {Op1, Op2} − {Op2} = {Op1}

”∪”-operatorn Beräknar unionen av två mängder, ex. {Op1, Op2} ∪ {Op3} = {Op1, Op2, Op3}

”⊆”-operatorn Kontrollerar om mängd är en delmängd av en annan mängd. COT-algoritmen

Med hjälp av ovanstående begrepp kan vi nu formalisera COT-algoritmen som följande punkter: COT-DO(Operation, Dokumenttillståndet):

(23)

1) transformera(Operation, Dokumenttillståndet - Kontext(Operation)) 2) Exekvera Operation

3) Dokumenttillståndet = Dokumenttillståndet ∪ {Original(Operation)}

Transformera-funktionen som används av COT-DO definieras med följande punkter: transformera(Operation, Kontextdifferens)

• Upprepa tills Kontextdifferens == {}:

1) Plocka ut Oxur Kontextdifferens, där Kontext(Ox) ⊆ Kontext(Operation)

2) transformera(Ox, Kontext(Operation) − Kontext(Ox))

3) Operation = IT(Operation, Ox)

4) Kontext(Operation) = Kontext(Operation) ∪ {Original(Ox)}

Exempel med COT

Här kommer ett exempel på hur COT fungerar, för enkelhets skull kombinerar vi den med dOPT-algoritmens transformfunktioner. Anta att vi har två klienter, klient A och klient B. Vi utgår från att vi redan har texten ”ABC” i dokumentet.

Klient A utför operationerna {Op1 = INS[1,�1�], Op2 = DEL[3,�C�]} i sekvens. Vilket medför att

dokumentet först har texten ”A1BC” och därefter ”A1B”. Kontext för Op1 är {} och för Op2 {Op1}.

Dokumenttillståndet i Klient A är de båda operationerna, dvs {Op1, Op2}

Klient B utför operationerna {Op3 = INS[0,�X�], Op4 = INS[4,�Y�]} i sekvens. Vilket medför att

dokumentet först innehåller texten ”XABC” och därefter ”XABCY”. Kontext för Op3 är {} och för Op4

{Op3}. Dokumenttillståndet i Klient B är {Op3, Op4}

Den slutgiltiga texten borde rent intuitivt bli ”XA1BY”. Låt oss undersöka hur COT tar hand om detta i klient A. Vi överlämnar till läsaren att verifiera att vi får samma slutresultat för Klient B.

Klient A får {Op3, Op4} från Klient B. Vi börjar först transformera Op3, genom att anropa COT-DO(Op3,

{Op1, Op2})

– Transformation av Op3 –

1. Transformera(Op3, {Op1, Op2} - {} = {Op1, Op2}) anropas.

(a) Vi plockar ut Op1 ur Kontextdifferens ({Op1, Op2}) då Kontext(Op1)= {} ⊆ Kontext(Op3)=

{}

(b) Transformera(Op1, {} − {} = {}) anropas, men avslutas direkt då Kontextdifferens är en tom

mängd.

(c) Op3IT-transformeras mot Op1⇒ Op3= INS[0,�X�]

(d) Op1läggs till i Op3:s Kontext. Kontext(Op3) = {Op1}

(e) – Loopa –

(f) Vi plockar ut Op2 ur Kontextdifferens ({Op2}) då Kontext(Op2)= {Op1} ⊆ Kontext(Op3)=

{Op1}

(g) Transformera(Op2, {Op1} − {Op1} = {}) anropas, men avslutas direkt då Kontextdifferens är

(24)

(h) Op3IT-transformeras mot Op2⇒ Op3= INS[0,�X�]

(i) Op2läggs till i Op3:s Kontext. Kontext(Op3) = {Op1, Op2}

2. Op3exekveras i Klient A och läggs till i Dokumenttillståndet som nu är {Op1= INS[1,�1�], Op2=

DEL[3,�C�], Op3= INS[0,�X�]}. Dokumenttexten blir steg för steg ”ABC”, ”A1BC”, ”A1B”, ”XA1B”

– Transformation av Op4 –

1. Transformera(Op4, {Op1, Op2, Op3} - {Op3} = {Op1, Op2}) anropas.

(a) Vi plockar ut Op1 ur Kontextdifferens ({Op1, Op2}) då Kontext(Op1)= {} ⊆ Kontext(Op4)=

{Op1}

(b) Transformera(Op1, {Op3} − {} = {Op3}) anropas.

i. Vi plockar ut Op3ur Kontextdifferens ({Op3}) då Kontext(Op3)= {} ⊆ Kontext(Op1)=

{}. Observera att Op3 här är inte är densamma som den transformerade Op3 utan det är

originalet.

ii. Transformera(Op3, {} − {} = {}) anropas, men avslutas direkt då Kontextdifferens är en

tom mängd.

iii. Op1IT-transformeras mot Op3⇒ Op1= INS[2,�X�]

iv. Op3läggs till i Op1:s Kontext. Kontext(Op1) = {Op3}

(c) Op4IT-transformeras mot den transformerade Op1⇒ Op4= INS[5,�Y�]

(d) Op1läggs till i Op4:s Kontext. Kontext(Op4) = {Op3, Op1}

(e) – Loopa –

(f) Vi plockar ut Op2 ur Kontextdifferens ({Op2}) då Kontext(Op2)= {Op1} ⊆ Kontext(Op4)=

{Op3, Op1}

(g) Transformera(Op2, {Op3, Op1} − {Op1} = {Op3}) anropas.

i. Vi plockar ut Op3ur Kontextdifferens ({Op3}) då Kontext(Op3)= {} ⊆ Kontext(Op2)=

{Op1}

ii. Transformera(Op3, {Op1} − {} = {Op1}) anropas

A. Vi plockar ut Op1ur Kontextdifferens ({Op1}) då Kontext(Op1)= {} ⊆ Kontext(Op3)=

{}

B. Op3 IT-transformeras mot Op1⇒ Op3= INS[0,�X�]

C. Op1 läggs till i Op3:s Kontext. Kontext(Op3) = {Op1}

iii. Op2IT-transformeras mot Op3⇒ Op2= DEL[4,�C�]

iv. Op3läggs till i Op2:s Kontext. (Op2) = {Op1, Op3}

(h) Op4IT-transformeras mot den transformerade Op2⇒ Op4= INS[4,�Y�]

(i) Op2läggs till i Op4:s Kontext. Kontext(Op4) = {Op3, Op1, Op2}

2. Op4exekveras i Klient A och läggs till i Dokumenttillståndet som nu är {Op1= INS[1,�1�], Op2=

DEL[3,�C], Op

3 = INS[0,�X�], Op4 = INS[4,�Y�]}. Dokumenttexten blir steg för steg ”ABC”,

”A1BC”, ”A1B”, ”XA1B”, ”XA1BY”.

(25)

2.4 JSON

JSON står för Javascript-objektnotation20 och är ett lättviktat datautbytesformat (jämfört med t.ex.

XML). Det är lätt för både människor och maskiner att läsa/parsa och skriva/generera. JSON baseras på ett subset av programmeringsspråket Javascript, men JSON som format är totalt oberoende av programmeringsspråk och har stöd i de flesta moderna programmeringsspråk. JSON byggs upp av två datastrukturer:

• En kollektion av namn/värde-par, vilket kan motsvara ex. ett objekt, en hashtabell, ett lexikon i vanliga programmeringsspråk.

• En ordnad lista med värden, vilket motsvarar t.ex. en array eller lista i vanliga programmerings-språk.

Ett värde kan vara en sträng, ett tal, ett objekt, en array, ett booleskt värde (”true”/”false”) eller ”null”. JSON kan uttryckas med hjälp av syntaxdiagramen i figurerna 2.11, 2.12, 2.13, 2.14, 2.15. [1][2].

Ett exempel på hur ett JSON-objekt kan se ut: {”a”:”sträng”, ”b”:[1, 2, 3.14, false], ”foo”:{”bar”:true,”c”:null}}

Figur 2.11: Syntaxdiagram för JSON-objekt. Eftertryckt med tillåtelse från json.org [1]

Figur 2.12: Syntaxdiagram för JSON-array. Eftertryckt med tillåtelse från json.org [1]

(26)

Figur 2.13: Syntaxdiagram för JSON-värde. Eftertryckt med tillåtelse från json.org [1]

(27)
(28)

Kapitel 3

Design

I detta kapitel berättar vi hur samarbetsramverket är designat. Vi går inte igenom alla detaljer utan försöker ge en överblickande men tillräckligt noggrann bild för att designen ska gå att använda för att skapa nya klienter.

3.1 Gränssnitt

I detta avsnitt kommer vi gå igenom de gränssnitt som binder samman en textredigerare med samarbets-verktyget, vilka komponenter som krävs för att textredigeringsklienten ska kunna redigera samma text tillsammans med andra klienter. Vi kommer också, utan större djup, gå igenom hur servern är uppbyggd. Servern är navet i all kommunikation mellan klienterna, figur 3.1. Teoretiskt sett kan servern ha n antal klienter, där n går mot oändligheten, men i praktiken begränsas detta av kombinationen av hur ofta klienterna anropar samt hur länge ett anrop tar.

Figur 3.1: Sammankoppling mellan klienter och server

3.1.1 Definitioner

(29)

Operation

En Operation har följande egenskaper som går att förändra:

Operation.kind Talar om vilken typ av operation. INS för insättningsoperation och DEL för raderings-operation.

Operation.string Talar om vilken sträng som ska sättas in / tas bort

Operation.position Talar om var operationen ska sätta in / ta bort strängen. Utifrån detta kan vi härleda fram följande egenskaper:

Operation.insert? Är sant om operationen är av typen INS Operation.delete? Är sant om operationen är av typen DEL

Operation.length Tar fram strängens längd. Se undersektionen nedan om tecken för detaljer om hur strängens längd ska beräknas.

OperationArray

En OperationArray är en ordnad mängd av operationer, där den kausala ordningen för operationerna är densamma som den ordning de är i. [Op1, Op2, Op3 ...]

Tecken

För att klienterna ska vara överens om längden för en sträng måste de vara överens om hur längden beräknas. Beräkningen skulle antingen göras per byte eller tecken. Beroende på vilken teckenkodning man använder så kan ett tecken representeras med olika antal bytes, t.ex. tecknet ”ä” representeras av en byte med teckenkodningen Latin-1 och två bytes med UTF-8. Detta skulle innebär en längd på 1 respektive 2 beroende på vilken kodning man väljer att köra, och att positionen efter tecknet därmed skulle vara 1 respektive 2.

Det andra alternativet som är det vi kommer använda oss av är att låta ett tecken alltid ha längden 1, oavsett hur många byte tecknet kräver när det teckenkodas. Alltså ett tecken oavsett vilket har alltid längden 1. När klienterna/servern skickar strängar till varandra ska de kodas som Unicode, då det är det JSON-formatet har stöd för.

3.1.2 Klientens gränssnitt

I figur 3.2 ser vi en överblick över de komponenter som krävs i en klient för den ska kunna ansluta till servern och för att sammankoppla med textredigeraren. Vi kommer nu gå igenom dessa komponenter och gränssnitten mellan dem.

Pilarna mellan komponenterna i figuren (figur 3.2) representerar vilken koppling komponenterna har till varandra. En enkelriktad pil innebär att den ena komponenten använder sig av den andra komponenten, men att den andra komponenten inte känner till den första komponenten. En dubbelriktad pil innnebär att båda komponenterna känner till varandra men åt det ena hållet oftast enbart i form av callback-funktioner. Den komponent pilen kommer från känner till den komponent pilen pekar på.

(30)
(31)

Vi börjar med nätverksdelen, som vi ser längst ner i figuren. Längst ner har vi en asynkron HTTP(S)-klient, som sköter kommunikationen med webbservern (mer om kraven på den finner vi i undersek-tion 3.2). Anledningen till att klienten ska vara asynkron, är för att klienten inte ska låsa sig medan en server-förfrågan skickas. Skulle man använda en synkron HTTP-klient skulle hela tråden vänta tills servern svarat på klientens förfrågan och textredigerarens responstid skulle upplevas som långsam. Man skulle kunna använda en synkron HTTP-klient om man enbart vill ha en klient som observerar vad de andra klienterna skriver alternativt om man anser att responstiden inte är viktig. En konsekvens av att använda synkron HTTP-klient är att det då inte krävs några operationstransform-algoritmer i klienten utan alla transformationer kan ske i servern. Detta skulle kräva mindre arbetsinsats vid implementation av klienten, men ge en mindre användbar klient. Både klienter med synkrona och asynkrona HTTP-klienter kan ansluta till servern samtidigt.

Ovanpå HTTP-lagret har vi ett lager som omvandlar mellan JSON och klientens interna representation för objekt/lexikon och tvärtom. En förfrågan skickas i form av ett objekt och ett objekt fås sedan tillbaka i svaret.

Överst i nätverkshanteringen har vi anslutningshanteraren som abstraherar bort nätverket och låter dokumenthanterare direkt skicka kommandon till servern.

En dokumenthanterare (DocumentManager), som kommunicerar via anslutningshanteraren, har ansva-ret för ett dokument. Den ska se till att skicka operationer som görs i textbuffern och ta emot och transformera operationer som kommer från servern och skicka dem till textbuffern via textbufferadap-tern (TextBufferAdapter). För varje dokument som samredigeras så har detta dokument en textbuffer i textredigeraren, denna textbuffern kopplas ihop med en dokumenthanterare via en textbufferadapter. För n textbuffrar krävs n textbufferadaptrar och n dokumenthanterare.

Som vi ser har vi komponenterna (klasserna) Operation och OperationArray, som används av både textbufferadapter och dokumenthanteraren. Operation representerar en operation och OperationArray en ordnad mängd av operationer. Båda dessa klasser använder sig av operationstransformerna i sektion 3.3.2.

Anslutningshanterare

Anslutningshanteraren (ConnectionManager) som ligger överst i nätverkshanteringen tillhandahåller me-toder för att utföra kommandon (se sektion 3.2 för detaljer om kommandona):

Metod Förklaring

execute(command, arguments, callback) Exekverar kommandot command med argumenten ar-guments mot servern, där command är en sträng och arguments är ett objekt/lexikon. När servern sedan sva-rar anropas callback där svars-objektet skickas med. Alla nedanstående metoder i denna tabell använder execute-metoder, och callback-argumentet fungerar på samma sätt i dessa.

login(callback) Loggar in på servern. logout(callback) Loggar ut från servern.

operate(document_id, operations, callback) Skickar operationerna operations till servern. docu-ment_id används för att tala om vilket dokument ope-rationerna gäller.

Anslutningshanterarens uppgift är att hålla koll på URL:en till servern, samt vilket användarid (user_id) som klienten har. URL:en krävs för att veta vilken adress som anslutningen sker mot och användarid:t krävs vid transformering.

(32)

Textbufferadapter

Textbufferadapterns uppgift är vara en brygga mellan en dokumenthanterare och en textbuffers gräns-snitt. Den ska ha följande metoder:

Metod Förklaring

applyOperation(operation) Utför operationen operation på textbuffern

setOnTextChange(callback) Ställer in så callback är den funktion som anropas när en textförändring skett.

catchTextChange() Ska anropas när förändringar skett i textbuffern. Metoden skapar då den mängd med de operationer som utförts (i form av en Opera-tionArray), anropar den callback-funktion som valts med setOnText-Change(callback) och skickar med operationerna som argument. Denna metod kan delas upp i två metoder, en för INS-operationer och en för DEL-operationer om det är mer lämpligt att göra det, om det är lämp-ligt beror på hur en textbuffer i textredigeraren fungerar.

Dokumenthanterare

Dokumenthanterarens uppgift är att se till att det kommuniceras operationer mellan textbufferadaptern och servern och transformera dem vid korrekt tillfälle. Den har följande metoder:

Metod Förklaring

onNewLocalOperations(operations) Denna metod tar emot operationer som skapats av textbuffe-radaptern och skickar dem sedan till servern. Den binds till textbufferadaptern genom textbufferadapterns metod setOn-TextChange(callback). onNewLocalOperations anropar sendO-perations för att skicka operationerna till servern.

sendOperations(operations) Skickar operationer till servern så länge inte en server-förfrågan för tillfället är pågående, då läggs operationerna i en kö istäl-let. Denna kö ska transformeras mot operationer inkommande från servern. Anslutningshanterarens operate-metod används för att skicka operationerna till servern, och getNewServerO-perations sätts som callback.

getNewServerOperations(operations) Denna metod ska anropas med ett intervall på ca 1-2 sekunder för att hämta nya operationer från servern när inte klienten har några lokala operationer att skicka till servern. Detta krävs för att klienten kontinuerligt ska kunna få in nya operationer från de andra klienterna även fast användaren vid klienten är passiv.

handleServerOperations(operations) handleServerOperations hanterar svar från servern efter att sendOperations anropats och svar kommit från servern.

Dokumenthanteraren har även som uppgift att hålla kolla på document_id för det dokument det hanterar, vilket identifierar dokumentet mot servern.

3.1.3 Serverns gränssnitt

I figur 3.3 ser vi en överblick över komponenterna i servern. Servern som är navet i kommunikationen för ramverket har till uppgift att samla in alla operationer från alla klienter och skicka vidare rätt operationer till varje klient. Servern har koll på vilka operationer varje klient har fått och kan därmed se till att en klient endast får operationer som är nya för den klienten. Operationerna lagras i kausal ordning i en databas.

(33)

När nya operationer inkommer transformeras de mot de operationer i databasen som inte den klient som skickar redan har, därefter läggs de till i databasen. Även det motsatta sker, de operationer som hämtats ur databasen transformeras mot de (icke transformerade) inkomna operationerna och skickas tillbaka till klienten. Detta sker i Operationshanteraren. Vi går igenom de övergripande transformeringsalgoritmerna mer i detalj i avsnitt 3.3.1.

Anslutningshanteraren hanterar de inkommande kommandona; inloggning och utloggning av klienter samt tar hand om att omvandla de operationer som kommer tillsammans med operate-kommandot till ett internt format och förmedlar detta vidare till operationshanteraren.

JSON-(av)kodaren ser till att konvertera mellan JSON och intern representation och vice versa. Underst i nätverkslagret har vi en HTTP(S)-server som hanterar HTTP-anropen och hanterar kommu-nikationen synkront, vilket i detta fall innebär att servern enbart hanterar en förfrågan åt gången. Om mer än en klient åt gången skulle hanteras samtidigt kan operationer försvinna eller förlora ordningen mellan varandra. Denna spärr kan alternativt läggas mot databasen så att bara en kan läsa databasen åt gången.

(34)
(35)

3.2 Nätverk/Protokoll

Som protokoll mellan klient och server används JSON över HTTP(S), se rubrik 2.4. Då det underliggande applikationsprotokollet är HTTP så innebär det att det alltid är klienten som skickar en förfrågan till server och servern svarar på förfrågan. Servern kan aldrig direkt skicka en förfrågan till en klient, det är alltid klienten som skapar konversationen.

När en klient har lokala operationer skickas de till servern. Om det finns operationer från andra klienter på servern som inte den lokala klienten har fått tidigare vid detta tillfälle så skickas de till klienten i svaret. Om klienten inte har några nya operationer att skicka till servern efter ca. 1-2s så bör en förfrågan utan operationer skickas för att på så sätt få uppdatering från andra klienter kontinuerligt.

Klienten skickar förfrågningarna via HTTP POST med ett POST-fält med namn data där värdet för data är en JSON-sträng till en HTTP-adress på servern (ex. http://localhost/api). Servern svarar med en JSON-sträng. HTTP cookies används för att hålla igång en session mellan klient och server.

Sammanfattningsvis krävs det av klienten att den har en HTTP-klient med stöd för POST-metoden samt sessionskakor samt klarar av att konvertera mellan JSON och intern representation och tvärtom. JSON-protokollet byggs upp med följande som grund:

Klient-förfrågan Server-svar { "command" : "kommando" , "argumentKey1" : value_1 "argumentKey2" : value_2 . . . } { " r e s p o n s e " : { " s t a t u s " : "OK/ERROR" " message " : " statusmeddelande " } "answerKey1" : value_1 "answerKey2" : value_2 . . . }

Klienten skickar command där ”kommando” byts ut mot vilket kommando man anropar. ”argumentKey1” osv. är används för att skicka data som tillhör kommandot. Observera att strängen ”argumentKey1” då byts ut mot lämplig nyckel t.ex. ”operations”.

Servern svarar alltid med ett response-objekt som innehåller nycklarna status och message. Status kan ha två värden: OK eller ERROR (observera att det ska vara med stora bokstäver). Servern svarar med status ERROR om ett fel har uppstått vid förfrågan, t.ex. om klienten försöker utföra ett kommando som kräver att klienten är inloggad men inte är det. Om inget fel uppstått är status OK. Message innehåller ett eventuellt människovänligt meddelande som talar om vilket fel som uppstått eller bekräftar vilken åtgärd som just utförts. ”answerKey1” osv. innehåller svarsdata, där ”answerKey1” byts ut mot lämpligt nyckelnamn.

Protokollet är som vi ser på grund av sin uppbyggnad av objekt/lexikon utbyggbart på både djupet och bredden. Det är möjligt att lägga till extra data med hjälp av oanvända nycklar och på så sätt bygga ut protokollet. Detta går att bygga ut protokollet i JSON-objekt på alla nivåer utan att det slutar fungera i någon klient som inte är gjord för att hantera det nya datat, den klienten bryr sig inte om mer än de nycklar den söker.

Man skulle exempelvis kunna bygga ut protokollet med stöd för att skicka med data om var en annan klients markör befinner sig. Denna utbyggnad måste då stödjas av servern, men måste nödvändigtvis inte implementeras i alla klienter, utan bara i de klienter som har möjlighet till att ge eller använda denna information.

Protokollet har stöd för fyra kommandon, se tabell 3.1, vilka kan användas i JSON som de är beskrivna i tabell 3.2.

(36)

Kommando Kräver Server return- Funktion argumentnyckel erar svarsnyckel

login ingen user_id Logga in klienten på servern. logged_in? ingen user_id Kontollera om klienten är inloggad. logout ingen ingen Logga ut klienten.

operate operations, document_id operations Skicka/ta emot operationer. Tabell 3.1: Protokollets fyra kommandon

Klient-förfågan Server-svar { "command" : " l o g i n " } { " r e s p o n s e " : { " s t a t u s " : "OK" , " message " : " Login �OK" } , " user_id " : i n t e g e r } { "command" : " logged_in ?" } { " r e s p o n s e " : { " s t a t u s " : "OK" , " message " : "Logged� i n " } , " user_id " : i n t e g e r } { "command" : " l o g o u t " } { " r e s p o n s e " : { " s t a t u s " : "OK" ,

" message " : "Logged� out " } } { "command" : " o p e r a t e " , " o p e r a t i o n s " : o p e r a t i o n s , "document_id" : document_id } { " r e s p o n s e " : { " s t a t u s " : "OK" , " message " : "" } " o p e r a t i o n s " : o p e r a t i o n s } eller

Om document_id saknas eller har dåligt format:

{

" r e s p o n s e " : {

" s t a t u s " : "ERROR" ,

" message " : "Document� i d � m i s s i n g � or �bad" }

}

eller

Om formatet för operationerna är dåligt:

{

" r e s p o n s e " : {

" s t a t u s " : "ERROR" ,

" message " : "Bad� o p e r a t i o n s � format " }

}

(37)

För allt utom kommandot login kan man även få fel-svaren (de olika svaren är separerade med /)

{

" r e s p o n s e " : {

" s t a t u s " : "ERROR" ,

" message " : "You� need � to � l o g i n /Data� i s � not � c o r r e c t �JSON/Command� not � d e f i n e d " }

}

operations definieras som en JSON-array med operationer ordnade i den ordning de utförts:

[ operation1 , operation2 , o p e r a t i o n 3 . . . ]

En operation från klienten definieras som ett JSON-objekt med strukturen:

{

" kind " : "INS/DEL" " p o s i t i o n " : i n t e g e r " s t r i n g " : " s t r i n g " }

Vid svaret från servern definieras en operation med tillägget user_id:

{ " kind " : "INS/DEL" " p o s i t i o n " : i n t e g e r " s t r i n g " : s t r i n g " user_id " : i n t e g e r }

Förklaring till en operations nycklar:

kind talar om vilken typ av operationer det är, INS (infoga) eller DEL (ta bort). Observera att INS/DEL skrivs med versaler.

position ett heltal som talar om var i texten operationen ska utföras, med start från 0. string texten som ska sättas in eller tas bort.

user_id användarid för att identifiera vilken klient operationen kommer från. När klienten skickar operationer till servern behövs inte denna information, då servern har koll på vilket användarid klienten har.

Något vi också såg tillsammans med operate-kommandot ovan var document_id. Om vi antar att vi har flera dokument som redigeras samtidigt på samma server, så är document_id till för att skilja dokumenten åt. Dock är exjobbet begränsat till att bara ha stöd för att redigera ett dokument åt gången, så i implementationen har document_id alltid satts till 0. Om man skulle bygga ut protokollet med lämpliga tillägg för hantering av flera dokument, så blir document_id mer användbar och nödvändig för att skilja dokumenten åt.

login-kommandot skulle kunna göras mer sofistikerad, t.ex. kräva användarnamn och lösenord, men det ligger utanför detta examensarbetes ramar.

I tabell 3.3 har vi ett exempel på hur kommunikationen mellan server och klienten kan se ut. I exemplet ser vi att:

(38)

Klient Server Dokumenttillstånd i klienten efter anrop

1 { "command" : " l o g i n " } { " r e s p o n s e " : { " s t a t u s " : "OK" , " message " : "Logged� i n " } , " user_id " : 33 } ”” 2 { "command" : " o p e r a t e " , " o p e r a t i o n s " : [ ] , "document_id" : 0 } { " r e s p o n s e " : { " s t a t u s " : "OK" , " message " : "" } " o p e r a t i o n s " : [ { " kind " : "INS" , " p o s i t i o n " : 0 , " s t r i n g " : "ABC" , " user_id " : 10 } ] } ”ABC” 3 { "command" : " o p e r a t e " , " o p e r a t i o n s " : [ { " kind " : "INS" , " p o s i t i o n " : 3 , " s t r i n g " : "YXA" } , { " kind " : "DEL" , " p o s i t i o n " : 2 , " s t r i n g " : "C" } , { " kind " : "DEL" , " p o s i t i o n " : 0 , " s t r i n g " : "A" } ] , "document_id" : 0 } { " r e s p o n s e " : { " s t a t u s " : "OK" , " message " : "" } " o p e r a t i o n s " : [ { " kind " : "INS" , " p o s i t i o n " : 0 , " s t r i n g " : "BAD" , " user_id " : 9 } ] } ”BADBYXA” 4 { "command" : " l o g o u t " } { " r e s p o n s e " : { " s t a t u s " : "OK" ,

" message " : "Logged� out " }

}

”BADBYXA”

(39)

2. Klienten skickar en operate-förfrågan utan några operationer, då den inte har skapat några än. Till svar får den en operation (från klient med ID 10) som säger att klienten ska lägga till strängen ABC.

3. Användaren på klienten ändrar så att det står BYXA istället för ABC. Klienten skickar detta till servern med operate-kommandot och får till svar att lägga till strängen BAD. Texten på klienten är därefter BADBYXA.

4. Klienten loggar ut.

3.3 Algoritmer

I denna sektion beskrivs de viktigaste algoritmerna som krävs för att designa nya klienter för samarbets-verktyget.

3.3.1 Övergripande operationshanteringsalgoritm

De Operationstransform-algoritmer vi gått igenom i bakgrundskapitlet, har utgått från att klienter direkt kommunicerat med varandra. Då har klienterna skickat de operationerna de skapat till varandra, den ena klienten har transformerat den andras operationer som inkommit och tvärtom. Här har vi däremot en klient-server struktur där transformationerna då sker mellan server och klient, servern är som ett nav mot vilket alla klienter kommunicerar mot och därmed sker inte transformationer direkt mellan klienter utan har servern som mellanhand. Servern har alltså hela tiden en kopia av texten som redigeras i form av operationer och kan därmed skicka dessa operationer till en eventuell ny klient som ansluter. Här nedan har vi den algoritm som konstruerats för att bestämma var och när transformationer ska ske i en klient-server modell:

• När användaren gör förändringar i dokumentet skapas operationer som skickas till servern. • Servern mottar dessa operationer och gör följande:

1. Servern känner till vilken som är den senaste operationen som servern senast skickat till klienten. Servern plockar ut alla operationer som är nyare än denna operation, låt oss kalla dem OP sserver. Låt oss kalla de operationer som kommer från klienten för OP sklient.

2. Servern transformerar nu OP sklient mot OP sserver och får då den transformerade mängden

OP s�

klient. De transformerade operationerna OP s�klient läggs till i databasen.

3. Informationen om klientens senaste operation uppdateras till den senaste operationen som nu är i databasen.

4. Servern transformerar nu OP sserver mot OP sklient och får då den transformerade mängden

OP s�

server. OP s�server skickas tillbaka till klienten.

• Klienten mottar OP s�

serverfrån servern. Om förfrågan från klienten var synkron (dvs klienten har

inte kunnat göra fler operationer under tiden förfrågan har gjorts) så händer följande: 1. Operationerna OP s�

serverutförs direkt på klienten, då de redan transformerats mot klientens

tidigare operationer på serversidan.

• Om klienten istället gjort en asynkron förfrågan (dvs. klienten har kunnat skapa nya operationer (OP sklient2) under tiden förfrågan skickats:

1. Operationerna OP s�

server måste transformeras mot de operationer som gjorts mellan det

kli-enten tidigare skickade förfrågan och det att servern svarat (OP sklient2) och tvärtom.

2. OP s�

server transformeras mot OP sklient2 och exekveras på klienten.

3. OP sklient2 transformeras mot OP s�server och skickas till servern på samma sätt som tidigare

Figur

Updating...

Relaterade ämnen :