TCP Connections
By the end of this lesson you will be able to explain what TCP guarantees, walk through the three-way handshake with real sequence numbers, calculate sequence and acknowledgement numbers for a stream of segments, decode TCP flags with Python, and estimate a retransmission timeout.
IP makes a best effort to deliver packets, but it promises nothing. Packets can be lost, duplicated or arrive out of order. Yet when you download your marksheet PDF, every byte arrives perfectly. That is the work of TCP (Transmission Control Protocol, currently specified in RFC 9293).
What TCP promises
TCP gives applications a reliable, ordered byte stream between two programs:
- Connection-oriented. Both sides agree to talk before any data flows.
- Reliable. Lost data is detected and sent again.
- Ordered. Bytes are handed to the application in the order they were sent.
- Full duplex. Both sides can send at the same time.
- Flow and congestion control. The sender slows down for a slow receiver or a busy network. That is the next lesson, Flow Control & Congestion Control.
Think of a registered post parcel compared with an ordinary postcard. Registered post needs a signature (an acknowledgement); if no signature comes back, the post office tries again. UDP is the postcard; TCP is registered post.
The TCP header
The TCP header is at least 20 bytes:
| Field | Bits | Purpose |
|---|---|---|
| Source port, destination port | 16 each | Which programs |
| Sequence number | 32 | Number of the first data byte in this segment |
| Acknowledgement number | 32 | The next byte the sender expects to receive |
| Data offset | 4 | Header length in 4-byte words |
| Flags | 8 used today | SYN, ACK, FIN, RST, PSH, URG, ECE, CWR |
| Window | 16 | How many bytes the receiver can accept now |
| Checksum | 16 | Covers header, data and a pseudo-header with the IP addresses |
| Urgent pointer | 16 | Rarely used |
| Options | 0 to 40 bytes | MSS, window scale, SACK, timestamps |
The most important flags:
- SYN: synchronise sequence numbers (start a connection).
- ACK: the acknowledgement number is valid. Set on almost every segment after the first.
- FIN: I have finished sending.
- RST: reset; abort the connection at once.
Flags are single bits in one byte, so Python can test them with &.
import struct
FLAGS = {0x01: "FIN", 0x02: "SYN", 0x04: "RST", 0x08: "PSH", 0x10: "ACK", 0x20: "URG"}
def decode(segment_hex):
seg = bytes.fromhex(segment_hex)
src, dst, seq, ack, off_flags, window = struct.unpack("!HHIIHH", seg[:16])
flags = [name for bit, name in FLAGS.items() if off_flags & bit]
print(f"{src} -> {dst} seq={seq} ack={ack} window={window} flags={'+'.join(flags)}")
decode("cb2001bb" "00000064" "00000000" "5002" "faf0") # first segment
decode("01bbcb20" "0000012c" "00000065" "5012" "ffff") # reply
decode("cb2001bb" "00000065" "0000012d" "5010" "faf0") # third segment52000 -> 443 seq=100 ack=0 window=64240 flags=SYN 443 -> 52000 seq=300 ack=101 window=65535 flags=SYN+ACK 52000 -> 443 seq=101 ack=301 window=64240 flags=ACK
Those three segments are a complete three-way handshake. Let us see why.
The three-way handshake
Before data flows, both sides pick a random starting sequence number, called the ISN (initial sequence number), and tell the other side.
- SYN. The client sends SYN with its ISN, say
seq=100. - SYN-ACK. The server replies with SYN and its own ISN,
seq=300, and acknowledges the client:ack=101. Why 101? A SYN uses up one sequence number, so the next byte expected is 101. - ACK. The client acknowledges the server:
seq=101, ack=301.
Now both sides know each other's starting numbers, and the connection is established. It is like a phone call: "Hello, can you hear me?" "Yes, I can hear you, can you hear me?" "Yes." Only then does the real talk begin.
ISNs are random, not 0, so that old segments from an earlier connection are not mistaken for new ones, and so that attackers cannot easily guess them.
Sequence and acknowledgement numbers
TCP numbers bytes, not segments. A segment's sequence number is the number of its first byte. The acknowledgement number says "I have received everything before this byte; send this one next". This is called a cumulative acknowledgement.
GATE questions often ask: given the starting number and segment sizes, find each segment's sequence number and the ACK that follows it.
isn = 5000
seq = isn + 1 # the SYN used one number
for size in [1000, 1000, 500]:
ack = seq + size
print(f"segment seq={seq}, {size} bytes covers {seq}-{ack - 1}, receiver replies ack={ack}")
seq = acksegment seq=5001, 1000 bytes covers 5001-6000, receiver replies ack=6001 segment seq=6001, 1000 bytes covers 6001-7000, receiver replies ack=7001 segment seq=7001, 500 bytes covers 7001-7500, receiver replies ack=7501
If the second segment is lost, the receiver keeps replying ack=6001 even when the third arrives, because 6001 is still the next byte it needs. Those duplicate ACKs are a strong hint to the sender that something was lost.
Retransmission and timeouts
The sender starts a timer for data it has sent. If no ACK arrives before the retransmission timeout (RTO), it sends the data again. But how long should the timer be? Too short wastes bandwidth on needless resends; too long leaves the connection idle.
TCP measures the round-trip time (RTT) and keeps a smoothed average. RFC 6298 gives the standard formulas:
- SRTT = (1 - α) × SRTT + α × R, with α = 1/8
- RTTVAR = (1 - β) × RTTVAR + β × |SRTT - R|, with β = 1/4 (using the old SRTT)
- RTO = SRTT + 4 × RTTVAR (with a minimum of 1 second in the RFC)
For the first sample R, SRTT = R and RTTVAR = R / 2.
samples = [100, 120, 80, 200] # measured RTTs in ms
srtt = samples[0]
rttvar = samples[0] / 2
print(f"after {samples[0]} ms: SRTT {srtt:.1f}, RTTVAR {rttvar:.1f}, RTO {srtt + 4 * rttvar:.1f}")
for r in samples[1:]:
rttvar = 0.75 * rttvar + 0.25 * abs(srtt - r)
srtt = 0.875 * srtt + 0.125 * r
print(f"after {r} ms: SRTT {srtt:.1f}, RTTVAR {rttvar:.1f}, RTO {srtt + 4 * rttvar:.1f}")after 100 ms: SRTT 100.0, RTTVAR 50.0, RTO 300.0 after 120 ms: SRTT 102.5, RTTVAR 42.5, RTO 272.5 after 80 ms: SRTT 99.7, RTTVAR 37.5, RTO 249.7 after 200 ms: SRTT 112.2, RTTVAR 53.2, RTO 325.0
Notice how the jump to 200 ms raises RTTVAR, so the timeout grows to give slow replies more room. (We ignored the RFC's 1-second minimum to keep the numbers readable.) If a timeout does fire, TCP doubles the RTO for the next try; this is called exponential backoff.
Older textbooks use a simpler formula from early TCP: RTO = 2 × SRTT. Use whichever your exam question states.
Closing a connection
Either side can close. Because TCP is full duplex, each direction is closed separately, usually in four steps:
- A sends FIN: "I have nothing more to send".
- B sends ACK.
- When B is also done, B sends FIN.
- A sends ACK, then waits in the TIME_WAIT state for twice the maximum segment lifetime, in case its last ACK was lost and B resends its FIN.
Steps 2 and 3 are often combined into one FIN+ACK segment. A RST ends a connection instantly, for example when a segment arrives for a port with no listening program.
Common mistakes
- Thinking TCP numbers segments. It numbers bytes. The sequence number is the first byte's number.
- Forgetting that SYN and FIN each use one sequence number. That is why the ACK to a SYN with seq 100 is 101.
- Thinking the ACK number is the last byte received. It is the next byte expected.
- Thinking TCP keeps message boundaries. TCP is a byte stream; two
sendcalls may arrive as onerecv.
Key points
- TCP gives a reliable, ordered, full-duplex byte stream with flow and congestion control.
- The three-way handshake is SYN, SYN-ACK, ACK, exchanging random initial sequence numbers.
- Sequence numbers count bytes; ACKs are cumulative and name the next byte expected.
- Lost data is resent after a timeout or after duplicate ACKs.
- RTO = SRTT + 4 × RTTVAR, updated from each RTT sample; timeouts double on each retry.
- Connections close with FIN and ACK in each direction, then TIME_WAIT; RST aborts.
Exercise
A client's ISN is 2,000 and the server's ISN is 9,000. The client sends 3 segments of 1,460 bytes after the handshake. Print the full handshake (seq and ack for each of the three segments), then each data segment's sequence number and the ACK the server sends back.
Show solution
c_isn, s_isn = 2000, 9000
print(f"SYN seq={c_isn}")
print(f"SYN-ACK seq={s_isn} ack={c_isn + 1}")
print(f"ACK seq={c_isn + 1} ack={s_isn + 1}")
seq = c_isn + 1
for _ in range(3):
print(f"DATA seq={seq} len=1460 -> server ack={seq + 1460}")
seq += 1460SYN seq=2000 SYN-ACK seq=9000 ack=2001 ACK seq=2001 ack=9001 DATA seq=2001 len=1460 -> server ack=3461 DATA seq=3461 len=1460 -> server ack=4921 DATA seq=4921 len=1460 -> server ack=6381
Each SYN uses one sequence number, so data starts at 2,001. Each ACK is the sequence number plus the bytes received: the next byte the server expects.
Quick Quiz
1. What is the correct order of the TCP three-way handshake?
The client sends SYN, the server answers SYN-ACK, and the client confirms with ACK.
2. A segment has seq = 4001 and carries 500 bytes. What ACK does the receiver send if all is well?
The next byte expected is 4001 + 500 = 4501.
3. Which flag aborts a TCP connection immediately?
RST resets the connection; FIN closes it politely.
4. SRTT is 100 ms and RTTVAR is 20 ms. Using RFC 6298, what is the RTO (ignoring the minimum)?
RTO = SRTT + 4 × RTTVAR = 100 + 80 = 180 ms.
Learn Computer Networks with a mentor
Free lessons get you started. Our Computer Science Fundamentals training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
