Core Networking Concepts
Every online interaction—fetching a web page or posting a status—can be viewed as input/output. At its heart, network communicasion is simply inter-process communication carried out across machines. The global internet identifies a unique process through the combination of an IP address (which locates the host) and a port number (which identifies the specific process on that host).
A port is a 16-bit unsigned integer handled at the transport layer. While a process can bind to multiple ports, a port cannot be shared by multiple processes simultaneously. Rather than using process IDs directly, the operating system maps ports to processes via an internal hash table, promoting a clean separation between system and network layers.
Transport Protocols: TCP and UDP
TCP (Transmission Control Protocol) establishes a connection before data transfer, guaranteeing ordered, error‑checked delivery. It suits applications like SSH where reliability is paramount.
UDP (User Datagram Protocol) is connectionless. It sends individual datagrams without handshaking, offering no delivery guarantees. This simplicity results in lower latency, making it ideal for live media streaming where occasional packet loss is acceptable.
Network Byte Order
Different CPU architectures store multi-byte integers in different byte orders (little‑endian or big‑endian). To standardize transmission, the network byte order is defined as big‑endian (most significant byte at the lowest memory address).
Portability requires converting between host and network byte order using these functions:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host to network, long (32-bit)
uint16_t htons(uint16_t hostshort); // host to network, short (16-bit)
uint32_t ntohl(uint32_t netlong); // network to host, long
uint16_t ntohs(uint16_t netshort); // network to host, short
These functions transform data only when the host byte order differs from big‑endian.
The Socket API
Key functions for socket operations:
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
The socket() call creates an endpoint; internally this resembles opening a special "network file" linked to the NIC. Subsequent reads and writes operate on the NIC’s buffer.
Unified Addressing with sockaddr
Different address families (IPv4, IPv6, UNIX domain) require different structures. The generic sockaddr structure acts as a common interface. Specific structures like sockaddr_in (IPv4) or sockaddr_un (local) are cast to sockaddr* when passed into API functions. The function inspects the sa_family field to determine the actual address type—a design reminiscent of polymorphism.
Building a UDP Echo Server
Server Initialization
The server creates a datagram socket, prepares an address structure, and binds them.
void InitServer() {
// 1. Create UDP socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(FATAL, "socket creation error: %s", strerror(errno));
exit(SOCKET_ERROR);
}
// 2. Fill address structure
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // convert to network order
local.sin_addr.s_addr = INADDR_ANY; // accept traffic on any interface
// 3. Bind socket to address
if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
LOG(FATAL, "bind error: %s", strerror(errno));
exit(BIND_ERROR);
}
LOG(INFO, "Server bound successfully.");
}
Binding to INADDR_ANY instructs the kernel to deliver packets destined for the server’s port, regardless of the network interface they arrive on. The IP address, originally presented in dotted‑decimal notation (e.g., "192.168.1.10"), must be converted to a 32‑bit integer using inet_addr().
Running the Service
The server loops indefinitely, receiving and echoing messages.
void Run() {
_isrunning = true;
char buffer[1024];
struct sockaddr_in client;
socklen_t addrlen = sizeof(client);
while (_isrunning) {
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client, &addrlen);
if (n > 0) {
buffer[n] = '\0';
InetAddr peer(client);
LOG(DEBUG, "From [%s:%u]: %s", peer.Ip().c_str(), peer.Port(), buffer);
// Echo data back
sendto(_sockfd, buffer, n, 0, (struct sockaddr*)&client, addrlen);
}
}
}
A helper class can encapsulate the conversion from network‑byte‑order binary to human‑readable form:
class InetAddr {
public:
explicit InetAddr(const struct sockaddr_in& addr) : _addr(addr) {
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
std::string Ip() const { return _ip; }
uint16_t Port() const { return _port; }
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
Client Implementation
The client also creates a socket, but it does not explicitly call bind(). The operating system automatically assigns a free ephemeral port, avoiding client‑side port conflicts. The client then enters an interactive send‑receive loop.
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage(argv[0]);
return 1;
}
std::string serverIP = argv[1];
uint16_t serverPort = std::stoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
std::cerr << "Socket creation failed." << std::endl;
return 1;
}
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(serverPort);
serverAddr.sin_addr.s_addr = inet_addr(serverIP.c_str());
std::string input;
while (true) {
std::cout << "> ";
std::getline(std::cin, input);
sendto(sock, input.c_str(), input.size(), 0,
(struct sockaddr*)&serverAddr, sizeof(serverAddr));
char replyBuffer[1024];
struct sockaddr_in responder;
socklen_t respLen = sizeof(responder);
ssize_t r = recvfrom(sock, replyBuffer, sizeof(replyBuffer) - 1, 0,
(struct sockaddr*)&responder, &respLen);
if (r > 0) {
replyBuffer[r] = '\0';
std::cout << "Echo: " << replyBuffer << std::endl;
}
}
return 0;
}
Testing
Start with a local loopback test using 127.0.0.1. For network‑wide testing, ensure the server’s firewall (e.g., ufw) allows UDP traffic on the chosen port, and use netstat -unap to verify the binding shows 0.0.0.0:port in listening state.