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

  1. What lives in the transport layer
  2. TCP and UDP: two services, a one-table refresher
  3. Sockets: the layer as the programmer sees it
  4. A minimal TCP server, line by line
  5. A minimal TCP client, line by line
  6. Connection states in practice: ss and netstat
  7. Ports in use and conflicts: "address already in use"
  8. 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 building

TCP 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 1socket(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 2bind(...): reserves port 8080 on IP 192.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 to 0.0.0.0 ("all my IPs") instead of a specific one.
  • Line 3listen(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 the LISTEN state.
  • Line 5accept(s): pulls an already-established connection from that queue and returns a new socket (c) exclusive to that conversation. Crucial detail: the listening socket s stays alive and keeps accepting; that is how the server serves many clients on a single port. If no connections are pending, accept blocks (the process sleeps until someone shows up).
  • Lines 6-7recv/send: reading and writing the stream. Remember: byte stream, not messages; recv can return "half a request" and the application must know how to reassemble it.
  • Line 8close(c): starts TCP's orderly teardown (the FIN/ACK exchange, seen conceptually in 02-04). This close is the origin of the TIME_WAIT state you'll see shortly in ss.

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 2connect(...): here the full handshake from 02-04 happens (SYN → SYN-ACK → ACK). When connect returns without an error, the connection is ESTABLISHED at both ends. Notice what the client did not do: no bind. 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:

Error: bind failed - Address already in use (EADDRINUSE)

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:

  1. The old process is still alive. The deployment didn't kill the previous version, which keeps its LISTEN on 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 -p option (requires root) gives away the occupant: process python3, 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 :8080 and look up the PID in Task Manager.

  2. Residual connections from the previous process (TIME_WAIT with that port as the local source, in certain fast-restart scenarios). That is why well-written servers set the SO_REUSEADDR socket option before bind: it tells the kernel "let me reuse the port even if leftovers remain in TIME_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 single recv() 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 accumulating CLOSE-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 (or netstat -ano) identifies the occupant in seconds.
  • Confusing the listening socket with the connections. The LISTEN on port 443 is one; every accepted client has its own ESTABLISHED socket. Closing one connection doesn't affect the LISTEN, and killing the LISTEN doesn'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

  1. Ana runs ss -tn on the intranet server and sees the line SYN-SENT 0 1 192.168.10.10:41230 192.168.20.7:443 repeated 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 in SYN-SENT? (c) Give two compatible hypotheses, using what you learned in 03-05.

  2. 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.

  3. 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

  1. (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 stuck SYN-SENT means 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.7 is off or unreachable (VPN down? — checkable with ping), 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.

  2. (a) Without listen, the socket never enters the LISTEN state and the kernel answers incoming SYNs with RST: Marta would see an immediate "connection refused". (Depending on the language, the accept at 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 to accept. 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: the ESTABLISHEDs (and, on the client side, hung waits) pile up, and with them the process's file descriptors, until the OS limit is exhausted and accept starts failing. It is the classic socket leak: diagnosed by seeing hundreds of stale connections in ss -tn tied to the process.

  3. (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.

© Copyright 2026. All rights reserved