Transport-Layer Services and Multiplexing
Imagine a single laptop running a video call, a background file sync, three browser tabs, and an email client, all at once. Every one of these processes sends and receives packets over the same network interface. Each is identified by the same IP address. Somehow, when a packet arrives, the operating system must hand it to the exact process that asked for it. Not the video call app when the browser is waiting. Not the email client when the file sync is waiting. This is the process-to-process delivery problem. It sits squarely at the boundary between the network layer and the applications we actually use. The network layer, through IP forwarding and protocols like BGP-driven inter-domain routing, only promises to move a packet from one host to another. It has no concept of "processes," "applications," or "connections." Its unit of delivery is the host, addressed by an IP address. The transport layer exists to extend that host-to-host delivery into genuine process-to-process delivery. It also layers additional services on top of what the network guarantees. These include reliability, ordering, congestion control, or none of these. This extension is achieved through a beautifully simple mechanism. It relies on port numbers and the multiplexing/demultiplexing logic that uses them. This article is foundational. It explains why a transport layer must exist at all. It shows how multiplexing and demultiplexing work using ports and socket identifiers. It also describes what menu of services a transport protocol can offer. This ranges from bare-bones datagram delivery to fully reliable, ordered, congestion-controlled byte streams. It sets up the two concrete protocols explored elsewhere. These are the connectionless minimalism of UDP: Connectionless Transport and the engineering of reliability explained in Principles of Reliable Data Transfer. Together, they give you the vocabulary and mental model both protocols share. By the end, you should be able to precisely state what "demultiplexing" means. You should be able to trace how an arriving segment gets routed to the correct socket. You should be able to explain why UDP and TCP use different socket-identification rules. Finally, you should be able to reason about what services must live at the transport layer. You should also see what the network layer could, in principle, provide instead.
- The transport layer converts the network layer's host-to-host delivery into true process-to-process delivery.
- Port numbers (16-bit, 0–65535) identify processes; a socket is the OS endpoint bound to a port.
- Multiplexing happens at the sender: many sockets' data is gathered into segments handed to IP.
- Demultiplexing happens at the receiver: incoming segments are routed to the correct socket using header fields.
- UDP demultiplexes on destination port alone; TCP demultiplexes on the full 4-tuple (source/dest IP and port).
- Transport protocols offer a spectrum of services — reliability, ordering, timing, congestion control — that the application layer selects by choosing UDP, TCP, or another transport protocol.
Ports plus a well-defined multiplexing/demultiplexing algorithm let one physical network connection carry independent, correctly-routed conversations for arbitrarily many processes at once.
The idea
The transport layer provides logical communication between processes running on possibly different hosts, as opposed to the network layer's logical communication between hosts themselves. This distinction is the crux of the whole article: IP delivers a packet to a machine; the transport layer must further deliver the packet's payload to the specific application process on that machine that is waiting for it. Every transport-layer packet, called a segment, carries a header with enough information to make this final delivery hop possible. The key piece of information is the port number, a 16-bit field (values 0–65535) that identifies an application-layer endpoint on a host. Ports are divided by convention into well-known ports (0–1023, assigned by IANA to standard services — port 80 for HTTP, port 443 for HTTPS, port 53 for DNS), registered ports (1024–49151, for other named applications), and ephemeral/dynamic ports (49152–65535, typically assigned automatically to client-side sockets by the OS). A process gains access to the network through a socket, the software abstraction that is the door between the application and the transport layer; a socket is uniquely identified by some combination of local IP address, local port, and — for connection-oriented protocols — the remote IP address and remote port as well. Two complementary operations make process-to-process delivery work. Multiplexing is the sender-side gathering of data chunks from multiple sockets, each wrapped with header information (crucially, port numbers), and passing the resulting segments down to the network layer. Demultiplexing is the receiver-side operation of using header information in an arriving segment to deliver its data to the correct socket among potentially many active sockets on that host. Multiplexing lets many application streams share one network-layer conduit; demultiplexing undoes that sharing correctly on arrival. Getting demultiplexing wrong — delivering a segment to the wrong or a nonexistent socket — is a violation of the entire purpose of the transport layer, so the demultiplexing rule must be an unambiguous, deterministic function of the segment header and the receiver's socket table.
How it works
Step 1 — Socket creation and binding. When a process wants network access, it asks the OS to create a socket. For a server, this typically means binding to a specific, well-known local port (e.g., a web server binds to port 80) so that clients know where to find it. For a client, the OS usually assigns an ephemeral port automatically.
Step 2 — Multiplexing at the sender. Each socket, when it sends data, has the transport layer attach a header. Minimally, this header includes a source port number and a destination port number (for connection-oriented transport, the destination IP and port were fixed at connection setup; for connectionless transport, the application specifies the destination on every send). The transport layer then hands the resulting segment to the network layer, which further wraps it in an IP datagram with source and destination IP addresses. Multiple sockets on the host interleave their segments onto the single outgoing network interface — this interleaving is multiplexing.
Step 3 — Demultiplexing at the receiver. When a segment arrives at a host (after the network layer strips the IP header and identifies the transport protocol via the IP header's protocol field, e.g., 17 for UDP, 6 for TCP), the transport layer must decide which socket, if any, receives the payload. The rule differs by protocol family:
- Connectionless demultiplexing (UDP-style): a socket is identified by the 2-tuple (destination IP, destination port). Any segment arriving with a given destination IP and port is delivered to the socket bound to that pair, regardless of source. Two UDP segments with different source IPs/ports but the same destination IP/port go to the same socket.
- Connection-oriented demultiplexing (TCP-style): a socket is identified by the full 4-tuple (source IP, source port, destination IP, destination port). Two arriving segments with the same destination IP and port, but different source IP or port, are demultiplexed to different sockets, because each represents a distinct connection (this is what allows a single web server on port 80 to serve thousands of simultaneous distinct client connections through one listening port).
Step 4 — Delivery. The identified socket's receive buffer receives the payload; the application process, blocked on a read call, wakes up and consumes the data. ``` // SENDER SIDE: multiplexing function send(socket, data): header = build_transport_header( src_port = socket.local_port, dst_port = socket.remote_port, // known for TCP; // supplied per-call for UDP protocol_specific_fields...) segment = header + data network_layer.send(segment, socket.local_ip, socket.remote_ip)
// RECEIVER SIDE: demultiplexing function on_segment_arrival(segment, src_ip, dst_ip, protocol): header = parse_transport_header(segment) src_port = header.src_port dst_port = header.dst_port if protocol == UDP: key = (dst_ip, dst_port) socket = socket_table.lookup_connectionless(key) if socket == NONE: send_icmp_port_unreachable(src_ip, src_port) return socket.receive_buffer.enqueue(segment.payload) else if protocol == TCP: key = (src_ip, src_port, dst_ip, dst_port) socket = socket_table.lookup_connection(key) if socket == NONE: // no matching established/pending connection listening_socket = socket_table.lookup_listener(dst_ip, dst_port) if listening_socket == NONE: send_reset(src_ip, src_port) return socket = listening_socket.handle_new_connection(src_ip, src_port) socket.receive_buffer.enqueue(segment.payload) ``` The pseudocode captures the entire mechanism: the only difference between UDP and TCP demultiplexing is the size of the key used to index the socket table — a 2-tuple versus a 4-tuple. Everything else (parsing the header, looking up a table, enqueueing into a buffer) is identical machinery.
Complexity & correctness
Time complexity. Both multiplexing and demultiplexing are, in the standard implementation, O(1) expected time per segment, because the socket table is a hash table keyed by the 2-tuple (UDP) or 4-tuple (TCP). Building the transport header on send is O(1) (a fixed number of fields). Parsing the header and doing a hash-table lookup on receive is O(1) expected, O(k) worst case in pathological hash-collision scenarios where k is the number of colliding entries — in practice negligible with a well-designed hash function over IP/port fields. This is essential: a host may be demultiplexing tens of thousands of TCP connections (e.g., a busy web server), and this operation happens on the critical path for every arriving packet, so anything worse than O(1) amortized would not scale.
Space complexity. The socket table requires O(S) space, where S is the number of open sockets on the host — each entry stores the identifying tuple, buffer pointers, and protocol state (for TCP: sequence numbers, window size, connection state). This is unavoidable: the OS must remember, for every open socket, enough information to route future segments to it and to track protocol state.
Correctness argument. The correctness of demultiplexing rests on an invariant we can state precisely: > Demultiplexing invariant. At any moment, no two active sockets on a host share the same identifying key (2-tuple for UDP-style, 4-tuple for TCP-style). If this invariant holds, then the lookup function `socket_table.lookup(key)` is a well-defined partial function — each key maps to at most one socket — so delivery is unambiguous. The invariant is maintained by the OS's socket-allocation logic:
- For UDP, `bind()` fails or is disallowed if another socket has already claimed the same (IP, port) pair (barring explicit `SO_REUSEPORT`-style opt-ins with well-defined additional rules), so the space of active 2-tuples is kept a valid partial function's domain by construction.
- For TCP, a listening socket is keyed only by (IP, port) with wildcards for the remote side, but each accepted connection is assigned a distinct 4-tuple, because the remote (client) IP and port are, by the birthday-bound argument over the client's own ephemeral port allocation and distinct client addresses, virtually always distinct across simultaneous clients; the rare theoretical collision (same client IP and port connecting "twice") cannot happen because that client's OS itself will not reuse an in-use (local IP, local port, remote IP, remote port) tuple while a connection using it is live.
Given the invariant, induction over segment arrivals shows every segment is delivered to exactly one socket (or correctly rejected, e.g. via ICMP port-unreachable for UDP or a TCP RST) — never dropped silently to the wrong destination and never duplicated across sockets.
Tradeoffs, honestly. The 2-tuple vs. 4-tuple design is not accidental generosity — it reflects what each protocol needs to track. UDP is connectionless: it has no notion of "a connection" to distinguish, so it only needs enough of a key to reach some socket bound to that port; if a UDP server wants to distinguish clients (e.g., a DNS-style responder), the application must do so using the source address information handed up alongside the payload, not the transport layer. TCP, being connection-oriented, must distinguish flows to maintain per-connection state (sequence numbers, congestion window), so the finer 4-tuple key is a necessity, not an optimization. This is one root cause of the qualitatively different service models offered by UDP: Connectionless Transport and TCP.
Worked example
Consider a web server at IP address `203.0.113.7`, listening on TCP port 80, simultaneously serving two browser clients, plus a DNS resolver on the same host listening on UDP port 53.
| Step | Segment arrives with | Protocol | Demux key used | Resolves to |
|---|---|---|---|---|
| 1 | src=198.51.100.5:51000, dst=203.0.113.7:80 | TCP | (198.51.100.5, 51000, 203.0.113.7, 80) | Socket S1 (Alice's connection) |
| 2 | src=198.51.100.9:52210, dst=203.0.113.7:80 | TCP | (198.51.100.9, 52210, 203.0.113.7, 80) | Socket S2 (Bob's connection) — new SYN → listener spawns S2 |
| 3 | src=198.51.100.5:51000, dst=203.0.113.7:80 | TCP | (198.51.100.5, 51000, 203.0.113.7, 80) | Socket S1 again (same 4-tuple) |
| 4 | src=192.0.2.44:33210, dst=203.0.113.7:53 | UDP | (203.0.113.7, 53) | Socket S3 (DNS service) |
| 5 | src=192.0.2.99:40012, dst=203.0.113.7:53 | UDP | (203.0.113.7, 53) | Socket S3 again — same socket despite different client! |
Step by step reasoning: 1. The first arriving segment is a fresh TCP SYN. No entry exists yet for the exact 4-tuple, but a listening socket exists for (203.0.113.7, 80), so the OS spawns a new connected socket S1 and inserts its 4-tuple key into the table. 2. A second client's SYN arrives, targeting the same destination IP and port 80, but with a different source IP and port. The 4-tuple differs, so a second socket S2 is spawned — Alice's and Bob's HTTP requests are never confused, even though they share a destination port. 3. A later data segment from Alice, still using her original ephemeral port 51000, matches S1's exact key and is delivered to the same socket, preserving her connection's byte stream. 4. A DNS query arrives via UDP. Because UDP demultiplexes only on (destination IP, destination port), it lands at S3 regardless of which client sent it. 5. A second, entirely different client's DNS query also lands at S3 — this is the qualitative difference from TCP: UDP's demultiplexing key is coarser, so all traffic to a given UDP port funnels into one socket, and it is the application's job (reading the sender's address from the received datagram's metadata) to tell the two queries apart if it needs to. This trace demonstrates precisely why TCP servers can maintain per-client state transparently (each client gets its own socket, hence its own buffers and sequence numbers) while UDP servers must manage multiple clients' state themselves within a single socket's application logic.
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Assuming a listening TCP socket "is" the connection | Confusing the passive listening socket (keyed on local IP/port only) with the per-connection sockets it spawns | Believing one socket handles all clients' data directly, leading to mis-modeled server design |
| Believing UDP can't tell clients apart at all | Over-generalizing "UDP demuxes on 2-tuple only" | Forgetting that the source address/port is delivered as metadata with each datagram — the application can and must use it |
| Thinking port numbers imply a specific protocol's semantics | Treating "port 80" as inherently meaning HTTP | Port numbers are conventions, not enforced bindings; any application can bind any free port |
| Forgetting ephemeral port exhaustion is possible | Ignoring that ephemeral range (typically ~49152–65535, or OS-specific wider ranges) is finite | Under very high connection churn (e.g., NAT gateways, load balancers), a host can run out of usable ephemeral ports for a given remote endpoint |
| Assuming identical destination port ⇒ identical socket for TCP | Not distinguishing 2-tuple vs 4-tuple demultiplexing rules | Misunderstanding how one server port serves many simultaneous TCP clients |
Pioneers & history
The transport layer's port-based multiplexing model crystallized during the design of the ARPANET's successor protocols, as researchers needed a general mechanism for many applications to share one network path. The abstraction of "ports" as software-addressable endpoints, distinct from network addresses, was central to making TCP/IP a genuinely multi-application internetworking architecture rather than a single-purpose file-transfer or terminal-access protocol.
Origins · 1 minVinton G. Cerf and how this was discovered
Cerf, together with Robert Kahn, authored the 1974 paper that first specified a Transmission Control Program capable of interconnecting heterogeneous packet networks. Their design separated concerns that had been conflated in earlier network protocols: what later became the IP layer handled host-to-host delivery, while the transport-layer concerns of process addressing, sequencing, and reliability were factored out, eventually splitting into the separate TCP and IP protocols in the late 1970s. This split is the direct ancestor of the port-based multiplexing scheme described in this article.
Where this matters
Every internet-facing application depends on transport-layer multiplexing whether or not its developers think about it explicitly. Web servers handling thousands of simultaneous HTTPS connections on port 443 rely on TCP's 4-tuple demultiplexing to keep each client's request stream separate and correctly sequenced. DNS resolvers, video-streaming protocols (many built atop UDP-based QUIC), and multiplayer games (often raw UDP for low latency) rely on the coarser UDP demultiplexing rule, pushing client-differentiation logic into the application layer by design. Network Address Translation (NAT) devices, firewalls, and load balancers manipulate and inspect exactly the port fields discussed here — a NAT box, for instance, rewrites source ports specifically to keep its own internal-to-external 4-tuple mappings unambiguous, a direct application of the demultiplexing invariant to a middlebox rather than an endpoint. Operating system kernels implement the socket table itself as a core piece of networking infrastructure, and its performance (O(1) hashed lookup) is one reason modern servers can sustain hundreds of thousands of concurrent connections. Understanding this layer is also the necessary foundation for the two natural next steps: the connectionless minimalism of UDP: Connectionless Transport, and the machinery — sequence numbers, acknowledgments, retransmission timers — that Principles of Reliable Data Transfer builds on top of an unreliable channel to deliver TCP's stronger guarantees.
Ask the tutor: How does NAT rewrite the 4-tuple, and why does that rewriting have to preserve the demultiplexing invariant on both sides of the translation?
What exactly is a socket, and how is it different from a port?
A port is just a 16-bit number identifying an endpoint address at the transport layer, like a mailbox number. A socket is the operating-system abstraction — a data structure with buffers, state, and an API — that a process uses to send and receive through that mailbox. One port number can be part of many sockets' identifying keys simultaneously (as with a TCP server on port 80, which has one listening socket plus one connected socket per client, all technically "using" port 80). The socket is the live object the application actually reads from and writes to; the port is merely one field, or part of one field, in the key that identifies that socket to the demultiplexing logic.
Why does UDP use a 2-tuple but TCP use a 4-tuple for demultiplexing?
The difference reflects what state each protocol must track. TCP is connection-oriented: it maintains per-connection state such as sequence numbers, acknowledgment numbers, and congestion windows, and mixing up two clients' state would corrupt both connections, so it must distinguish flows precisely using the full 4-tuple. UDP is connectionless and stateless at the transport layer — it has no "connection" to protect — so it only needs enough of a key to reach any socket listening at that destination address and port. Applications that need to distinguish UDP senders (like a DNS server replying to many resolvers) do so themselves, using the source address delivered alongside each datagram.
Can two different applications on the same host use the same port number?
Generally no, for the same protocol and same local IP address — the OS enforces the demultiplexing invariant by refusing a `bind()` call that would create a duplicate key, returning an "address already in use" error. Exceptions exist: some operating systems support `SO_REUSEPORT`, letting multiple sockets share a listening port cooperatively (typically to load-balance incoming connections across worker processes), but this is an explicit, controlled relaxation with its own well-defined selection rule, not a violation of demultiplexing correctness. Different protocols can reuse the same port number independently, since UDP port 53 and TCP port 53 are entirely separate namespaces.
What happens if a segment arrives for a port nobody is listening on?
For UDP, the host typically returns an ICMP "destination port unreachable" message to the sender, since there is no socket to notify the application otherwise. For TCP, if a SYN arrives with no matching listening socket, the receiving host replies with a TCP segment carrying the RST (reset) flag, informing the sender's TCP stack that the connection attempt is refused. In both cases, the transport layer does not silently discard the segment without any signal — well-behaved implementations always generate an explicit rejection, which is important both for user-visible error reporting (e.g., "connection refused") and for network diagnostics.
Is it possible for demultiplexing to fail or be ambiguous?
Not if the underlying invariant — uniqueness of socket keys — is properly enforced by the OS's bind/connect logic, which is the whole point of proving that invariant explicitly. Ambiguity would only arise from a bug or a deliberately malicious scenario, such as IP address spoofing, where an attacker crafts packets with a forged source address matching an existing legitimate connection's 4-tuple, attempting to inject or hijack traffic. This is precisely why TCP relies on additional safeguards beyond the 4-tuple, such as unpredictable initial sequence numbers, to make blind spoofing attacks far harder even though the demultiplexing table itself remains a simple, unambiguous lookup.
How does this relate to what I already know about the network layer and BGP?
The network layer, using inter-domain routing protocols like BGP, solves a global problem: getting a packet from any host to any other host across the entire internet, using only IP addresses. It has no idea that "processes" exist. The transport layer picks up exactly where that job ends — once a packet has physically arrived at the destination host — and solves the local problem of which process on that host should receive it. Multiplexing and demultiplexing are the mechanism for that local hand-off, making the transport layer the essential bridge between the network's coarse host-level view and the application's fine-grained, process-level view of communication.
Why do we say the transport layer provides a "spectrum" of services rather than one fixed service?
Different applications have fundamentally different needs: a file transfer cannot tolerate lost or reordered bytes, while a live video call would rather drop a frame than wait for a retransmission that arrives too late to be useful. Because a single one-size-fits-all service cannot satisfy both, the transport layer is designed as a menu: UDP offers a minimal, low-overhead, connectionless service with no reliability or ordering guarantees, while TCP offers a connection-oriented, reliable, ordered, congestion-controlled byte stream built with substantially more machinery. Application developers choose the transport protocol — and hence the point on this spectrum — that matches their application's tolerance for loss, latency, and complexity.