Networking Fundamentals · beginner · ~12 min

TCP vs UDP, ports, and the ones to know

- 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

Overview

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.

Why it matters

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:

  • Whether a service speaks TCP or UDP decides which kind of scan even works against it.
  • Recognizing a port number instantly tells you the likely service behind it, so you know what to investigate next — before you have read a single banner.
  • Understanding the handshake explains why a SYN scan is quieter than a full connect scan.

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.

Core concepts

1. TCP: connection-oriented and reliable

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.

2. UDP: connectionless and best-effort

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?

3. Ports: addressing a program, not a machine

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.

4. The three-way handshake (and how scans use it)

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?

Syntax notes

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.

Lesson

The transport layer delivers data between processes. Each process is identified by a port (a number from 0 to 65535).

TCP vs UDP

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

The three-way handshake

TCP opens a connection with three messages:

SYN -> SYN-ACK -> ACK

Port scanners take advantage of this:

  • A full connect scan lets the handshake complete normally.
  • A SYN scan sends only the first SYN, reads the reply, and never finishes the handshake.

Ports you must recognize

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

Why it matters

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.

Code examples

/* tcp_probe.c -- open a TCP connection to host:port on the loopback

  • interface and report whether something is listening.
  • This is the essence of a "full connect" port check: if connect()
  • succeeds, the three-way handshake completed and a service is there.
  • Build: cc -Wall -Wextra -o tcp_probe tcp_probe.c
  • Run: ./tcp_probe 127.0.0.1 22 */

#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 \n", argv[0]); return 2; }

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;

}

/*

  • WHAT IT DOES
  • The program takes an IPv4 address and a port on the command line,
  • validates both, then tries to open a TCP connection. If connect()
  • succeeds, the handshake completed and a service is listening; if it
  • fails with ECONNREFUSED the port is closed; other errors usually mean
  • filtered (a firewall dropped the packet).
  • EXPECTED OUTPUT (machine with an SSH server running):
  • $ ./tcp_probe 127.0.0.1 22
  • 127.0.0.1:22 is OPEN (a service accepted the connection)
  • EXPECTED OUTPUT (nothing listening):
  • $ ./tcp_probe 127.0.0.1 9999
  • 127.0.0.1:9999 is not open (Connection refused)
  • EDGE CASES
  • A bad port (0, 70000, abc) is rejected before any network call.
  • A malformed IP is caught by inet_pton. A filtered port gives a
  • different error string (often a timeout or "No route to host") than a
  • refused one -- the code prints strerror(errno) so you can tell them
  • apart. This example only checks loopback / hosts you own; scanning
  • machines you do not control is out of scope. */

Line by line

  1. 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.
  2. Argument check. argc != 3 means the user did not pass exactly an IP and a port; we print usage and exit with code 2.
  3. Zeroing the struct. memset(&addr, 0, sizeof addr) clears every byte, including padding, so no stale stack data leaks into the address.
  4. Filling 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.
  5. Creating the socket. 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.
  6. 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
  1. Reporting. On success we print OPEN; on failure we print the reason from strerror(errno), which distinguishes "Connection refused" (closed) from timeouts and routing errors (filtered).
  2. close(fd) returns the socket to the OS. Even a short-lived program should close what it opens, so descriptors are not leaked.

Common mistakes

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.

Debugging tips

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.
  • A long hang before failure usually means a filtered port: packets are being dropped and connect waits for a timeout.

Logic errors.

  • Connecting to the wrong port every time → missing htons.
  • inet_pton returning 0 → the address string is malformed (e.g., 256.0.0.1).

Questions to ask when it does not work.

  1. Is anything actually listening there? Check with ss -tlnp (Linux) or netstat -an.
  2. Did I convert the port with htons?
  3. Am I using the right protocol — SOCK_STREAM for TCP, SOCK_DGRAM for UDP?
  4. Is a firewall between me and the target dropping the packets?

Memory safety

Even a tiny networking program touches several classic C hazards:

  • Uninitialized address struct. 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.
  • Untrusted input. The port comes from the command line — untrusted data. Parse it with 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.
  • Integer range. A port must fit in a uint16_t. Validate the range explicitly; do not rely on the silent truncation that (uint16_t) casting would perform on a too-large value.
  • File-descriptor leaks. Every 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.
  • Return-value checks. 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.

Real-world uses

Where these ideas show up.

  • Web and APIs. Your browser opens a TCP connection to port 443, completes the handshake, then TLS and HTTP ride on top — the chain continues in the next lesson, HTTP, HTTPS, and TLS.
  • Operating systems and servers. ss, netstat, and lsof list which ports each process is listening on; admins use them daily to see what is exposed.
  • DNS and DHCP. These sit on UDP (port 53 and 67/68) precisely because a small, fast, retryable exchange fits UDP's model.
  • Security assessment. Tools like 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.
  • Firewalls. Rules are written in terms of protocol + port ("allow TCP 22 from this subnet"), so understanding transports and ports is a prerequisite to configuring them.

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

Practice tasks

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.

Summary

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.

Practice with these exercises