Networks and the web
How HTTP Works: Requests, Responses, Headers and Status Codes
Read a real HTTP exchange line by line: methods, URLs, headers, status code classes and cookies, tested with curl against a local server.
A port is a numbered door on your machine. Learn how TCP and UDP use ports, then start, find and test a local server on 127.0.0.1.
A port is a number that tells the operating system which program should receive a piece of network traffic. Your machine has an IP address, and it can run dozens of network programs at the same time: a browser that fetches pages, a mail client, a database, a local web server. The IP address gets traffic to the right computer. The port gets it to the right program.
If you have read what DNS does, you already know how a name becomes an address. This article explains where ports come from, how the two main transport protocols use them, and how to see which ports your own machine is listening on. The lab runs on 127.0.0.1, the loopback address that always means "this machine," so nothing leaves your computer.
A port is a 16-bit number, which means it runs from 0 to 65535. TCP and UDP headers both carry a source port and a destination port. When a packet reaches your computer, the operating system reads the destination port and hands the data to the program that asked to receive on that number.
Think of an apartment building. The street address gets the delivery to the building. The apartment number tells the carrier which door to knock on. In that picture the IP address is the building and the port is the apartment. Someone decides which apartment is which, and a tenant can move out and let someone else take the number.
A listening port is one where a program has told the operating system, in effect, "send me connections for this number." A program in that state is listening. When the program exits, the operating system releases the port, and nothing answers there anymore.
Every service you run opens at least one port. That includes programs you did not start on purpose: updaters, sync clients, development servers, plugins, and software that installs itself as a background service. The list of listening ports is the simplest accurate picture of what your machine offers to the network.
If you ever review a host you own, that list is the first thing to write down. Before you ask whether a service is patched or configured well, you need to know that it exists. Security work starts with inventory, and the inventory starts with the doors.
TCP/IP is the protocol suite the internet runs on. It is organized in layers, and you do not need all of them yet. Four layers matter here:
Each layer wraps the data from the layer above in its own header. The IP header carries the source and destination addresses. The TCP or UDP header carries the source and destination ports. The application data sits inside. When the packet arrives, each layer removes its header and passes the rest upward.
The port lives in the transport layer. IP does not know which program should get the data. That job belongs to the transport layer, and it is why one IP address can serve a web page on port 443 and a database on port 5432 at the same time.
TCP, specified in RFC 9293, gives a program a reliable, ordered stream of bytes. Before any application data moves, the two ends agree on starting sequence numbers and confirm that each side can send and receive. That agreement is the three-way handshake.
When no program listens on the port, the target machine does not send SYN-ACK. It sends a RST (reset) segment, and the client gives up at once with a message such as "connection refused." That fast answer is useful. It tells you the host received your packet and that nothing accepts connections on that port.
A silent wait ending in a timeout means something different. Very often a firewall dropped the packets without replying. So "refused" and "timed out" are two different clues. Learn to read them.
Connections on 127.0.0.1 use the same TCP logic even though the packets never reach a network card. The operating system runs the handshake in software, which makes the loopback address a safe place to practice.
UDP, specified in RFC 768, is much simpler. Its header has 8 bytes: source port, destination port, length and checksum. There is no handshake, no acknowledgement, no retransmission and no ordering. The sender transmits a datagram, and the network delivers it or it does not. The receiver confirms nothing unless the application builds that logic itself.
That simplicity has uses. DNS queries usually travel over UDP port 53, because a short question and a short answer fit in one exchange, and a handshake would add round trips. Real-time voice and video often use UDP, and so does QUIC, the transport under HTTP/3. Each of these protocols brings its own rules for loss and retries.
Ports work the same way for UDP. The destination port picks the program. The difference is that a UDP port has no connection state. Any program bound to that port receives whatever arrives, from whoever sends it. A TCP listener only sees connections that complete the handshake. Both still answer the same question: which program gets this packet.
The Internet Assigned Numbers Authority (IANA) maintains the Service Name and Transport Protocol Port Number Registry. The registry records which service is commonly associated with each port number for TCP, UDP and a few other transport protocols. Well-known entries include 22 for ssh, 53 for domain (DNS), 80 for http and 443 for https.
RFC 6335 divides the number space into three ranges:
The registry records assignments and conventions. Nothing in TCP or UDP enforces them. Any program with permission can bind almost any free port. The registry tells you what usually runs on a number, and it can be wrong about your machine. Look at what is actually listening before you draw a conclusion.
You need a terminal and Python 3. The lab uses the HTTP server built into Python, so you install nothing extra.
I ran the Windows commands below on Windows 11 Pro with Git Bash, Python 3.12.10 and curl 8.12.1. I did not run the Linux and macOS commands on this machine, so their output is not shown. Where an output block appears, it came from the command shown above it.
One note on Python: in my Git Bash session python3 was not found, while python ran Python 3.12. If python3 fails for you on Windows, use python. The rest of the command stays the same.
The server publishes whatever sits in the folder where you start it, so put something small there first.
mkdir -p ~/port-lab && cd ~/port-lab
printf '<!doctype html><title>lab</title><h1>Port 8000 lab</h1>\n' > index.htmlOn PowerShell, create the same folder and write those two lines into index.html with any editor.
Keep this terminal open. The server runs until you stop it.
cd ~/port-lab
python3 -m http.server 8000 --bind 127.0.0.1The flag --bind 127.0.0.1 tells Python to listen only on the loopback interface. Without it, Python listens on every interface of the machine. That difference matters later when you read the listening list.
Pick the command for your operating system. Each one lists the sockets in the LISTEN state that use port 8000.
Linux:
ss -tlnp | grep 8000macOS:
lsof -iTCP -sTCP:LISTEN -n -P | grep 8000Windows PowerShell:
Get-NetTCPConnection -State Listen -LocalPort 8000This is the output I got from the Windows command on my machine, piped through Format-Table:
LocalAddress LocalPort State OwningProcess
------------ --------- ----- -------------
127.0.0.1 8000 Listen 45608The process ID will differ on your machine. Run Get-Process -Id <OwningProcess> with your own number to confirm that the owner is the Python process you started. In my run it was python.
What to look for:
LocalAddress is 127.0.0.1. The socket accepts connections only from this machine. If you had left out --bind, you would expect to see 0.0.0.0 here instead. I did not test that variant on this machine, so treat that as the expected result, not an observed one.State is Listen. The socket is waiting for connections.OwningProcess points to the program you expect. If it points to something you do not recognize, stop and find out what it is before you go on.Open a second terminal and run:
curl -v http://127.0.0.1:8000/This is the output from my run, trimmed to the lines that matter:
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
< HTTP/1.0 200 OK
< Server: SimpleHTTP/0.6 Python/3.12.10
< Content-type: text/html
<!doctype html><title>lab</title><h1>Port 8000 lab</h1>Read it in order. Trying means curl is opening a TCP connection. The address is already an IP, so no DNS lookup happens. Connected means the three-way handshake finished. GET / HTTP/1.1 is the application layer: curl now speaks HTTP over that connection. 200 OK means the server found the file and answered. The HTML line at the bottom is your file.
The server writes a line about the request to its own terminal. Mine showed:
127.0.0.1 - - [08/Nov/2026 06:07:56] "GET / HTTP/1.1" 200 -The source address is the same loopback address, so the request came from this machine.
Go back to the server terminal and press Ctrl+C. That is the clean way to stop it. On my machine I stopped the process with Stop-Process -Force, which ends it without giving it a chance to shut down cleanly. Either way, the listening socket goes away once the process ends.
Confirm the socket is gone:
Get-NetTCPConnection -State Listen -LocalPort 8000My output was empty. Then run the same curl command again:
curl -v http://127.0.0.1:8000/This is the output from my run:
* Failed to connect to 127.0.0.1 port 8000 after 2048 ms: Could not connect to server
curl: (7) Failed to connect to 127.0.0.1 port 8000 after 2048 ms: Could not connect to serverThe exit code of curl was 7, the code that means it could not connect.
Here is what happened. Your SYN reached 127.0.0.1 on port 8000. Nothing was listening there, so the operating system answered with a reset. curl reported the refusal immediately, without waiting for a timeout. If you see a long wait followed by a timeout instead, suspect a firewall or a route that drops packets.
The same address and port behaved in two ways. With a listener, the handshake completed and the server answered. Without one, the refusal came back in milliseconds. The listening list is what separates the two cases. When a connection surprises you, check the list first.
Binding to every interface by accident. Many quick servers listen on all addresses by default. On a laptop connected to a café network, that can expose a development server to other devices on the same network. Use an explicit bind address when you only need local access.
Treating a port number as proof of identity. Port 80 usually means HTTP, but nothing forces a program to speak HTTP on it. Connect and read what the service sends, or check which process owns the socket.
Forgetting UDP. ss -tlnp shows TCP listeners only. Use ss -ulnp for UDP, or Get-NetUDPEndpoint on Windows. A machine can have a busy UDP service that never appears in your TCP check.
Reading a timeout as "no service." A timeout often means a filter sits between you and the port. A refusal means the host answered and nothing listens. They call for different next steps.
Killing the wrong process. Confirm the owning process name and its command line before you stop it. A leftover server from an earlier attempt can still hold the port and answer with files from a folder you did not mean to publish. Check the owner before you assume the port is yours.
Listening ports describe what your machine offers to its network, whether you meant to offer it or not. Before you test anything else on a host you own, list what listens, match each entry to a program you know, and close what you do not need. A development server you forgot to stop is a good example of an entry that looks harmless and still answers.
The same commands can be used on machines you do not control, but scanning or probing a system you do not own requires written permission from its owner. Keep your practice on your own machines and your own lab.
Repeat the exercise with UDP. A short Python listener on 127.0.0.1 port 5399 received a datagram from a sender in my run. The listener did nothing special:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 5399))
data, addr = s.recvfrom(1024)
print("got", data, "from", addr)Run it in one terminal, then send a datagram from another with socket.sendto. The output I got was got b'hello' from ('127.0.0.1', 63411). Notice that no handshake happened. The packet simply arrived. Port 5353 was already in use on my machine, and binding it failed with a permission error, so I picked 5399 for the test.
Next, read the connection establishment part of RFC 9293 and compare it with the packet flow above. Then move on to how HTTP works, which covers how HTTP runs on top of TCP and what a request and a response contain.
RFC 9293, Transmission Control Protocol (TCP), IETF, August 2022. https://www.rfc-editor.org/rfc/rfc9293
RFC 768, User Datagram Protocol, IETF, August 1980. https://www.rfc-editor.org/rfc/rfc768
Service Name and Transport Protocol Port Number Registry, IANA. https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
RFC 6335, Internet Assigned Numbers Authority (IANA) Procedures for the Management of the Service Name and Transport Protocol Port Number Registry, IETF, August 2011. https://www.rfc-editor.org/rfc/rfc6335
Networks and the web
Read a real HTTP exchange line by line: methods, URLs, headers, status code classes and cookies, tested with curl against a local server.
Networks and the web
TLS 1.3 gives HTTPS confidentiality, integrity and server authentication. Learn the handshake, certificate chains and what the padlock does not prove.
Networks and the web
How DNS turns names into addresses: resolvers, authoritative servers, TTL caching, the A, AAAA, CNAME, MX and TXT records, and a safe lookup.