Networking Tutorial · Core Protocols · Lesson 9 of 28
TCP
How TCP delivers a reliable, ordered byte stream: the three-way handshake, acknowledgements, and closing connections.
What TCP gives you
TCP (Transmission Control Protocol) turns unreliable packets into a reliable, ordered byte stream:
- Lost packets are retransmitted.
- Data arrives in order, without duplicates.
- Flow control stops a fast sender from overwhelming a slow receiver.
- Congestion control slows down when the network is overloaded.
HTTP/1.1, HTTP/2, database protocols, SSH and SMTP all run over TCP.
The three-way handshake
Before any data flows, client and server exchange three packets:
client -> server SYN "let's talk; my sequence number starts at x"
server -> client SYN-ACK "OK; mine starts at y, I got x"
client -> server ACK "got y"
That costs one round trip before the first byte of the request is sent, and TLS adds more. That is why reusing connections (keep-alive, connection pools) makes such a difference.
STATES = {
("CLOSED", "connect"): ("SYN_SENT", "send SYN"),
("SYN_SENT", "recv SYN-ACK"): ("ESTABLISHED", "send ACK"),
("ESTABLISHED", "close"): ("FIN_WAIT", "send FIN"),
("FIN_WAIT", "recv FIN+ACK"): ("TIME_WAIT", "send ACK, wait 2 x MSL"),
("TIME_WAIT", "timer"): ("CLOSED", "connection forgotten"),
}
state = "CLOSED"
for event in ["connect", "recv SYN-ACK", "close", "recv FIN+ACK", "timer"]:
state, action = STATES[(state, event)]
print(f"{event:14} -> {state:12} ({action})")
Output
connect -> SYN_SENT (send SYN) recv SYN-ACK -> ESTABLISHED (send ACK) close -> FIN_WAIT (send FIN) recv FIN+ACK -> TIME_WAIT (send ACK, wait 2 x MSL) timer -> CLOSED (connection forgotten)
Closing and TIME_WAIT
Each side sends FIN to close its direction. The side that closes first keeps the connection in TIME_WAIT for a while (60 seconds on Linux) so that late packets from the old connection can't be mistaken for a new one. A client that opens and closes thousands of short connections per second can run out of ephemeral ports this way. Reusing connections avoids it.
Timeouts you'll configure
- Connect timeout: how long to wait for the handshake (keep it short, around 1 to 3 seconds).
- Read timeout: how long to wait for response data.
- Idle timeout: how long an unused connection stays open. Load balancers, NAT gateways and firewalls all have one, and when they disagree with your app's keep-alive settings you get mysterious resets.
rtt_ms = 90 # e.g. a user in Europe calling a server in the eastern US
requests = 30
new_connection_each_time = requests * (1 + 1 + 1) * rtt_ms # TCP handshake + TLS 1.3 + request
reused_connection = (1 + 1) * rtt_ms + requests * rtt_ms # handshakes once, then one RTT per request
print(f"new connection per request: {new_connection_each_time / 1000:.1f} s")
print(f"one reused connection: {reused_connection / 1000:.1f} s")
Output
new connection per request: 8.1 s one reused connection: 2.9 s
Test yourself
What is the correct order of the TCP three-way handshake?
SYN, ACK, FINSYN, SYN-ACK, ACKACK, SYN, SYN-ACKHELLO, SYN, ACK
Show answer
Answer: SYN, SYN-ACK, ACK
The client sends SYN, the server answers SYN-ACK, and the client confirms with ACK.