Computing History · Networking History
NCP and the Limits That Forced TCP/IP
How the ARPANET host-to-host protocol split messages into connections, why its addressing and flow control could not span multiple networks, and what TCP added to replace it.
By 1970 the ARPANET had a working subnet and no working convention for what hosts said to each other through it. The 1822 interface could move a message from one host to another, but it delivered messages, not conversations: a program that sent four messages might receive four replies, or three, or none, and there was no way to know which host process should get them. The Network Control Protocol filled that gap, and its structure explains why it could not be extended to an internetwork.
NCP is small, and its smallness is the point. It consists of little more than a handshake for opening a connection and a rule for how a byte stream is cut into the subnet’s messages. It does not address hosts, because the 1822 leader already does. It does not reassemble, because messages arrive whole. It does not detect loss, because the subnet’s RFNM handshake already reports delivery. NCP is a session layer over a reliable message service, and every one of those dependencies later became a reason to replace it.
Connections built from link numbers
A connection under NCP is identified by a pair of sockets, and a socket is a 40-bit quantity: a 32-bit host address and an 8-bit link number, with the socket number itself carried in the first bytes of the message payload rather than in the leader. The link number is the piece the IMP knows about. The socket number is the piece the host’s operating system knows about. Together they name a port on a machine.
Opening a connection required an exchange rather than an assignment. A host sent an Initiate command naming its own socket and the socket it wanted to reach, with a flag for whether it expected to send, receive, or both. The peer could refuse. Until both sides had agreed, no data could flow, and the agreement was symmetric in a way that made the common client-server pattern awkward: the server had to have issued a Listen before the client’s Initiate arrived, or the Initiate was refused and the client retried later.
This is recognisably the ancestor of the three-way handshake, and it has the same purpose: both sides must learn that the other is ready, and both must agree on an identifier before either commits state. The difference is where the state lives. Under NCP, the connection is state in two operating systems and nothing else; the network has no notion of it, and no mechanism to report that a peer has vanished except the IMP’s host-down message.
The message boundary problem
NCP carried a byte stream by cutting it into messages, and the cutting rule was the source of a persistent class of bug. The protocol required that a message be filled to its maximum before a short one was sent, except at the end of a transmission, and it forbade a message of zero length except as an explicit end-of-transmission marker. A host that wanted to send a byte immediately, as an interactive terminal does, either waited for more bytes or sent a short message and paid the overhead of a leader plus an RFNM round trip.
The workaround that hosts adopted was to mark message boundaries in the payload itself with an escape convention, effectively reconstructing a record structure on top of a stream. Applications then depended on the convention, so the boundary between transport and application blurred. When TCP later defined a pure byte stream with no message boundaries at all, applications that had relied on the boundary convention had to be rewritten to frame their own records.
The deeper issue was that the subnet’s message size was the transport’s segmentation size. A path with a slow link in the middle set the pace for the whole conversation, and there was no negotiation about it: 8,096 bits was the maximum everywhere, so the number was uniform and no path discovery was needed. Uniformity was a benefit while one network existed and a constraint once paths crossed networks with different limits.
Three assumptions that an internetwork breaks
The first assumption is addressing. An NCP socket names a host by its position on one IMP and its own bit position on that host. The leader’s destination field is wide enough for the IMPs of a single network and no more, so the address space is exactly the size of one network built by one contractor. Joining two networks requires a name that is meaningful outside either one, which is why the TCP specification that Cerf and Kahn published in 1974 introduced a global network identifier and a longer address, and why the final IPv4 header spends 32 bits on the address and treats the network portion as a variable-length prefix.
The second assumption is reliability. NCP did not retransmit, because the IMPs did. That works when every hop acknowledges, and it stops working when a path crosses a network whose interior drops messages without telling anyone, or a radio link where loss is the normal condition rather than a fault. Retransmission had to move to the endpoints, and with it came the need for sequence numbers, a reassembly buffer, and a way to distinguish a duplicate from a new message.
The third assumption is that the sender can be trusted to pace itself. The one-message window per link was enforced by the subnet, so a host could not overrun its own IMP. Across an internetwork there is no such per-link window at the boundary, because the boundary no longer exists as a single point; a gateway connects networks and has no authority over the hosts behind it. Pacing must therefore be negotiated end to end, and the mechanism for that negotiation is the subject of AIMD Congestion Control and the Collapse It Prevented.
What the split into TCP and IP changed
The 1974 specification described a single protocol, the Transmission Control Program, that did both addressing and reliable delivery. The split into TCP and IP came a few years later, and the reason was operational rather than theoretical: some applications did not want retransmission. Packet voice, real-time control, and the early network management traffic all preferred to lose data rather than wait for it, and a monolithic protocol forced reliability on them.
Separating IP as a bare datagram service made the network’s promise smaller and the endpoints’ responsibility larger. IP promises to attempt delivery of a self-describing packet to an address, and nothing else. TCP on top adds sequencing, acknowledgement, retransmission, flow control, and connection state. A protocol that wants none of those can sit directly on IP. The layering that the ARPANET had between IMP and host was reproduced between network and transport, one level up.
The cost of that reproduction is duplicated function. Every TCP connection re-derives what the subnet used to guarantee: that the peer exists, that it is alive, that it is willing to receive. The three-way handshake, the retransmission timer, and the keepalive mechanism all exist because the network below no longer reports anything about the endpoint. A host that crashes leaves its peer retransmitting into silence until a timer expires, and the length of that timer is a guess about a path whose round-trip time is unknown.
# Connection identity under a host-to-host protocol: the four-tuple that
# must be agreed before data flows, and must be unique at each endpoint.
def connection_key(src_ip, src_port, dst_ip, dst_port):
return (src_ip, src_port, dst_ip, dst_port)
# The same pair of hosts may hold many simultaneous connections, so the
# identity cannot be the host pair alone; the ports are what separate them.
a = connection_key("10.0.0.1", 40000, "10.0.0.2", 25)
b = connection_key("10.0.0.1", 40001, "10.0.0.2", 25)
assert a != b
The four-tuple is the durable inheritance. NCP’s socket pair was a host number plus a link number plus a socket number; the modern form keeps the pair-of-endpoints structure and replaces the network-specific parts with addresses that survive a change of underlying network.
The residue in later protocols
NCP’s design shows up in places that are easy to miss. The idea that a connection is opened by a symmetric agreement rather than by a unilateral request persists in TCP’s simultaneous-open path, which is rarely used but still specified. The idea that the transport should not know the path’s maximum message size persists in the way TCP discovers the path MTU by probing and backing off, rather than by being told.
The idea that failed most completely is the assumption of a single administrative domain. NCP had no concept of a policy boundary, because there was one network and one operator. Once there were many, addressing had to encode topology and routing had to encode policy, which is why the address is now a prefix with an owner and why the routing protocol carries attributes rather than distances. That transition, from a flat address space to one that carries administrative meaning, is traced in Classful Addressing, Subnets, and the Move to CIDR.
References
- RFC 33: New HOST-HOST Protocol — IETF
- RFC 675: Specification of Internet Transmission Control Program — IETF
- RFC 793: Transmission Control Protocol — IETF
- A Protocol for Packet Network Intercommunication — Vinton G. Cerf and Robert E. Kahn, IEEE Transactions on Communications, Vol. 22 No. 5, 1974