Six fields, filled in by four calls
◈ 10 cardsName the six fields of a socket's association and which call fills in each; distinguish a passive listening socket from the active one accept() returns.
A socket is an endpoint, and both ends need one
A socket is a file type — like a directory or a FIFO — and it is a full-duplex endpoint of communication. It is the only IPC channel in UNIX that crosses machines: pipes and FIFOs are confined to one host, and sockets are not. Both parties create one; there is no arrangement in which a client attaches to a server's socket. A socket is process-persistent, meaning it lives exactly as long as the process that created it, and it is used through the same open–read–write–close paradigm as any other file.
What makes a socket useful is not the descriptor. It is the six-field record the kernel keeps behind it, and the fact that a conversation needs all six.
The six fields of an association
An association is what identifies a conversation on the Internet, and it has exactly six parts:
- the protocol family —
PF_INETfor IPv4; - the service type —
SOCK_STREAM(TCP) orSOCK_DGRAM(UDP); - the local IP address;
- the local port;
- the remote IP address;
- the remote port.
Four calls fill those six fields in, and knowing which call fills in which is the whole mental model.
socket(domain, type, protocol) fills in the first two and returns a descriptor. Nothing about the machine is decided yet — the socket has a family and a service type and no address at all. Passing protocol as 0 lets the kernel choose the only sensible protocol for that family and type, which is why you almost always write 0.
bind(s, addr, addrlen) stores the local IP address and the local port. The socket now knows where it lives and nothing about who it will talk to. This state has a name: the socket is half associated.
The remaining two fields — the remote IP address and the remote port — are filled in by the rendezvous of two calls on two different machines. On the client, connect() supplies them. On the server, accept() learns them from the arriving connection request. When both ends have all six fields the socket is fully associated, and a full association is precisely what a virtual connection is. Connection-oriented communication requires it; connectionless communication does not, which is why UDP can get by with far less.
Worked example — a server and a client, side by side
Take a server on example.host that will answer on TCP port 9000, and a client on some laptop.
Server, step by step. socket(PF_INET, SOCK_STREAM, 0) returns descriptor 3. Fields 1 and 2 are set: IPv4, stream. Fields 3–6 are empty. The server then zeroes a struct sockaddr_in, sets sin_family = AF_INET, sets sin_addr.s_addr = htonl(INADDR_ANY), sets sin_port = htons(9000), and calls bind(). Fields 3 and 4 are now set — any local address, port 9000 — and descriptor 3 is half associated. listen(3, 16) changes no field at all; it marks the socket passive and tells the kernel to start queueing connection requests behind it. Then accept(3, ...) blocks.
Client, step by step. socket(PF_INET, SOCK_STREAM, 0) returns descriptor 3 on the laptop. There is no bind() — the client does not care which local port it gets, so it lets connect() pick an unused one from the dynamic range 49152–65535. connect(3, &server_addr, len) performs the three-way TCP handshake. When it returns, the client's descriptor 3 has all six fields: IPv4, stream, the laptop's IP, the ephemeral port the kernel chose, example.host, port 9000. Fully associated.
Back on the server. The handshake the client just completed is what unblocks accept(), and accept() returns descriptor 4. Descriptor 4 has all six fields, the mirror image of the client's. Descriptor 3 still has only four, and is still passive, and is still queueing.
The structural fact everyone misses: accept() returns a NEW socket
That second descriptor is not an implementation detail, and it is the fact most often got wrong in an exam. The reason it must exist is a one-line argument: after accepting a connection the server has to go back to listening, and it cannot also be conversing on the same socket. One socket cannot simultaneously be a queue for strangers and a private channel to one client. So accept() mints a second socket for the conversation and hands the first one straight back to its job.
That gives every server two kinds of socket, with two different lifetimes:
- The passive socket — the one you
listen()ed on. It is never used for data. Not one byte of the client's request travels through it. It exists to receive connection requests, and it lives for the entire life of the server. - The active or ephemeral socket — the one
accept()returned. It carries all the data, and it lives only as long as that one connection.
A server serving three clients therefore holds four sockets, and the counting lesson later in this module makes that arithmetic exact. It also explains why accept() on a socket you never listen()ed on fails with EINVAL: there is no queue to take anything off.
Byte order, at the wire boundary
One detail crosses the machine boundary with the address. A little-endian machine stores the low-order byte at the low address; a big-endian machine stores it at the high address. Internet protocols were standardised on big-endian, which is why big-endian is called network byte order. Intel is little-endian; Sun and Motorola are big-endian.
So any multi-byte value you put into a socket address structure must be converted: htons() for a 16-bit value (a port), htonl() for a 32-bit one (an IPv4 address), and ntohs()/ntohl() coming back the other way. Port 9000 is 0x2328. Stored raw on an Intel machine its bytes leave in memory order, 28 23, and the far end reads that pair as port 10275 — so the connection goes to the wrong service, or nowhere.