We keep climbing the stack. The Internet layer (04-03) moved packets between machines; the transport layer connects processes to each other: Marta's browser with the intranet's web server, Jon's mail client with the SMTP server, each identified by a port. This layer's two tenants — TCP and UDP — you already dissected in 02-04 (handshake, sequence numbers, window, datagrams), so we won't repeat their internal mechanics here. What this lesson adds is the layer seen from the keyboard: how a program uses TCP through sockets (with line-by-line pseudocode), how to read connection states with ss and netstat, what each state means operationally, and what happens when two processes fight over the same port — the famous "address already in use" that will one day bring Meridiano's intranet to a halt. It matters because, for a developer or an administrator, the transport layer is not theory: it is the interface their code touches the network with every day.
Contents
- What lives in the transport layer
- TCP and UDP: two services, a one-table refresher
- Sockets: the layer as the programmer sees it
- A minimal TCP server, line by line
- A minimal TCP client, line by line
- Connection states in practice:
ssandnetstat - Ports in use and conflicts: "address already in use"
- TCP vs UDP as an architecture decision
What lives in the transport layer
TCP/IP's transport layer corresponds to OSI's layer 4 (03-05) and has two main inhabitants:
- TCP (Transmission Control Protocol): a connection-oriented, reliable service — it establishes a session with the three-way handshake, numbers the bytes, acknowledges with ACKs, retransmits what is lost and paces the flow with the window (all covered in 02-04).
- UDP (User Datagram Protocol): a connectionless, no-guarantees service — it sends loose datagrams and washes its hands of them. Fast, lightweight, and honest about what it doesn't promise.
Both share the layer's key contribution: multiplexing by ports. IP delivers the packet to machine 192.168.10.10; the destination port number decides which process on that machine gets the contents. The complete pair identifying a conversation is the socket pair: (source IP, source port, destination IP, destination port) — and in TCP, the protocol as well. That is why the intranet server can serve Marta and Ana at the same time on the same port 443: the two conversations differ in the source IP and port.
TRANSPORT LAYER at 192.168.10.10
+---------------------------------------------------+
IP | port 443 -> nginx process (intranet HTTPS) |
packet->| port 22 -> sshd process (administration) |
| port 3306 -> mysqld process (database) |
+---------------------------------------------------+
one machine, one IP, many processes: ports are
the "floor number" inside the buildingTCP and UDP: two services, a one-table refresher
Without repeating the mechanics of 02-04, let's fix the contrast as the service contract the layer offers applications:
| Property | TCP | UDP |
|---|---|---|
| Prior connection | Yes (3-step handshake) | No: just send |
| Reliability | Guaranteed (ACK + retransmission) | None: what's lost stays lost |
| Arrival order | Guaranteed (sequence numbers) | Not guaranteed |
| Unit the app sees | Continuous byte stream | Datagrams with boundaries |
| Flow/congestion control | Yes (window) | No |
| Startup cost | 1 extra RTT for the handshake | Zero |
| Header | 20+ bytes | 8 bytes |
| Segment/PDU | TCP segment | UDP datagram |
One nuance that stayed implicit in 02-04 and now, through a programmer's eyes, matters a great deal: TCP delivers a stream, not messages. If the client does two send() calls of 100 bytes each, the server may receive them in a single recv() of 200, or in three chunks of 80+80+40. The boundaries are set by the application (for example, HTTP with its Content-Length). In UDP, by contrast, each datagram arrives whole or not at all: one send = one recv.
Sockets: the layer as the programmer sees it
A socket is the object the operating system offers programs so they can use the transport layer. Remember from 04-01 that the TCP/IP stack lives inside the kernel: your program builds no segments and computes no ACKs; it asks the OS "give me a TCP socket", writes bytes into it, and the kernel does everything else (segmenting, numbering, retransmitting, and handing it down to the Internet layer).
The sockets API (born in 1980s Unix BSD and copied ever since into C, Python, Java, Go, JavaScript...) revolves around a handful of operations:
| Operation | Who uses it | What it does |
|---|---|---|
socket() |
both | Create the socket (choose TCP or UDP) |
bind() |
server | Reserve a local IP and a port |
listen() |
server | Declare itself ready to accept connections |
accept() |
server | Wait for and accept an incoming connection |
connect() |
client | Start the handshake toward a remote IP:port |
send() / recv() |
both | Write/read bytes from the stream |
close() |
both | End the connection (triggers TCP's teardown) |
The asymmetry mirrors the real world: the server waits on a well-known port; the client calls. Let's see it in pseudocode.
A minimal TCP server, line by line
This is essentially how Meridiano's intranet server starts (the real thing is nginx + an API, but the socket skeleton is this):
1 s = socket(TCP) # ask the kernel for a TCP socket
2 bind(s, 192.168.10.10, 8080) # reserve the local IP and port 8080
3 listen(s) # "I'm open for business"
4 infinite loop:
5 c = accept(s) # BLOCKS until a connection arrives
6 request = recv(c) # read bytes from the stream (e.g. an HTTP request)
7 send(c, response) # write the response into the stream
8 close(c) # close THIS conversation (not the listening socket)Line by line, with what happens underneath:
- Line 1 —
socket(TCP): the kernel creates the internal structure and returns an identifier (on Unix, a file descriptor: to the program, a socket is read and written almost like a file). - Line 2 —
bind(...): reserves port 8080 on IP192.168.10.10. This is the only point where the "address already in use" error can pop up (we'll see it in section 7). Many servers bind to0.0.0.0("all my IPs") instead of a specific one. - Line 3 —
listen(s): from here on the kernel completes handshakes on its own. When Marta's SYN arrives (02-04), the kernel replies SYN-ACK and queues the completed connection, without the program doing anything. The socket enters theLISTENstate. - Line 5 —
accept(s): pulls an already-established connection from that queue and returns a new socket (c) exclusive to that conversation. Crucial detail: the listening socketsstays alive and keeps accepting; that is how the server serves many clients on a single port. If no connections are pending,acceptblocks (the process sleeps until someone shows up). - Lines 6-7 —
recv/send: reading and writing the stream. Remember: byte stream, not messages;recvcan return "half a request" and the application must know how to reassemble it. - Line 8 —
close(c): starts TCP's orderly teardown (the FIN/ACK exchange, seen conceptually in 02-04). This close is the origin of theTIME_WAITstate you'll see shortly inss.
Real servers add concurrency (a thread or task per accepted connection) and read loops, but the skeleton — socket → bind → listen → accept → recv/send → close — is identical in any language.
A minimal TCP client, line by line
And this is what Marta's side looks like when her browser (or a script of hers) requests something from the intranet:
1 c = socket(TCP) # TCP socket, same as on the server
2 connect(c, 192.168.10.10, 8080) # fires the three-way handshake
3 send(c, "GET /api/proyectos ...") # write the request into the stream
4 response = recv(c) # read the response
5 close(c) # orderly teardown- Line 1 — same as on the server: the socket is the same piece on both sides.
- Line 2 —
connect(...): here the full handshake from 02-04 happens (SYN → SYN-ACK → ACK). Whenconnectreturns without an error, the connection isESTABLISHEDat both ends. Notice what the client did not do: nobind. The kernel assigns it an ephemeral source port on its own (typically between 32768 and 60999 on Linux) — which is why in captures clients always speak "from" high, odd-looking ports. - Lines 3-5 — symmetrical to the server: write, read, close.
A failure in connect is pure diagnosis, and you already know it from 03-05: immediate rejection (RST) = the machine is alive but nobody is listening on that port; timeout = the machine or the network is not responding. The transport layer talks to you; you have to know how to listen.
Connection states in practice: ss and netstat
Every TCP connection lives in a state machine (the handshake and the teardown are transitions within it). No need to memorize it all: day to day, three states explain 95% of what you'll see. The modern tool on Linux is ss (successor to netstat, which still exists and is what you have on Windows).
On the intranet server, on any given Tuesday:
$ ss -tn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 192.168.10.10:443 192.168.10.21:52814
ESTAB 0 0 192.168.10.10:443 192.168.10.35:49102
TIME-WAIT 0 0 192.168.10.10:443 192.168.20.7:51230
$ ss -tln
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*Option breakdown: -t = TCP only, -n = raw numbers (doesn't translate 443 to "https" or IPs to names — faster and unambiguous), -l = listening sockets only. On Windows, netstat -ano gives the equivalent view (the -o adds the process PID).
The three key states, read operationally:
| State | What it means | Operational reading |
|---|---|---|
LISTEN |
Server socket waiting for connections (after listen()) |
"The service is up and accepting". If you expected a LISTEN on 443 and it's not there, the problem is the process, not the network |
ESTABLISHED |
Handshake completed; stream open in both directions | A live conversation. The first line above is Marta (.10.21) browsing the intranet right now |
TIME_WAIT |
The connection has already closed; the end that closed first holds the socket pair for ~60 s | Normal and healthy: it prevents straggler segments from the old connection sneaking into a new one with the same socket pair. Seeing lots of TIME_WAIT on a busy server is routine, not a leak |
The example's third line tells a complete story: Jon (192.168.20.7, Bilbao) consulted the intranet over the VPN, finished, and his connection is in TIME_WAIT serving its minute of quarantine before disappearing.
Other states you'll see in passing: SYN-SENT (client waiting for the SYN-ACK; if these pile up, the destination isn't responding), CLOSE-WAIT (the other side closed and your application hasn't called close() yet — if these accumulate for hours, suspect an application bug: unclosed sockets).
Ports in use and conflicts: "address already in use"
The classic incident at Meridiano: Ana deploys a new version of the intranet API and the startup dies with:
Exact translation: the server's line-2 bind() asked for a port that another socket already has reserved. The two usual causes, from most to least frequent:
-
The old process is still alive. The deployment didn't kill the previous version, which keeps its
LISTENon the port. One-line diagnosis:$ ss -tlnp | grep :8080 LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=1147,fd=3))The
-poption (requires root) gives away the occupant: processpython3, PID 1147. Solution: stop that process cleanly (the service manager —systemctl restart— exists precisely so you never end up here). On Windows:netstat -ano | findstr :8080and look up the PID in Task Manager. -
Residual connections from the previous process (
TIME_WAITwith that port as the local source, in certain fast-restart scenarios). That is why well-written servers set theSO_REUSEADDRsocket option beforebind: it tells the kernel "let me reuse the port even if leftovers remain inTIME_WAIT". It is the first line a developer adds to a server after suffering this once.
Operational rule: "address already in use" is never a network problem — it is a local, machine-level problem between processes. The network never even noticed.
And its first cousin on the client side: if a client opens and closes thousands of connections per second toward the same destination, it can exhaust the ephemeral ports (all stuck in TIME_WAIT). Rare in a small business like Meridiano, but it explains why intensive applications reuse connections (connection pooling, HTTP keep-alive) instead of opening one per request.
TCP vs UDP as an architecture decision
Choosing a transport is not a preference: it is a design decision made by answering two questions — can I afford to lose data? and can I afford to wait? Let's review Meridiano's real services:
| Meridiano service | Transport | Why |
|---|---|---|
Intranet HTTPS (GET /api/proyectos) |
TCP (443) | A JSON response with one lost byte is garbage: reliability is mandatory |
Shared files (SMB, .10.10 server) |
TCP (445) | A corrupted document is unacceptable; they are also long transfers: TCP's window paces them |
| Email (SMTP/IMAP) | TCP (587/993) | Losing half an email is not an option |
| SSH to the router and the server | TCP (22) | Every keystroke must arrive, and in order |
| DNS (resolution queries) | UDP (53) | Question and answer fit in one datagram; if it's lost, the client asks again after ~2 s. TCP's handshake would cost more than the entire query (TCP stays in reserve for large responses) |
| DHCP (02-05's DORA) | UDP (67/68) | The client has no IP yet: keeping a connection is impossible; it's 4 broadcast datagrams |
| Valencia–Bilbao video calls | UDP (RTP) | A frame that arrives late is useless: better to lose it than stop the video waiting for a retransmission. The app compensates in its own way (adjusts quality) |
| Router monitoring (SNMP) | UDP (161) | Small, frequent probes; a lost one is repeated in the next cycle |
The pattern that emerges is as reliable as a law: data that must arrive intact → TCP; real time, or tiny exchanges where retrying is cheaper than guaranteeing → UDP. And a modern third way worth knowing by name: QUIC, HTTP/3's transport, which runs over UDP but reimplements reliability, ordering and encryption inside the application itself — proof that when UDP "offers no guarantees", it is actually offering the freedom to build your own.
Common Mistakes and Tips
- Believing TCP delivers "messages". It delivers a byte stream: two
send()calls may arrive as a singlerecv()or as several chunks. Message boundaries are defined by the application protocol. This misunderstanding is the number one cause of networking bugs in beginners' code. - Panicking over
TIME_WAITs. They are TCP's teardown working correctly, not hung connections or leaks. Worry instead about accumulatingCLOSE-WAIT: those do indicate your application isn't closing sockets. - Looking to the network for an "address already in use". It is always a local conflict between processes over a port.
ss -tlnp(ornetstat -ano) identifies the occupant in seconds. - Confusing the listening socket with the connections. The
LISTENon port 443 is one; every accepted client has its ownESTABLISHEDsocket. Closing one connection doesn't affect theLISTEN, and killing theLISTENdoesn't cut established connections. - Choosing UDP "because it's faster" with no plan B. UDP is not TCP-minus-baggage: it is a different contract. If you choose UDP, whatever reliability you need, you program yourself (or you accept the losses, as video does).
- Tip: memorize the trio
ss -tln(what is listening?),ss -tn(what is talking?),ss -tlnp(which process is it?). Those three commands settle most "is the service up?" questions before touching anything else.
Exercises
-
Ana runs
ss -tnon the intranet server and sees the lineSYN-SENT 0 1 192.168.10.10:41230 192.168.20.7:443repeated for 30 seconds, until it disappears with an error. (a) Who is acting as client and who as server in this attempt? (b) What does it mean that the state stays stuck inSYN-SENT? (c) Give two compatible hypotheses, using what you learned in 03-05. -
In the server pseudocode from section 4, explain exactly what would happen (and what Marta would see from her browser) if: (a) line 3 (
listen) is removed; (b) the program gets stuck processing a slow request at line 6 while three new clients arrive; (c) line 8 (close) is removed and the server handles hundreds of requests. -
Meridiano is going to set up two new services: (a) a nightly backup system that uploads the Valencia server's files to storage in Bilbao over the VPN; (b) a panel at reception showing the server room's temperature, updated every 2 seconds from a sensor. Choose a transport for each and justify it with the two questions from section 8.
Solutions
-
(a) The intranet server is acting here as a client: the source port is ephemeral (41230) and the destination is port 443 on Jon's PC or some Bilbao machine (
192.168.20.7) — probably a health check or an integration. Client/server roles are per connection, not per machine. (b) A stuckSYN-SENTmeans the SYN was sent and the SYN-ACK never arrived: the handshake won't start; the kernel retries the SYN several times and eventually gives up (that's the error at ~30 s). (c) Hypotheses compatible with a timeout (03-05): machine.20.7is off or unreachable (VPN down? — checkable withping), or a firewall silently drops the SYNs toward that port. If the port were simply closed with the machine alive, an RST would have come back and the error would be immediate ("connection refused"), not a timeout. -
(a) Without
listen, the socket never enters theLISTENstate and the kernel answers incoming SYNs with RST: Marta would see an immediate "connection refused". (Depending on the language, theacceptat line 5 would also fail when executed.) (b) Nothing serious in the short term: the kernel completes the handshakes on its own and queues the three established connections (within the backlog limit); the three clients see their connection open but unanswered until the server finishes and returns toaccept. They perceive slowness, not rejection — this is what a saturated single-threaded server looks like. If the queue overflows, subsequent SYNs do get dropped. (c) Every served connection stays open forever: theESTABLISHEDs (and, on the client side, hung waits) pile up, and with them the process's file descriptors, until the OS limit is exhausted andacceptstarts failing. It is the classic socket leak: diagnosed by seeing hundreds of stale connections inss -tntied to the process. -
(a) Backup: TCP. Can I lose data? No — a corrupted backup is worse than no backup. Can I wait? Yes — it's nightly, latency is irrelevant; and TCP's window will also make good use of the VPN tunnel by pacing the transfer. (b) Temperature panel: UDP is reasonable. Can I lose data? Yes — if the 10:00:02 reading is lost, the 10:00:04 one replaces it; stale data expires on its own. Can I wait? Retransmitting an obsolete reading makes no sense. A small datagram every 2 s with no handshake is the natural fit. (TCP wouldn't be wrong here either — with so little traffic you'd barely notice — but UDP expresses the design better: last reading wins.)
Conclusion
The transport layer is where the network stops being plumbing and becomes a programming interface: TCP and UDP are two service contracts — reliable stream versus no-promises datagrams — and the socket is the door any program uses them through, with the socket → bind → listen → accept ritual on the server and socket → connect on the client, while the kernel does 02-04's dirty work underneath. You can now read the state of that machinery (ss -tln for what listens, ss -tn for what talks), interpret LISTEN, ESTABLISHED and TIME_WAIT operationally, defuse an "address already in use" in two commands, and choose a transport for what it really is: an architecture decision answered with two questions. One last rung of the stack remains: the application layer, home to the protocols Marta and Jon use without knowing it — and where we will see, step by step, what happens from the moment the intranet's code requests GET /api/proyectos until those bytes enter the socket you have just learned to open.
Networking Course
Module 1: Introduction to Networks
Module 2: Communication Protocols
- Introduction to Communication Protocols
- Data Link Protocols
- Network Protocols
- Transport Protocols
- Application Protocols
Module 3: The OSI Model
- Introduction to the OSI Model
- Physical Layer
- Data Link Layer
- Network Layer
- Transport Layer
- Session Layer
- Presentation Layer
- Application Layer
Module 4: The TCP/IP Model
- Introduction to the TCP/IP Model
- Network Access Layer
- Internet Layer
- Transport Layer
- Application Layer
- OSI vs TCP/IP Comparison
