TCP, UDP & Ports — Conversations Between Programs
What you'll learn
- What a port is and how one server can talk to thousands of clients on the same port
- How UDP works: small, fast and fire-and-forget
- How TCP works: the three-way handshake, sequence and acknowledgment numbers, retransmission, flow control, congestion control, and closing a connection
- How to detect corrupted data with the Internet checksum
- How to choose between TCP and UDP
- You will build three tools: a TCP state machine, a cumulative-ACK receiver and a checksum calculator
The apartment building and the phone call 📞
The IP address gets a packet to the right building. But a building has many apartments, and each one is a different program: your browser, your music app, your chat client. The port number is the apartment number. IP delivers to the computer; the transport layer delivers to the right program on it.
Now compare two ways of talking. UDP is like shouting a message through a window or dropping a postcard in the mailbox: quick, no setup, but you never learn whether it arrived, and two postcards can arrive in the wrong order. TCP is like a phone call: you dial, the other side picks up and says hello, you both know the line works, you talk in order, anything garbled gets repeated ("sorry, say that again?"), and at the end you both say goodbye. The call costs a little more effort, but you can trust what you hear.
Ports and sockets
A port is a 16-bit number, so it ranges from 0 to 65535. IANA divides the range by convention:
- 0 to 1023, well-known (system) ports: standard services.
22SSH,25SMTP,53DNS,80HTTP,123NTP,443HTTPS. On Unix-like systems, listening on them normally requires elevated privileges. - 1024 to 49151, registered ports: applications such as
3306MySQL,5432PostgreSQL and8080(a common alternative for HTTP). - 49152 to 65535, dynamic / ephemeral ports: handed out by your operating system as the source port of outgoing connections. (Linux by default uses 32768-60999, so do not be surprised by other numbers.)
A server listens on a well-known port. A client picks a free ephemeral port for itself and connects. A socket is one endpoint: an IP address plus a port, for example 203.0.113.80:443. A TCP connection is identified by both endpoints together with the protocol, the 5-tuple:
(protocol, source IP, source port, destination IP, destination port)
That is how a web server handles thousands of clients on one port, 443. Every client has a different source IP or source port, so every 5-tuple is different. TCP port 53 and UDP port 53 are also completely separate. You can see what your machine is listening on with ss -tuln (TCP and UDP, listening, numeric) on Linux.
Ports below 1024 are the well-known service ports and 49152 and above are the dynamic range. The three connections share the server's IP and port 443, yet none of them clashes with another, because the source IP or source port differs. Try adding a fourth tuple that repeats an existing one and see that a set keeps only one: two identical 5-tuples cannot exist at the same time.
SERVICES = {22: "SSH", 25: "SMTP", 53: "DNS", 80: "HTTP", 123: "NTP", 443: "HTTPS"}
def port_kind(port):
if port <= 1023:
return "well-known"
if port <= 49151:
return "registered"
return "dynamic/ephemeral"
for port in (22, 80, 443, 3306, 8080, 51514):
print(str(port).rjust(5), port_kind(port).ljust(18), SERVICES.get(port, "-"))
# One server port, three different clients: every 5-tuple is unique.
connections = {
("TCP", "198.51.100.10", 51514, "203.0.113.80", 443),
("TCP", "198.51.100.10", 51515, "203.0.113.80", 443), # same client, new source port
("TCP", "198.51.100.77", 51514, "203.0.113.80", 443), # other client, same source port
}
print(len(connections), "distinct connections, all to server port 443")
UDP: send it and forget it
UDP (User Datagram Protocol) is the thin protocol. Its header is only 8 bytes: source port, destination port, length and checksum (16 bits each). That is all. UDP does not set up a connection, does not number or re-send data and does not slow down when the network is busy. Each sendto becomes one self-contained datagram whose boundaries are preserved; datagrams may be lost, duplicated or reordered, and it is the application's job to cope.
Why would anyone want that? Because no handshake means no delay, and because for some traffic an old packet is worthless. In a voice call, a retransmitted packet that arrives a second late is garbage; a short glitch is better than waiting. UDP is used for DNS queries (one small question, one small answer), DHCP, NTP, live audio and video, online games, and QUIC (the transport under HTTP/3, which builds its own reliability on top of UDP). If you send UDP to a port where nobody listens, the destination usually answers with an ICMP "port unreachable" message.
TCP: a reliable stream
TCP (Transmission Control Protocol) gives programs a reliable, ordered stream of bytes over an unreliable network. It does that with a bigger header (20 bytes minimum), whose main fields are:
- source port and destination port (16 bits each)
- sequence number (32 bits): the number of the first data byte in this segment. TCP numbers bytes, not packets
- acknowledgment number (32 bits): the next byte the sender of this segment expects to receive
- flags:
SYN(start),ACK(acknowledging),FIN(done sending),RST(abort), and more - window size (16 bits): how much more data the receiver can accept
- checksum (16 bits), and optional options
Opening: the three-way handshake
Before any data flows, both sides agree on their starting sequence numbers (the ISN, chosen unpredictably for security):
Client Server
|---- SYN seq=100 ------------------->|
|<--- SYN+ACK seq=300 ack=101 ------------|
|---- ACK seq=101 ack=301 ------------>|
- The client sends SYN with its initial number (100).
- The server answers SYN+ACK: its own initial number (300) and
ack=101, "I got your SYN, I expect byte 101 next". A SYN uses up one sequence number. - The client confirms with ACK (
ack=301). Now both sides are ESTABLISHED.
Closing: FIN in each direction
Each direction is closed separately. The side that is done sends FIN, the other side acknowledges it; later the other side sends its own FIN and gets acknowledged. The side that closed first then waits in TIME_WAIT (about a minute on Linux, twice the maximum segment lifetime) so late stray packets cannot be mistaken for a new connection. A connection can also be aborted at once with RST; that is what a machine sends back when you connect to a TCP port nobody is listening on, and what you see as "connection refused".
Lab exercise 1
PythonGraded in Code LabModel a TCP endpoint as a state machine. It starts in CLOSED. Each input line is one event. After each event print the new state, or an error if that event is not allowed in the current state.
Allowed transitions (this is a simplified version of the real diagram; anything not listed is an error):
CLOSED:listengoes toLISTEN;connectgoes toSYN_SENTLISTEN:recv SYNgoes toSYN_RECEIVED;closegoes toCLOSEDSYN_SENT:recv SYN+ACKgoes toESTABLISHED;closegoes toCLOSEDSYN_RECEIVED:recv ACKgoes toESTABLISHED;closegoes toFIN_WAIT_1ESTABLISHED:closegoes toFIN_WAIT_1;recv FINgoes toCLOSE_WAITFIN_WAIT_1:recv ACKgoes toFIN_WAIT_2;recv FINgoes toCLOSINGFIN_WAIT_2:recv FINgoes toTIME_WAITCLOSING:recv ACKgoes toTIME_WAITCLOSE_WAIT:closegoes toLAST_ACKLAST_ACK:recv ACKgoes toCLOSEDTIME_WAIT:timeoutgoes toCLOSED
Output, one line per event (blank lines are skipped): <event> -> <STATE>. For a disallowed event print <event> -> error (stays <STATE>) and keep the current state.
Example
Input:
connect
recv SYN+ACK
recv SYN
Output:
connect -> SYN_SENT
recv SYN+ACK -> ESTABLISHED
recv SYN -> error (stays ESTABLISHED)
- A client opens, closes actively and waits in TIME_WAIT
- A server accepts a connection and is closed by the peer
- Simultaneous close goes through CLOSING
- Giving up early from LISTEN, SYN_SENT and SYN_RECEIVED
- Events that are not allowed leave the state unchanged
- Empty input prints nothing
Example run
- Input
connect recv SYN+ACK close recv ACK recv FIN timeout
- Expected output
connect -> SYN_SENT recv SYN+ACK -> ESTABLISHED close -> FIN_WAIT_1 recv ACK -> FIN_WAIT_2 recv FIN -> TIME_WAIT timeout -> CLOSED
import sys
# (state, event) -> next state
# TODO: add every transition listed in the prompt
TRANSITIONS = {
("CLOSED", "listen"): "LISTEN",
("CLOSED", "connect"): "SYN_SENT",
}
state = "CLOSED"
for line in sys.stdin:
event = line.strip()More lines in Code Lab: 4
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Reliability: sequence numbers, ACKs and retransmission
Packets get lost, duplicated and reordered, yet TCP delivers a perfect stream. The mechanism is built from the numbers in the header:
- The receiver sends back an acknowledgment number: "everything before this byte arrived; send me this one next". ACKs are cumulative: they only ever name the next byte still missing, even if later bytes have already arrived.
- If a segment is lost, the receiver keeps repeating the same ACK for each later segment (duplicate ACKs). After three duplicate ACKs the sender retransmits the missing segment at once (fast retransmit), without waiting.
- If no ACK arrives at all, a retransmission timeout (RTO) fires, computed from the measured round-trip time, and the sender retransmits.
- Out-of-order segments are buffered by the receiver and handed to the application in order, once the gap is filled. Duplicates are discarded.
Two more mechanisms stop TCP from overwhelming anyone:
- Flow control protects the receiver. Every segment carries a window size: how many more bytes the receiver has room for. The sender never has more unacknowledged bytes in flight than that.
- Congestion control protects the network. The sender keeps a congestion window and treats loss as a sign of overload. In the classic scheme it starts small and grows quickly (slow start), then grows by about one segment per round trip (congestion avoidance), and cuts the window roughly in half when it detects loss. Modern stacks use refined algorithms such as CUBIC or BBR, but the idea is the same: be fast, but back off when the network says stop.
Watch the difference between the two protocols on a link that loses some transmissions:
UDP delivered three of five messages and never knew. TCP delivered all five, in order, at the price of two extra transmissions and some waiting. Neither is better in general: it depends on whether a late message is worth having. Add m3 to the lost set and run it again.
// A link that loses certain transmissions. There is no randomness: the loss pattern is fixed.
const messages = ["m1", "m2", "m3", "m4", "m5"];
const lostOnFirstTry = new Set(["m2", "m4"]);
// UDP style: send every message once and never look back.
const udpReceived = messages.filter(function (m) { return !lostOnFirstTry.has(m); });
console.log("UDP received:", udpReceived.join(", "));
// TCP style: keep each message until it is acknowledged, retransmit after a timeout.
const tcpReceived = [];
let transmissions = 0;
messages.forEach(function (m) {
for (let attempt = 1; ; attempt++) {
transmissions++;
const lost = lostOnFirstTry.has(m) && attempt === 1;
if (!lost) {
tcpReceived.push(m);
break;
}
console.log("TCP " + m + " lost on attempt " + attempt + ": timeout, retransmit");
}
});
console.log("TCP received:", tcpReceived.join(", "));
console.log("TCP needed " + transmissions + " transmissions for " + messages.length + " messages");
Lab exercise 2
JavaScript (Node.js)Graded in Code LabWrite a TCP receiver that answers every arriving segment with a cumulative ACK.
Input: the first line is the sequence number of the first byte the receiver expects (the initial expected number). Every following line is an arriving segment <seq> <length>: it carries the bytes seq up to seq + length - 1. Segments can arrive out of order, duplicated or overlapping.
Rules: the receiver keeps out-of-order segments in a buffer. After each segment, the expected number moves forward over every contiguous byte it now has. It never moves backwards.
Output: after each segment print ack=<expected>, the acknowledgment number the receiver would send.
Example
Input:
1000
1000 100
1200 100
1100 100
Output:
ack=1100
ack=1100
ack=1300
(The segment at 1200 arrives before 1100, so the receiver repeats ack=1100: a duplicate ACK. Once 1100 shows up, 1100-1299 are complete.)
- Segments arrive in order
- One segment is lost and arrives late: duplicate ACKs, then a jump
- A retransmission that overlaps already received bytes
- A complete duplicate does not move the ACK
- No segments: nothing is printed
- 500 segments arriving in reverse order
Example run
- Input
1000 1000 100 1100 100 1200 50
- Expected output
ack=1100 ack=1200 ack=1250
const lines = require("fs").readFileSync(0, "utf8").split("\n").map((l) => l.trim()).filter((l) => l !== "");
if (lines.length > 0) {
let expected = Number(lines[0]); // next byte the receiver is waiting for
let pending = []; // buffered segments: { seq, end } where end = seq + length
for (const line of lines.slice(1)) {
const [seq, length] = line.split(/\s+/).map(Number);
pending.push({ seq, end: seq + length });
// TODO: move 'expected' forward over every buffered segment that touches it
// (a segment helps when seq <= expected < end), and drop segments that are no longer needed.
// Then print: ack=<expected>More lines in Code Lab: 2
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
Detecting corruption: the Internet checksum
Wires and radios flip bits now and then. The Ethernet FCS catches corruption on one link, but a packet can also be damaged inside a router. So IPv4, TCP and UDP carry a 16-bit Internet checksum (RFC 1071), computed like this:
- Treat the data as a sequence of 16-bit big-endian words. If the byte count is odd, pretend there is one extra zero byte at the end.
- Add all words with one's complement addition: whenever the sum overflows 16 bits, the carry is added back into the low bits ("folded").
- The checksum is the bitwise NOT of the folded sum.
The sender stores it in the header field (which counts as zero while computing). The receiver sums everything including the checksum field; if nothing changed, the folded sum is 0xFFFF, so recomputing gives 0. The checksum is cheap to compute but weak: it catches most random damage, not all, and it offers no protection against an attacker. For that you need cryptography (TLS).
For UDP and TCP the checksum also covers a pseudo-header (source IP, destination IP, protocol, length) so that a segment delivered to the wrong host is detected. Here you will compute the raw checksum, in C, the language this code lives in inside every operating system.
Lab exercise 3
CGraded in Code LabCompute the Internet checksum (RFC 1071) in C. Each input line is a hexadecimal string (no spaces, upper or lower case) that encodes the data bytes. Blank lines are skipped.
Output, one line per input line: checksum=0x<4 lowercase hex digits>. If a line has an odd number of hex digits or a character that is not a hex digit, print error: invalid hex instead.
The starter already decodes the hex into bytes. You write internet_checksum: add the bytes as 16-bit big-endian words (a missing last byte counts as 0), fold the carries back into 16 bits, then return the bitwise NOT.
Example
Input:
0001f203f4f5f6f7
Output:
checksum=0x220d
(The words are 0001, f203, f4f5, f6f7. Their sum is 0x2ddf0, folded it becomes 0xddf2, and the NOT of that is 0x220d.)
- The example from RFC 1071
- An IPv4 header: checksum field zeroed, then filled in (verifies to zero)
- An odd number of bytes is padded with a zero byte
- All zeros and all ones
- Invalid hex is reported and blank lines are skipped
- 1400 bytes of data (many carries to fold)
Example run
- Input
0001f203f4f5f6f7
- Expected output
checksum=0x220d
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static int hex_digit(int c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
More lines in Code Lab: 36
Hints and instant feedback are in Code Lab. Optional practice: it does not block finishing the lesson.
JavaScript and Python also run in your browser without an account; other languages use the secure server runner and need a free account.
TCP or UDP?
Ask one question: what should happen when data is lost?
- Every byte must arrive, in order, and a short delay is fine: web pages (HTTP/1.1 and HTTP/2), email, file transfer, SSH, databases. Use TCP.
- Fresh data matters more than complete data, or the application already handles loss itself: voice and video calls, games, DNS lookups, sensor readings, QUIC. Use UDP.
Remember the cost of TCP: a round trip for the handshake before the first byte, buffering and head-of-line blocking (one lost segment delays everything behind it), and per-connection state on both ends. Remember the cost of UDP: you must handle loss, ordering, congestion and sometimes security yourself, which is hard to do well. Many modern protocols, QUIC first of all, are exactly that work done once, properly, on top of UDP.
Troubleshooting with what you know
- "Connection refused" immediately: the packet arrived, but nothing listens on that port (the machine answered with RST).
- A connection that just hangs and times out: the SYN never got an answer. A firewall silently dropping it, a wrong IP or a broken route are typical causes. This difference between "refused" and "timed out" is one of the most useful clues in networking.
ss -tulnshows what is listening locally;ss -tnshows established TCP connections with their ports.
Question 1 of 5
What is the correct order of the TCP three-way handshake?
Question 2 of 5
Two different clients connect to the same server on port 443. How does the server tell their connections apart?
Question 3 of 5
Which application is the best fit for UDP?
Question 4 of 5
A receiver expects byte 1000. A 100-byte segment starting at 1000 arrives, then a 100-byte segment starting at 1200 (the one at 1100 was lost). What ACK number does it send after the second segment?
Question 5 of 5
You connect to a TCP port and get "connection refused" at once. What does that most likely mean?
Recap
- A port (16 bits) picks the program; a connection is identified by the 5-tuple. Well-known ports are 0-1023, ephemeral ports are used by clients.
- UDP: 8-byte header, no connection, no reliability. Best for DNS, real-time media, games and as a base for QUIC.
- TCP: reliable ordered byte stream. SYN, SYN+ACK, ACK opens it; FIN in each direction closes it; RST aborts it.
- Reliability comes from sequence numbers, cumulative ACKs, retransmission (timeout or three duplicate ACKs); flow control protects the receiver and congestion control protects the network.
- The Internet checksum is a 16-bit one's complement sum, inverted; it detects most accidental corruption.
- "Refused" means something answered; "timed out" means nothing did.
Practice next: run ss -tn while loading a web page and find your ephemeral source port and the server's port 443. If you can, capture a handshake with Wireshark or tcpdump and find the SYN, SYN+ACK and ACK. Then move on to DNS, HTTP and TLS.
Practice
Reading is not learning until you run something. Pick one.
Graded lab exercises
- Lab exercise 1PythonNot passed yet Try in Code Lab
- Lab exercise 2JavaScript (Node.js)Not passed yet Try in Code Lab
- Lab exercise 3CNot passed yet Try in Code Lab