Networking Fundamentals · beginner · ~12 min
- Explain the difference between **TCP** (connection-oriented, reliable) and **UDP** (connectionless, best-effort) in plain language - Decide correctly which transport protocol a given application should use - Describe what a **port** is, why it is 16 bits, and how an IP address plus a port identifies one endpoint - Walk through the **three-way handshake** and explain how connect and SYN scans exploit it - Recognize the common well-known service ports on sight (22, 80, 443, 445, 3306, 3389, and friends) - Read a small C program that opens a TCP connection to a port and understand what each socket call is doing
When two programs on different machines talk to each other, the network has to solve two problems: get the bytes to the right computer, and then hand them to the right program on that computer. The first job belongs to IP addresses, which you met in The TCP/IP and OSI models. The second job belongs to the transport layer, and that is what this lesson is about.
The transport layer offers two main protocols. TCP (Transmission Control Protocol) behaves like a phone call: you dial, the other side picks up, and then you have a reliable, ordered conversation where nothing is lost. UDP (User Datagram Protocol) behaves like dropping postcards in a mailbox: each message goes out on its own, they might arrive out of order, and some might never arrive at all — but it is fast and simple.
To tell programs apart on the same machine, the transport layer uses a port: a number from 0 to 65535. A web server listens on port 80 or 443, a database on 3306, SSH on 22. The combination of an IP address and a port (written 93.184.216.34:443) names exactly one endpoint you can talk to.
In terminology: TCP and UDP live at layer 4 (transport) of the OSI model, directly above IP at layer 3. TCP is connection-oriented and reliable; UDP is connectionless and best-effort. Ports are the transport-layer addressing mechanism, just as IP addresses are the network-layer addressing mechanism.
Almost everything you do on a network rests on this choice. Loading a web page, pushing to a Git repository, or logging into a server over SSH all ride on TCP because they cannot tolerate missing or scrambled data. A live video call, a DNS lookup, or a fast-paced multiplayer game uses UDP because a fresh packet is more useful than a perfectly reconstructed old one.
For anyone learning defensive security, ports and transports are the map of the attack surface. Scanning and service enumeration — figuring out what is running where — is the first real step of assessing a machine, and it is built entirely on these ideas:
Every open port is a running service, and every running service is a door. Knowing which doors exist is the whole point of the exercise.
Definition. TCP is a transport protocol that establishes a connection before sending data, then guarantees that the bytes arrive complete and in order.
Plain-language explanation. Think of a phone call. Before you say anything, both sides agree the line is open. During the call, if the other person misses a word, they say "what?" and you repeat it. TCP does the same thing automatically for data.
How it works internally. Each byte TCP sends is numbered with a sequence number. The receiver sends back acknowledgements (ACKs) saying "I have everything up to byte N." If an ACK does not come back in time, TCP assumes the data was lost and retransmits it. The receiver reorders anything that arrives out of sequence, so your program always reads a clean, ordered stream.
When to use it. Any time correctness matters more than raw speed: web pages, file transfers, email, remote login, databases.
When NOT to use it. Real-time media where a late packet is worthless (the moment has passed), or tiny one-shot request/response messages where the handshake overhead is not worth it.
Common pitfall. People assume "TCP guarantees delivery," full stop. It guarantees delivery as long as the connection stays up. If the network dies, TCP eventually gives up and reports an error — it does not magically deliver bytes to an unreachable host.
Definition. UDP is a transport protocol that sends independent messages (datagrams) with no connection setup, no retransmission, and no ordering.
Plain-language explanation. You write a message on a postcard and drop it in the mail. You do not call ahead. Most arrive; a few might not; two mailed together might arrive in either order. UDP trades reliability for speed and simplicity.
How it works internally. UDP adds a tiny header — source port, destination port, length, checksum — in front of your data and hands it to IP. That is essentially all it does. There is no state kept about a "connection," which is why a UDP server can answer thousands of clients without tracking any of them.
When to use it. DNS lookups (small, fast, retry in the app if needed), DHCP, streaming audio/video, VoIP, online games, and telemetry where the newest sample matters most.
When NOT to use it. Anything where losing or reordering data corrupts the result and the application is not prepared to handle that itself.
Common pitfall. Believing UDP is "unreliable = useless." Reliability can be added on top of UDP in the application when you want speed plus control (this is exactly what modern protocols like QUIC do).
TCP (phone call) UDP (postcards)
Client Server Client Server
| --- SYN ---> | | --- msg1 ---> | (may arrive)
| <- SYN-ACK - | | --- msg2 ---> | (may be lost)
| --- ACK ---> | | --- msg3 ---> | (may reorder)
| == data flows =| | |
| ordered, | | no setup, |
| acked, | | no acks, |
| retransmitted | | no ordering |
Knowledge check. A DNS resolver sends one small query and expects one small answer. Why is UDP a reasonable default here, and what does the application have to do that TCP would otherwise handle?
Definition. A port is a 16-bit unsigned number (0 to 65535) that identifies a specific endpoint — usually one program — inside a host.
Plain-language explanation. The IP address is the street address of an apartment building; the port is the apartment number. Mail (packets) reaches the building via IP, then the port says which apartment gets it. That is why a single server at one IP can run a web server, an SSH server, and a database at the same time: each listens on a different port.
How it works internally. Every TCP or UDP packet header carries a source port and a destination port, each 16 bits — which is exactly why the range stops at 65535 (2^16 - 1). The operating system keeps a table mapping listening ports to the programs that own them. When a packet arrives, the OS looks at the destination port and delivers the data to the right program.
Port ranges.
| Range | Name | Typical use |
|---|---|---|
| 0–1023 | Well-known | Standard services (HTTP 80, SSH 22); binding usually needs admin rights |
| 1024–49151 | Registered | Assigned to specific applications (MySQL 3306, RDP 3389) |
| 49152–65535 | Ephemeral / dynamic | Temporary source ports the OS hands out to client connections |
When to care. Every time you connect, listen, scan, or configure a firewall, you are working with ports.
Common pitfall. Assuming a service is always on its standard port. Port 80 is a convention, not a law — an admin can run a web server on 8080 or SSH on 2222. Standard ports are a strong hint, never a guarantee.
Knowledge check. Why can port numbers only go up to 65535 and not, say, 100000? Tie your answer to the size of the port field in the packet header.
Definition. The three-way handshake is TCP's connection-setup procedure: SYN, then SYN-ACK, then ACK.
Plain-language explanation. It is a short greeting where both sides confirm they can hear each other before real data flows. The client says "let's talk" (SYN), the server says "okay, and let's talk" (SYN-ACK), and the client says "agreed" (ACK). Now the connection is established.
How it works internally. Each side picks an initial sequence number and tells the other about it during the handshake, so both ends know where the byte numbering starts. Only after the third message does TCP consider the connection open and let data flow.
How scanning exploits it. A port scanner learns whether a port is open by watching how the target responds to a SYN:
| Scan type | What it sends | What it observes | Trade-off |
|---|---|---|---|
| Full connect | Completes the whole handshake | Connection succeeds ⇒ port open | Reliable, but the connection is logged by the service |
| SYN ("half-open") | Only the first SYN | SYN-ACK back ⇒ open; RST back ⇒ closed | Quieter, never finishes the handshake; needs raw-socket privileges |
Common pitfall. Thinking a closed port stays silent. A normally-closed TCP port usually replies with a RST (reset) immediately; it is a filtered port (blocked by a firewall) that tends to give no answer at all. "No reply" and "closed" are different results.
Open port Closed port
Scanner Target Scanner Target
| -- SYN ---> | | -- SYN ---> |
| <- SYN-ACK -| open | <-- RST ----| closed
| --- RST --> | (SYN scan | |
| | tears down) | |
Knowledge check. In a SYN scan, why does the scanner send a RST after receiving the SYN-ACK instead of completing the handshake with an ACK? What does that buy the person scanning?
This is a concept lesson, so the "syntax" is really the shape of an address and the socket calls that use these ideas. In C's networking API, you fill in an address structure with a port and an IP, then choose TCP or UDP when you create the socket:
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
/* TCP vs UDP is chosen at socket() time: */
int tcp = socket(AF_INET, SOCK_STREAM, 0); /* SOCK_STREAM = TCP */
int udp = socket(AF_INET, SOCK_DGRAM, 0); /* SOCK_DGRAM = UDP */
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(443); /* port, in network byte order */
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); /* the IP */
The key detail: htons converts the port from your CPU's byte order to network byte order (big-endian). Ports and addresses always travel across the network big-endian, so you must convert — forgetting htons is one of the most common beginner bugs.
The transport layer delivers data between processes. Each process is identified by a port (a number from 0 to 65535).
| TCP | UDP | |
|---|---|---|
| Connection | Yes (3-way handshake) | No |
| Reliable | Yes (acks, retransmit, ordering) | No |
| Overhead | Higher | Lower |
| Used for | Web, SSH, email | DNS, DHCP, VoIP, games |
TCP opens a connection with three messages:
SYN -> SYN-ACK -> ACK
Port scanners take advantage of this:
| Port | Service |
|---|---|
| 21 | FTP |
| 22 | SSH |
| 25 | SMTP (mail) |
| 53 | DNS |
| 80 | HTTP |
| 110 | POP3 |
| 143 | IMAP |
| 443 | HTTPS |
| 445 | SMB |
| 3306 | MySQL |
| 3389 | RDP |
A port scan finds which of these ports are open.
Each open port is a running service, and each service is a possible entry point. Recognizing the port number tells you what is likely listening, even before you grab a banner.
/* tcp_probe.c -- open a TCP connection to host:port on the loopback
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h>
/* Parse a decimal port string into 1..65535, or return -1 on bad input. */ static int parse_port(const char *s) { char *end; long v = strtol(s, &end, 10); if (*s == '\0' || end != '\0') return -1; / not a clean number / if (v < 1 || v > 65535) return -1; / out of port range */ return (int)v; }
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s
int port = parse_port(argv[2]);
if (port < 0) {
fprintf(stderr, "invalid port: %s\n", argv[2]);
return 2;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr); /* zero every field first */
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port); /* host -> network byte order */
/* inet_pton returns 1 on success, 0 for a malformed address. */
if (inet_pton(AF_INET, argv[1], &addr.sin_addr) != 1) {
fprintf(stderr, "invalid IPv4 address: %s\n", argv[1]);
return 2;
}
int fd = socket(AF_INET, SOCK_STREAM, 0); /* SOCK_STREAM = TCP */
if (fd < 0) {
perror("socket");
return 1;
}
/* connect() drives the three-way handshake for us. */
if (connect(fd, (struct sockaddr *)&addr, sizeof addr) == 0) {
printf("%s:%d is OPEN (a service accepted the connection)\n",
argv[1], port);
} else {
/* ECONNREFUSED = a RST came back: closed. Other errors: filtered. */
printf("%s:%d is not open (%s)\n", argv[1], port, strerror(errno));
}
close(fd); /* always release the socket */
return 0;
}
/*
parse_port turns the untrusted argument string into a validated integer. strtol gives us both the value and, through end, where parsing stopped. If end does not point at the string's terminator, there was trailing junk ("80x"), so we reject it. We also enforce the legal port range 1–65535.argc != 3 means the user did not pass exactly an IP and a port; we print usage and exit with code 2.memset(&addr, 0, sizeof addr) clears every byte, including padding, so no stale stack data leaks into the address.sin_family = AF_INET selects IPv4. sin_port = htons(port) stores the port in network byte order. inet_pton converts the dotted-decimal string into the binary sin_addr.socket(AF_INET, SOCK_STREAM, 0) asks the OS for a TCP socket and returns a file descriptor (a small non-negative integer) or -1 on failure.connect. This single call performs the whole three-way handshake. The trace below shows what changes as it runs:| Step | Call | Result / state |
|---|---|---|
Parse "22" |
parse_port |
returns 22 |
| Build address | htons(22), inet_pton |
addr = 127.0.0.1:22 |
| Get socket | socket(...) |
fd = 3 (example) |
| Handshake | connect(...) |
SYN → SYN-ACK → ACK, returns 0 |
| Report | printf |
"is OPEN" |
| Clean up | close(fd) |
descriptor released |
strerror(errno), which distinguishes "Connection refused" (closed) from timeouts and routing errors (filtered).close(fd) returns the socket to the OS. Even a short-lived program should close what it opens, so descriptors are not leaked.Mistake 1: forgetting htons on the port.
addr.sin_port = 443; /* WRONG on little-endian CPUs */
On a typical x86 machine, writing 443 directly stores the bytes in the wrong order, so you actually try to reach port 50433. It is wrong because ports travel across the network big-endian, and your CPU is probably little-endian. Fix: always convert with htons:
addr.sin_port = htons(443); /* correct */
Recognize it when connections mysteriously go to the wrong port or always fail.
Mistake 2: assuming UDP is reliable.
Sending a UDP datagram and never checking for a reply, then wondering why data is "lost." UDP does not retransmit; if you need reliability you must add timeouts and retries yourself, or use TCP. Fix: pick the protocol to match the need — reliability by default means TCP.
Mistake 3: treating standard ports as guarantees.
Assuming port 80 is always HTTP and 22 is always SSH, then getting confused when a scan shows SSH on 2222. Fix: treat the well-known table as a strong hint, and confirm by grabbing a banner or inspecting the actual protocol.
Mistake 4: confusing "closed" with "filtered."
Reading "no response" as "port closed." A closed TCP port usually answers with a RST; silence more often means a firewall dropped the packet (filtered). Fix: inspect the actual error/response — Connection refused (closed) is not the same as a timeout (filtered).
Mistake 5: not validating the port from user input.
int port = atoi(argv[2]); /* accepts "70000", "-1", "abc" silently */
atoi gives no way to detect bad input and silently returns 0 for junk. Fix: use strtol with range checks, as parse_port does.
Compiler errors.
implicit declaration of 'socket' / 'htons' — you forgot an include. Add <sys/socket.h>, <netinet/in.h>, and <arpa/inet.h>.storage size of 'addr' isn't known — you misspelled struct sockaddr_in or omitted <netinet/in.h>.Runtime errors (read errno / perror).
Connection refused — the handshake got a RST: nothing is listening on that port. This is a successful probe of a closed port, not a bug.Permission denied — you tried to bind a well-known port (<1024) without privileges, or your raw-socket scan needs elevation.Address family not supported — you built an IPv6 address but created an AF_INET (IPv4) socket, or vice versa.connect waits for a timeout.Logic errors.
htons.inet_pton returning 0 → the address string is malformed (e.g., 256.0.0.1).Questions to ask when it does not work.
ss -tlnp (Linux) or netstat -an.htons?SOCK_STREAM for TCP, SOCK_DGRAM for UDP?Even a tiny networking program touches several classic C hazards:
struct sockaddr_in has padding bytes. Always memset it to zero before filling fields; otherwise stale stack contents ride along and can cause subtle, non-deterministic bugs.strtol and enforce the 1–65535 range before using it. Passing raw input to atoi or straight into htons invites out-of-range values. This is the exact skill practiced in the related exercise Parse a port number string.uint16_t. Validate the range explicitly; do not rely on the silent truncation that (uint16_t) casting would perform on a too-large value.socket() that succeeds must be paired with close(). In a loop (say, scanning many ports) a leak exhausts the process's descriptor table and later socket() calls start failing with EMFILE.socket, connect, and inet_pton all report failure. Ignoring their return values leads to acting on an invalid descriptor or address — undefined behavior.Defensive / ethical note. Port scanning is a legitimate diagnostic technique, but only against systems you own or are explicitly authorized to test. The examples here target 127.0.0.1 (your own machine) on purpose. Treat validation, least privilege (do not run as root unless a raw-socket scan truly requires it), and staying inside authorized scope as non-negotiable habits.
Where these ideas show up.
ss, netstat, and lsof list which ports each process is listening on; admins use them daily to see what is exposed.nmap map open ports and fingerprint services — the automated version of the tcp_probe example, scaled to whole address ranges and both TCP and UDP.Professional best-practice habits.
| Habit | Beginner focus | Advanced focus |
|---|---|---|
| Validation | Range-check every port and address from input | Fuzz the parser; reject ambiguous input early |
| Error handling | Check every return value; print strerror |
Distinguish closed vs filtered vs timeout paths |
| Resource cleanup | close() each socket |
Bound concurrent descriptors; use timeouts |
| Naming/readability | Name the protocol clearly (tcp_fd) |
Document why a given transport was chosen |
| Scope/ethics | Scan only loopback / your own hosts | Written authorization before any external scan |
Beginner 1 — Protocol chooser. Write a program (or a plain table) that, given a service name, prints whether it typically uses TCP or UDP and its standard port. Cover at least: HTTP, HTTPS, SSH, DNS, DHCP, RDP, MySQL. Requirements: handle an unknown name gracefully. Concepts: well-known ports, TCP vs UDP. Hint: a small array of {name, port, proto} structs makes this clean.
Beginner 2 — Port validator. Implement int parse_port(const char *s) that accepts a decimal string and returns 1–65535 or -1 on any invalid input. Example: "443" -> 443, "0" -> -1, "70000" -> -1, "8a" -> -1. Constraints: no atoi; use strtol and check end. Concepts: input validation, integer range. Hint: reject empty strings and trailing characters explicitly.
Intermediate 1 — Loopback port checker. Extend the lesson's tcp_probe into a function int is_port_open(const char *ip, int port) returning 1 (open), 0 (closed/refused), or -1 (error/filtered). Requirements: clean up the socket on every path; distinguish refused from other errors. Concepts: socket, connect, the handshake, errno. Hint: ECONNREFUSED means closed.
Intermediate 2 — Small range scan. Using your is_port_open, scan ports 1–1024 on 127.0.0.1 and print only the open ones with their likely service name (from your Beginner 1 table). Requirements: do not leak descriptors across the loop; keep output readable. Constraints: loopback only. Concepts: looping over ports, resource cleanup, well-known ports. Hint: close each socket before opening the next.
Challenge — Connect vs SYN, explained. Without writing a raw-socket scanner, write a clear technical note (with a text diagram) that contrasts a full-connect scan and a SYN scan: what packets each sends, what it observes for open/closed/filtered ports, why the SYN scan is quieter, and what privilege it requires. Requirements: correctly distinguish closed (RST) from filtered (no reply). Concepts: three-way handshake, RST, scan trade-offs. Hint: map each outcome to the exact packet that produces it.
The transport layer's job is to hand data to the right program, and it offers two ways to do it. TCP is connection-oriented and reliable: it runs a three-way handshake (SYN → SYN-ACK → ACK), numbers bytes, acknowledges them, and retransmits losses — use it when correctness matters (web, SSH, email, databases). UDP is connectionless and best-effort: no handshake, no retransmission, no ordering — use it when speed and simplicity win (DNS, DHCP, VoIP, games).
Ports are 16-bit numbers (0–65535, which is why 65535 is the ceiling) that pick a program on a host; an IP plus a port names one endpoint. Memorize the common ones: 21 FTP, 22 SSH, 25 SMTP, 53 DNS, 80 HTTP, 110 POP3, 143 IMAP, 443 HTTPS, 445 SMB, 3306 MySQL, 3389 RDP.
Key syntax to remember: choose the transport at socket() time (SOCK_STREAM = TCP, SOCK_DGRAM = UDP), and always convert ports with htons.
Common mistakes: forgetting htons, assuming UDP is reliable, treating standard ports as guarantees, confusing closed (RST) with filtered (silence), and skipping input validation.
What to remember: transport = TCP or UDP; the choice is reliability vs speed; ports address programs; standard ports are hints, not laws; and scanning stays inside systems you are authorized to touch.