TCP — den pålitelige
strømmen
Hvordan to maskiner blir enige om å snakke, holder styr på hver eneste byte, og overlever et nettverk som mister, omorganiserer og forsinker pakker.
Hva TCP egentlig lover deg
IP-laget under TCP er upålitelig. Pakker forsvinner, kommer i feil rekkefølge, blir duplisert. TCP sin jobb er å skjule alt dette og gi applikasjonen en illusjon av en perfekt rørledning.
Tenk deg at du sender et brev gjennom et postsystem som av og til mister konvolutter, av og til leverer dem i feil rekkefølge, og noen ganger lager kopier av samme brev uten å si fra. Hvis du vil at mottakeren skal få teksten din i rett orden, fullstendig, og uten duplikater — så må du bygge et system rundt postsystemet. Du må nummerere sidene, du må be om kvitteringer, du må sende på nytt det som ikke kommer fram.
Det er nøyaktig dette TCP gjør oppå IP. IP er postsystemet som ikke garanterer noe. TCP er protokollen som sitter på toppen og bygger opp en pålitelig, ordnet bytestrøm av byggeklossene IP gir den.
TCP er ikke én idé — det er en stabel av løsninger på en rekke konkrete problemer som dukker opp når man prøver å være pålitelig oppå et upålitelig nettverk. Hvert felt i headeren, hver tilstand i protokollen, er svaret på et spørsmål som måtte besvares.
Hva TCP garanterer
Fra et applikasjonsperspektiv tilbyr TCP fire ting som UDP ikke gir deg:
- PÅLITELIG Ingen byte forsvinner. Det som sendes, ankommer.
- I RIKTIG REKKEFØLGE Bytes leveres til applikasjonen i samme rekkefølge som de ble sendt.
- FLOW-CONTROLLED Senderen overvelder ikke mottakerens buffer.
- FORBINDELSESORIENTERT Begge parter blir enige om å snakke før data flyter.
Legg merke til at TCP er en bytestrøm uten meldingsgrenser. Hvis applikasjonen skriver "Hei" og deretter "Verden" til en TCP-socket, kan TCP velge å sende dem i én pakke, to pakker, eller dele dem opp på et helt annet sted. Mottakeren får bare en strøm av bytes — ingen markeringer for hvor "Hei" sluttet og "Verden" begynte. Hvis du trenger meldingsgrenser, må applikasjonen din legge dem på selv.
Anatomien til et segment
Hvert TCP-segment er en liten konvolutt med et standardisert sett av felt foran selve datalasten. Hvert felt er svaret på et konkret problem.
Hvorfor finnes hvert felt?
| Felt | Problemet det løser |
|---|---|
| Source / Dest Port | IP-adressen identifiserer maskinen, men hvilken applikasjon på maskinen? Portnumrene gjør multipleksing/demultipleksing mulig. |
| Sequence Number | Hvor i bytestrømmen begynner denne lasten? Nødvendig for å oppdage tap, dupliseringer og rekkefølgefeil. |
| ACK Number | Hvilken byte forventer jeg neste? Lar senderen vite hva som har kommet fram (kumulativ ACK). |
| Receive Window (rwnd) | Hvor mye plass har jeg igjen i mottaksbufferet? Grunnlaget for flow control. |
| Flags (SYN, FIN, ACK…) | Kontrollmeldinger for å åpne, lukke og bekrefte forbindelser. |
| Checksum | Oppdager bit-feil i headeren eller dataene — IP gir ingen slik garanti. |
| HLen (Data offset) | Hvor lang er headeren? Trengs fordi options-feltet er variabelt. |
Payload = Total IP Length − IP Header − TCP Header. Lengden kommer fra laget under.
MSS — den maksimale segmentstørrelsen
MSS (Maximum Segment Size) er den største mengden applikasjonsdata som kan legges inn i ett TCP-segment. På et vanlig Ethernet-nettverk er MTU 1500 bytes, og når du trekker fra 20 bytes IP-header og 20 bytes TCP-header, sitter du igjen med 1460 bytes som typisk MSS. Hvorfor bry seg? Fordi hvis TCP lager segmenter som er større enn det IP-laget kan håndtere uten fragmentering, må rutere stykke pakkene opp underveis — og det er både tregt og skjørt. MSS forhandles fram under det første håndtrykket nettopp for å unngå dette.
Hvis MTU på en lenke er 1500 bytes og man bruker IPv4 + standard TCP, hva blir typisk MSS?
Hvordan TCP teller bytes
Sekvensnummer er kjernen i hele påliteligheten. Misforstå dette og resten av TCP gir ikke mening.
Det vanligste misforståelsen er å tro at sekvensnummeret nummererer segmenter — segment 1, segment 2, segment 3. Det gjør det ikke. Sekvensnummeret er en peker inn i en bytestrøm. Hvis senderen skriver 1000 bytes inn i socketen sin og TCP velger å dele dem opp i to segmenter på 500 bytes hver, så får første segment seq=0 (eller hva enn startnummeret er) og andre segment seq=500 — fordi det andre segmentet inneholder bytene 500 til 999.
ACK-nummeret følger samme logikk, men fra mottakerens side: "den neste byten jeg forventer å se er denne." Hvis mottakeren har fått alle bytes opp til 999, sender hun ACK=1000. Dette er en kumulativ ACK — den bekrefter ikke bare den siste pakken, men alt opp til byte 1000.
Hvis en ACK forsvinner på veien, er det ikke en katastrofe — neste ACK overstyrer den uansett. ACK=1000 forteller senderen at alt før byte 1000 er trygt framme, selv om den forrige ACK=500 aldri kom fram. Dette gir robusthet uten ekstra arbeid.
Et lite telnet-eksempel
Brukeren på Host A skriver bokstaven 'C'. Den sendes til Host B, som ekkoer den tilbake. Følg sekvens- og ACK-numrene:
Hvor lenge skal vi vente?
Hvis et segment går tapt må senderen sende det på nytt. Men hvor lenge skal hun vente før hun bestemmer seg for at det er tapt?
Dette er et overraskende delikat spørsmål. Vent for kort, og du sender ting unødvendig på nytt — du forverrer den allerede stressede situasjonen og belaster nettverket. Vent for lenge, og applikasjonen din føles treg og ubrukelig på forbindelser med tap. Det riktige svaret er "litt mer enn typisk RTT", men hva er typisk RTT på et nettverk hvor latency hopper opp og ned?
RTT varierer fra øyeblikk til øyeblikk. En enkelt måling kan være atypisk — en tilfeldig topp, eller en uvanlig rask runde. Hvis vi setter timeout basert på siste måling alene, blir den voldsomt ustabil.
Vi glatter ut målingene med et eksponentielt vektet glidende gjennomsnitt. Hver nye måling påvirker estimatet litt — gamle målinger taper innflytelse eksponentielt fort.
EstimatedRTT = (1 − α) × EstimatedRTT + α × SampleRTT
Med α = 0.125 bidrar hver ny måling med 12.5% til estimatet. Estimatet svinger mye mindre enn rådataene, men reagerer fortsatt på reelle endringer i nettverket.
Sikkerhetsmargin: DevRTT
Men EstimatedRTT alene er ikke nok. Hvis vi setter Timeout = EstimatedRTT ville halvparten av målingene ligget over og halvparten under — vi ville fått false timeouts hele tiden. Vi trenger en sikkerhetsmargin, og marginen bør være større når RTT-en svinger mye.
Løsningen er å estimere variansen — hvor mye SampleRTT typisk avviker fra estimatet:
DevRTT = (1 − β) × DevRTT + β × |SampleRTT − EstimatedRTT|
Og så får TCP sin endelige timeout-formel:
TimeoutInterval = EstimatedRTT + 4 × DevRTT
Faktoren 4 er ikke magisk — den er en praktisk valgt verdi som gir en romslig margin uten å være absurd stor. Når RTT er stabil, blir DevRTT lite og timeouten ligger tett over EstimatedRTT. Når RTT begynner å svinge, vokser DevRTT og timeouten utvider seg automatisk.
Én timer per forbindelse
Den naive intuisjonen sier: én timer per pakke. Det er feil — og av en god grunn.
Hvis du tenker logisk på det, virker det fornuftig at hver enkelt pakke skal ha sin egen timer. Sender du tre pakker, så starter du tre stoppeklokker, og hvis en av dem renner ut vet du nøyaktig hvilken pakke som skal sendes på nytt. Klart, ryddig, intuitivt.
En travel server kan ha 10 000 åpne forbindelser, hver med tusenvis av pakker i flukt. Det blir millioner av aktive timer-objekter. Bare det å vekke prosessen for å sjekke hver av dem ville kvelt CPU-en. Det skalerer ikke.
Standard TCP-implementasjoner bruker én enkelt retransmisjons-timer per forbindelse. Den følger med på den eldste ubekreftede pakken — SendBase. Når en ACK kommer som flytter SendBase framover, restarter timeren (hvis det fortsatt finnes ubekreftede pakker). Hvis timeren renner ut, sendes den eldste ubekreftede pakken på nytt.
Senderens tre hendelser
Senderen reagerer på tre ting, og bare tre ting:
-
01 ·
Data fra applikasjonen. Lag et nytt segment med
seq = NextSeqNum, send det til IP-laget, oppdater NextSeqNum. Hvis timeren ikke allerede er i gang, start den. - 02 · Timeout. Send det eldste ubekreftede segmentet på nytt. Restart timeren.
-
03 ·
ACK mottatt med verdi y. Hvis
y > SendBase, så har noe nytt blitt bekreftet — flytt SendBase til y. Hvis det fortsatt finnes ubekreftede segmenter, restart timeren; ellers stopp den.
SendBase er venstre kant (eldste ubekreftede byte), NextSeqNum er høyre kant (neste byte som skal sendes). Alt mellom dem er "in flight" — sendt, men ikke bekreftet ennå.
Når ting går galt
La oss se hva senderen faktisk gjør i tre vanlige uhell — og hvorfor det enkle regelsettet i forrige kapittel håndterer dem alle.
Tre retransmisjonsscenarier
Hva disse tre scenariene viser
Den tredje situasjonen er den vakre: selv om en ACK forsvinner på veien, redder den neste ACK alt — fordi ACK-er er kumulative. Senderen trenger ikke retransmittere noe i det hele tatt. Tap av en isolert ACK koster ingenting, så lenge en senere ACK kommer fram før timeouten.
Den andre situasjonen — premature timeout — er mer interessant. Senderen blir utålmodig og sender den første pakken på nytt før ACK-en rakk fram. Mottakeren ser et duplikat, som hun rolig kaster. ACK-ene som var underveis kommer fram en etter en, SendBase glir framover, og alt ender opp riktig. Litt sløsing med båndbredde, men ingen feil.
Ikke vent på timeouten
Timeouts er konservative — de er gjerne satt til hundrevis av millisekunder. Hvis vi har et tydelig hint om at en pakke er tapt, hvorfor ikke handle på det med en gang?
Mottakerens regel for ACK-generering, definert i RFC 5681, er enkel: hvis det kommer et segment ut av rekkefølge — altså med høyere sekvensnummer enn forventet — så skal mottakeren umiddelbart sende en duplikat ACK som gjentar nummeret på den byten hun fortsatt venter på. Hvert segment som ankommer etter hullet vil produsere en ny duplikat ACK med samme nummer.
Senderen som ser dette tenker: "Hvorfor får jeg den samme ACK-en om og om igjen? Det må bety at nyere segmenter har kommet fram, men noe gammelt mangler." Det er en sterk indikasjon på tap — sterkere enn å bare vente på timeout.
Hvis senderen mottar tre duplikat-ACKs på rad for samme byte, retransmitterer hun det manglende segmentet umiddelbart — uten å vente på at timeren skal renne ut. Dette kalles fast retransmit.
Hvorfor akkurat tre?
Det er et godt spørsmål. Hvorfor ikke én? Eller fem? Svaret ligger i at duplikat-ACKs ikke alltid betyr tap. Nettverket kan også omorganisere pakker — pakke 5 kan komme fram før pakke 4 selv om begge er sendt korrekt. Hvis vi handlet på den første duplikat-ACK-en ville vi ofte retransmittere helt unødvendig, fordi den manglende pakken er på vei og bare ble litt forsinket.
Tre duplikat-ACKs er en empirisk valgt balanse: nok til at omorganisering sannsynligvis ikke er forklaringen, men ikke så mange at vi taper for mye tid før vi handler. Det er en konstant som har fått stå seg gjennom mer enn 20 år med erfaring med TCP-implementasjoner.
Hvorfor venter ikke TCP på bare én duplikat-ACK før den retransmitterer?
Ikke drukne mottakeren
TCP er pålitelig på vegne av nettverket — men den må også beskytte mottakeren mot seg selv. Hva om senderen er raskere enn applikasjonen som skal lese dataene?
Forestill deg en server som strømmer data inn i en mottakers TCP-buffer mye raskere enn applikasjonen klarer å lese ut av den. Bufferet vokser. Til slutt er det fullt — og det neste segmentet som ankommer har ingen plass å være. Det blir kastet, må retransmitteres, og hele situasjonen blir verre. TCP løser dette ved å la mottakeren stadig fortelle senderen nøyaktig hvor mye plass hun har igjen.
I hver eneste ACK forteller mottakeren senderen hvor mange ledige bytes hun har i mottaksbufferet sitt — verdien legges inn i Receive Window-feltet i headeren. Senderen forplikter seg til å aldri ha mer enn rwnd bytes "in flight" (sendt, men ikke bekreftet) samtidig.
Resultatet er en slags rytme: senderen sender data, mottakerens applikasjon leser data, og rwnd vokser og krymper i takt. Hvis applikasjonen plutselig stopper å lese, krymper rwnd raskt mot null, og senderen pauser automatisk. Når applikasjonen våkner og leser igjen, vokser rwnd og senderen får lov til å fortsette.
16-bits flaskehalsen
Det originale rwnd-feltet er bare 16 bits — maksimalt 65 535 bytes, eller 64 KB. På et raskt moderne nettverk er dette altfor lite. Hvis du har en linje på 1 Gbps med 100 ms ping, kan du bare ha 64 KB i flukt før du må vente på en ACK. Resultatet er at lenken står ledig mesteparten av tiden. Dette kalles "long fat networks"-problemet.
Løsningen er TCP Window Scale — en option som forhandles fram under det første håndtrykket. Den lar begge sider bli enige om en multiplikator (en bit-shift på opptil 14 bits). Med maksimal skalering kan rwnd representere opptil omtrent 1 GB. Det er nok til å fylle selv ekstremt raske og høyforsinkede lenker.
Hva er forskjellen på flow control og congestion control?
Det tre-veis håndtrykket
Før noen data kan flyte, må de to partene bli enige om at de skal snakke — og avtale parametere som startsekvensnummer. Hvorfor trenger vi tre meldinger for noe så enkelt?
Den naive løsningen er et to-veis håndtrykk: klienten sier "la oss snakke", serveren svarer "OK". Klart, enkelt — og dessverre ødelagt. Problemet er at meldinger kan forsinkes vilkårlig lenge i nettverket, og gamle meldinger kan dukke opp lenge etter at klienten har gitt opp og fortsatt videre.
Tenk deg at klienten sender req_conn(x), men aldri får svar. Hun gir opp og lukker. I mellomtiden var req_conn(x) bare forsinket i nettverket — den ankommer endelig serveren, som glade åpner en forbindelse og venter på data. Klienten er borte. Serveren har en halvåpen forbindelse som aldri vil bli brukt — og potensielt mange av dem, hvis dette skjer ofte. Dette er en reell sårbarhet (SYN flooding-angrep utnytter nettopp dette).
Verre: en gammel req_conn(x) kombinert med gamle data(x+1)-pakker fra en avsluttet forbindelse kan forveksles med starten på en ny økt — og dataene blir akseptert som om de hørte til. Dette er ikke teoretisk; det er en reell måte protokoller har feilet på.
I et 3-veis håndtrykk velger begge parter et tilfeldig startsekvensnummer (x og y). Klienten må deretter bekrefte serverens y i en tredje melding før forbindelsen anses som etablert. Det betyr at serveren ikke åpner en forbindelse på alvor før hun har fått bevis på at klienten faktisk eksisterer og er aktivt klar — ikke bare en gjenganger fra fortiden.
Selve dansen
Legg merke til at den siste ACK-meldingen kan inneholde data — den trenger ikke være tom. Dette er en optimalisering: hvis klienten allerede vet hva hun vil si (for eksempel en HTTP-forespørsel), kan hun pakke den inn i den tredje håndtrykksmeldingen og spare en runde.
Et høflig farvel
Å lukke en forbindelse er ikke trivielt heller — begge sider kan ha mer data å sende, og siste melding kan bli borte.
Når en av partene er ferdig, sender hun et segment med FIN-flagget satt. Den andre siden svarer med en ACK. Men det er bare den ene retningen som er stengt — TCP er full duplex, så den andre siden kan fortsatt ha mer å si. Når også den siden er ferdig, sender hun sin egen FIN, og den første parten svarer med en ACK.
Fire meldinger totalt — selv om FIN og ACK fra samme side kan slås sammen til ett segment. Klienten som initierer lukkingen går inn i en spesiell TIME_WAIT-tilstand etter sin siste ACK, hvor hun blir værende i typisk 30 sekunder til 2 minutter (2 × MSL — Maximum Segment Lifetime). Hvorfor? Fordi den siste ACK-en kan ha gått tapt. Hvis den gjorde det, vil serveren retransmittere sin FIN, og klienten må fortsatt være rundt for å svare på den. Først etter timeoutet utløper får klienten faktisk lov til å glemme forbindelsen helt.
Det store bildet
Vi har gått gjennom segmentstruktur, sekvensnummer, RTT-estimering, retransmisjonslogikk, fast retransmit, flow control, og forbindelsesetablering og -avslutning. Alt henger sammen: hver mekanisme finnes fordi en tidligere mekanisme ville feilet uten den. Sekvensnumre gjør ACK-er meningsfulle. Kumulative ACK-er gjør tapte ACK-er overlevelige. RTT-estimering gjør timeouts robuste. En enkelt timer per forbindelse gjør protokollen skalerbar. Tre-veis håndtrykk gjør forbindelser sikre mot gjengangere fra fortiden.
Det neste kapittelet — congestion control — handler om noe ganske annet: ikke hvordan man holder en enkelt forbindelse pålitelig, men hvordan tusenvis av forbindelser sammen kan dele et begrenset nettverk uten å kollapse det. Der dekker vi slow start, AIMD, TCP Reno vs. Tahoe, CUBIC, rettferdighet og QUIC.
Se også: Prinsipper for pålitelig dataoverføring — rdt-protokollene, pipelining, GBN og SR som TCP bygger videre på.
Se også: Transportlagets tjenester, multipleksing/demultipleksing og UDP — grunnlaget for hele kapittelet.