pk.org: CS 417/Lecture Notes

Socket Programming

The core of network communication

Paul Krzyzanowski – 2026-01-26

Every time you browse a website, send a message, or stream a video, your application is using sockets. Sockets are the standard interface for network communication on every modern operating system. They give processes a uniform way to send and receive data over a network, whether that network is the global Internet or a local connection between processes on the same machine.

Higher-level abstractions like HTTP libraries and remote procedure calls hide much of the complexity, but they are all built on top of sockets. When something goes wrong, when you need to optimize performance, or when you need to implement a custom protocol, you will need to understand what is happening at the socket level.


The Origin of Sockets

Early networking stacks were tied to specific protocols and hardware, and this made interoperability painful.

The socket API was developed at the University of California, Berkeley as part of the 4.2BSD (Berkeley Software Distribution) Unix release in August 1983. The work was funded by DARPA (the Defense Advanced Research Projects Agency) as part of an effort to integrate the emerging TCP/IP protocols into Unix.

The design treated network communication like file I/O. Unix had already established the convention that “everything is a file.” Processes read from and write to files using file descriptors, which are small integers that serve as handles to open resources. The socket API extended this model to network connections. A socket is represented by a file descriptor, and once a connection is established, sending data is much like writing to a file.

Existing Unix tools and programming patterns could therefore be adapted for network communication with little change. A program could create a socket, map its file descriptor to standard input and standard output, and the rest of the program could run unmodified. Programmers used familiar operations: open a connection, read and write data, close it when done.

The socket API was not the only approach proposed at the time. At Bell Labs, Dennis Ritchie (the creator of C) developed an alternative called STREAMS (later called the Transport Layer Interface, or TLI). STREAMS had a cleaner architecture in some respects, but the socket API had one decisive advantage: it was already widely deployed. By the time STREAMS shipped in AT&T’s Unix release, sockets had become the de facto standard, and that standard has persisted for over four decades.


Cross-Platform Compatibility

The socket API is remarkably consistent across operating systems. The core functions and data structures are essentially the same whether you are programming on Linux, macOS, FreeBSD, or Windows. The differences between the Unix-derived systems and Windows are roughly the difference between British English and American English, while a more divergent API would feel like the difference between English and Estonian.

On Unix-like systems (Linux, macOS, FreeBSD, and others), the socket API is part of the POSIX standard. The API is implemented in the kernel and accessed through system calls. Socket descriptors are regular file descriptors and can be used with standard Unix I/O functions like read() and write().

On Windows, the socket API is provided by Winsock (Windows Sockets). Winsock was developed in the early 1990s by a consortium of vendors who wanted a standard TCP/IP API for Windows. They deliberately kept it close to the Berkeley interface to simplify porting. The core functions (socket(), bind(), listen(), accept(), connect(), send(), recv()) work almost identically.

A few differences appear when writing cross-platform code:

Aspect Unix/POSIX Windows
Header files <sys/socket.h>, <netinet/in.h>, <arpa/inet.h> <winsock2.h>, <ws2tcpip.h>
Initialization None required Must call WSAStartup() before using sockets
Cleanup None required Should call WSACleanup() when done
Closing a socket close(fd) closesocket(fd)
Error handling Check errno Call WSAGetLastError()
Socket type int (signed) SOCKET (unsigned)

If you stick to the core socket functions and avoid platform-specific extensions, your code will be portable with minor adjustments for these differences.


The Socket Abstraction

A socket is an endpoint for communication. Think of it as a mailbox: it has an address (an IP address and port number), and you can send and receive messages through it.

Sockets were designed as a general-purpose interface for communication, not specifically for the Internet. This makes them flexible. You can create local sockets or use different versions of IP, but the setup phase asks for more information than you might expect.

When you create a socket, you specify three things:

  1. Address family: The type of addresses the socket will use. AF_INET is for IPv4 addresses, AF_INET6 is for IPv6. Other families include AF_UNIX (or AF_LOCAL) for communication between processes on the same machine without going through the network stack, and AF_BLUETOOTH for Bluetooth networks.

  2. Socket type: The communication semantics. SOCK_STREAM provides a reliable, ordered byte stream (used for TCP). SOCK_DGRAM provides unreliable, unordered datagrams (used for UDP).

  3. Protocol: Usually set to 0, which tells the system to choose the default protocol for the given address family and socket type. For AF_INET with SOCK_STREAM, the default is TCP. For AF_INET with SOCK_DGRAM, the default is UDP.

A newly created socket has no address. It is an endpoint waiting to be configured. For a server, you bind the socket to a specific address and port. For a client, the local address is usually assigned automatically when you connect.


Programming

The next sections walk through TCP and UDP communication, beginning with the system calls and C examples.

Even if you don’t plan to write networking code in C, it’s useful to see exactly what the operating system provides. The C examples invoke the system calls directly. Other frameworks, such as Java and Python, wrap these calls in higher-level abstractions that simplify the code while hiding some of the details.

The examples were written and tested on a Raspberry Pi 5 running Debian Linux but should work on any Linux system and will likely compile on a Mac as well. Some Mac builds may need an extra header file.

TCP Socket Programming

TCP (Transmission Control Protocol) provides reliable, ordered, connection-oriented communication. “Reliable” here means the TCP stack retransmits lost segments, reorders arrivals, performs flow control, and manages congestion.

Before data can be exchanged, a connection must be established between the client and server. This connection persists until one side closes it.

A TCP server uses two kinds of sockets:

  1. A listening socket bound to a local address and port.

  2. A connected socket returned by accept(), representing one client session.

The listening socket stays open to accept more connections. Each accepted connection gets its own connected socket.

The Server Side

A TCP server follows a specific sequence of operations:

  1. Create a socket using socket().

  2. Bind the socket to an address and port using bind().

  3. Listen for incoming connections using listen().

  4. Accept a connection using accept(), which returns a new socket for communicating with the client.

  5. Send and receive data using send() and recv() (or read() and write()).

  6. Close the connection using close().

The listen() call turns the socket into a listening socket (one that waits for incoming connections). The argument to listen() specifies a backlog: the maximum number of pending connections that can be queued while the server is busy.

The accept() call blocks until a client connects. When a connection arrives, accept() creates a new connected socket for that client and returns it. The original socket continues listening for additional connections. This is how a server handles multiple clients: the listening socket stays open while each client gets its own communication socket.

The Client Side

A TCP client is simpler:

  1. Create a socket using socket().

  2. Connect to the server using connect().

  3. Send and receive data using send() and recv().

  4. Close the connection using close().

The client does not need to call bind(). The operating system automatically assigns a local port when connect() is called. The client also does not call listen() or accept() because it is initiating the connection, not waiting for one.

Linux System Calls in Detail

System calls are the interfaces the operating system gives us to system services (allocating memory, accessing files, creating processes, and so on). The socket system calls are the only interface processes have for interacting with the network. All other APIs are built on top of these.

socket()

int socket(int domain, int type, int protocol);
Creates a new socket and returns a file descriptor. Returns -1 on error.

bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Associates the socket with a specific address and port. The address is passed as a sockaddr_in structure (for IPv4), which must be cast to the generic sockaddr type. Returns 0 on success, -1 on error.

listen()

int listen(int sockfd, int backlog);
Marks the socket as passive (willing to accept connections). The backlog parameter hints at how many pending connections the system should queue. Returns 0 on success, -1 on error.

accept()

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
Waits for and accepts an incoming connection. Returns a new socket descriptor for the connection, or -1 on error. The client’s address is stored in addr.

connect()

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Initiates a connection to the specified address. For TCP, this performs the three-way handshake. Returns 0 on success, -1 on error.

send() and recv()

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
Send and receive data on a connected socket. These are similar to write() and read(), but allow flags to modify behavior. Both return the number of bytes transferred, or -1 on error. A return value of 0 from recv() indicates the connection was closed.

close()

int close(int fd);
Closes the socket and releases resources. For TCP, this initiates the connection termination sequence.

A Simple TCP Server in C

The following program creates a TCP server that listens on port 8080, accepts one connection, receives a message, and sends a response.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(void) {
    int server_fd, client_fd;
    struct sockaddr_in address;
    socklen_t addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    const char *response = "Hello from server";

    // Create socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Allow address reuse (avoids "address already in use" errors)
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // Bind to address
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // Accept connections on any interface
    address.sin_port = htons(PORT);        // Convert port to network byte order

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // Listen for connections
    if (listen(server_fd, 5) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d\n", PORT);

    // Accept a connection
    client_fd = accept(server_fd, (struct sockaddr *)&address, &addrlen);
    if (client_fd < 0) {
        perror("accept failed");
        exit(EXIT_FAILURE);
    }
    printf("Client connected\n");

    // Receive data
    ssize_t bytes_read = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Received: %s\n", buffer);
    }

    // Send response
    send(client_fd, response, strlen(response), 0);
    printf("Response sent\n");

    // Clean up
    close(client_fd);
    close(server_fd);
    return 0;
}

A Simple TCP Client in C

The following program connects to the server above, sends a message, and receives the response. The client is hardcoded to connect to 127.0.0.1 (a special loopback address representing the local host) on port 8080.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(void) {
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE] = {0};
    const char *message = "Hello from client";

    // Create the socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Set up the server address
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);

    // Convert address from text to binary
    if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
        perror("invalid address");
        exit(EXIT_FAILURE);
    }

    // Connect to the server
    if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        exit(EXIT_FAILURE);
    }
    printf("Connected to server\n");

    // Send message
    send(sock_fd, message, strlen(message), 0);
    printf("Message sent\n");

    // Receive response
    ssize_t bytes_read = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Received: %s\n", buffer);
    }

    // Clean up
    close(sock_fd);
    return 0;
}

You can compile this via:

cc -o client client.c
cc -o server server.c

Then run ./client in one window and ./server in another.


UDP Socket Programming

UDP (User Datagram Protocol) provides unreliable, connectionless communication. There is no connection setup, no guarantee of delivery, and no guarantee that datagrams arrive in order. Each sendto() transmits a single datagram, and each recvfrom() receives a single datagram.

UDP is simpler than TCP. There is no three-way handshake, no connection state to maintain, and no need for listen() or accept(). A UDP server creates a socket, binds it to a port, and starts receiving datagrams. A UDP client creates a socket and starts sending datagrams to the server’s address.

The Server Side

  1. Create a socket using socket() with SOCK_DGRAM.

  2. Bind the socket to an address and port using bind().

  3. Receive datagrams using recvfrom(), which also provides the sender’s address.

  4. Send responses using sendto(), specifying the destination address.

  5. Close the socket when done.

The Client Side

  1. Create a socket using socket() with SOCK_DGRAM.

  2. Send datagrams using sendto(), specifying the server’s address.

  3. Receive responses using recvfrom().

  4. Close the socket when done.

The client does not need to call bind(). The operating system assigns a local port automatically when the first sendto() is called.

System Calls for UDP

sendto()

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
Sends a datagram to the specified destination address. Returns the number of bytes sent, or -1 on error.

recvfrom()

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
Receives a datagram and stores the sender’s address in src_addr. Returns the number of bytes received, or -1 on error.

A Simple UDP Server in C

Like the TCP example, this server is hardcoded to receive requests on port 8080.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(void) {
    int sock_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];
    const char *response = "Message received";

    // Create a UDP socket
    sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Bind to the default local address
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP server listening on port %d\n", PORT);

    // Receive and respond to datagrams
    while (1) {
        ssize_t bytes_read = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0,
                                      (struct sockaddr *)&client_addr, &client_len);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("Received: %s\n", buffer);

            // Send response back to client
            sendto(sock_fd, response, strlen(response), 0,
                   (struct sockaddr *)&client_addr, client_len);
        }
    }

    close(sock_fd);
    return 0;
}

A Simple UDP Client in C

The client sends messages to localhost at 127.0.0.1 on port 8080.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(void) {
    int sock_fd;
    struct sockaddr_in server_addr;
    socklen_t server_len = sizeof(server_addr);
    char buffer[BUFFER_SIZE];
    const char *message = "Hello from UDP client";

    // Create UDP socket
    sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Set up server address
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    // Send message
    sendto(sock_fd, message, strlen(message), 0,
           (struct sockaddr *)&server_addr, server_len);
    printf("Message sent\n");

    // Receive response
    ssize_t bytes_read = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0,
                                  (struct sockaddr *)&server_addr, &server_len);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Received: %s\n", buffer);
    }

    close(sock_fd);
    return 0;
}

Byte Order

Network protocols use big-endian byte order (most significant byte first), also called network byte order. Many computers, including those with x86 processors and ARM-based CPUs on the Mac, use little-endian byte order internally. This means you must convert multi-byte values (like port numbers and IP addresses) when putting them into socket address structures.

The socket API provides four functions for this conversion:

Function Description
htons() Host to network short (16-bit)
htonl() Host to network long (32-bit)
ntohs() Network to host short (16-bit)
ntohl() Network to host long (32-bit)

Always use htons() when setting the port number in a sockaddr_in structure, and htonl() when setting the IP address (though inet_pton() handles the conversion for addresses specified as strings).

Socket API Summary

Task TCP UDP
Create endpoint socket(AF_INET6, SOCK_STREAM, 0) socket(AF_INET6, SOCK_DGRAM, 0)
Choose local port bind() bind() (if receiving on a known port)
Server readiness listen() (not used)
Server receives client accept() recvfrom()
Client establishes “session” connect() optional connect()
Send data send() sendto() (or send() if “connected”)
Receive data recv() recvfrom() (or recv() if “connected”)

Socket Programming in Python

Python’s socket module provides a direct interface to the operating system’s socket API. The main difference is that Python sockets are objects rather than file descriptors, and methods are called on the socket object.

TCP Server in Python

import socket

HOST = ''        # Empty string means all available interfaces
PORT = 8080

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(5)
    print(f"Server listening on port {PORT}")

    conn, addr = server_socket.accept()
    with conn:
        print(f"Connected by {addr}")
        data = conn.recv(1024)
        if data:
            print(f"Received: {data.decode()}")
            conn.sendall(b"Hello from server")

TCP Client in Python

import socket

HOST = '127.0.0.1'
PORT = 8080

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
    client_socket.connect((HOST, PORT))
    client_socket.sendall(b"Hello from client")
    data = client_socket.recv(1024)
    print(f"Received: {data.decode()}")

The code can be even shorter because Python’s socket module provides a helper that creates a TCP socket and connects it in one step, combining socket() and connect():

import socket

HOST = '127.0.0.1'
PORT = 8080

with socket.create_connection((HOST, PORT), timeout=5) as client_socket:
    client_socket.sendall(b"Hello from client")
    data = client_socket.recv(1024)
    print(f"Received: {data.decode()}")

UDP Server in Python

import socket

HOST = ''
PORT = 8080

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server_socket:
    server_socket.bind((HOST, PORT))
    print(f"UDP server listening on port {PORT}")

    while True:
        data, addr = server_socket.recvfrom(1024)
        print(f"Received from {addr}: {data.decode()}")
        server_socket.sendto(b"Message received", addr)

UDP Client in Python

import socket

HOST = '127.0.0.1'
PORT = 8080

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_socket:
    client_socket.sendto(b"Hello from UDP client", (HOST, PORT))
    data, addr = client_socket.recvfrom(1024)
    print(f"Received: {data.decode()}")

The Python version is far shorter: 10 lines versus 56 for the client, and 10 lines versus 69 for the server.

Most of the C code was setup. In a real application that does substantial work over the connection, the setup ends up being a small portion of the code base.


Socket Programming in Java

Java provides socket classes in the java.net package. For TCP, the main classes are ServerSocket (for servers) and Socket (for clients and for the server-side connection to each client). For UDP, use DatagramSocket and DatagramPacket.

TCP Server in Java

import java.io.*;
import java.net.*;

public class TcpServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;

        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("Server listening on port " + port);

            try (Socket clientSocket = serverSocket.accept();
                 BufferedReader in = new BufferedReader(
                     new InputStreamReader(clientSocket.getInputStream()));
                 PrintWriter out = new PrintWriter(
                     clientSocket.getOutputStream(), true)) {

                System.out.println("Client connected");
                String message = in.readLine();
                System.out.println("Received: " + message);
                out.println("Hello from server");
            }
        }
    }
}

TCP Client in Java

import java.io.*;
import java.net.*;

public class TcpClient {
    public static void main(String[] args) throws IOException {
        String host = "127.0.0.1";
        int port = 8080;

        try (Socket socket = new Socket(host, port);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()))) {

            out.println("Hello from client");
            String response = in.readLine();
            System.out.println("Received: " + response);
        }
    }
}

UDP Server in Java

import java.io.*;
import java.net.*;

public class UdpServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        byte[] buffer = new byte[1024];

        try (DatagramSocket socket = new DatagramSocket(port)) {
            System.out.println("UDP server listening on port " + port);

            while (true) {
                DatagramPacket request = new DatagramPacket(buffer, buffer.length);
                socket.receive(request);

                String message = new String(request.getData(), 0, request.getLength());
                System.out.println("Received: " + message);

                byte[] response = "Message received".getBytes();
                DatagramPacket reply = new DatagramPacket(
                    response, response.length,
                    request.getAddress(), request.getPort());
                socket.send(reply);
            }
        }
    }
}

UDP Client in Java

import java.io.*;
import java.net.*;

public class UdpClient {
    public static void main(String[] args) throws IOException {
        String host = "127.0.0.1";
        int port = 8080;

        try (DatagramSocket socket = new DatagramSocket()) {
            InetAddress address = InetAddress.getByName(host);

            byte[] message = "Hello from UDP client".getBytes();
            DatagramPacket request = new DatagramPacket(
                message, message.length, address, port);
            socket.send(request);

            byte[] buffer = new byte[1024];
            DatagramPacket response = new DatagramPacket(buffer, buffer.length);
            socket.receive(response);

            String reply = new String(response.getData(), 0, response.getLength());
            System.out.println("Received: " + reply);
        }
    }
}

Download Examples

You can download a zip file with the sample code here.

When you unzip it, you’ll see three directories under socket-demo named c, java, and python. Each contains TCP and UDP client and server code for that language. You can compile the C and Java code by running

make
in the socket-demo directory.


Common Pitfalls

A few errors come up often, depending on the language and the type of session.

Forgetting byte order conversion. In C, if you forget to use htons() for the port number, your server might listen on the wrong port, or your client might try to connect to the wrong port. The bug is confusing because the numbers look correct in source code. Java and Python handle byte ordering inside their library methods, so it isn’t something you need to deal with there.

Not handling partial reads. TCP is a byte stream; UDP is a message stream. A call to recv() or read() is not guaranteed to receive all the data you asked for. It returns as soon as any data is available. For TCP, you must loop until you have received the expected amount of data. For UDP, each recvfrom() returns exactly one datagram.

Not handling partial writes. Similarly, send() might not transmit all your data in one call, especially for large buffers. Check the return value and loop if necessary. With UDP, send() fails if you try to send a message larger than the network interface can accept.

Address already in use. If you restart a server quickly after stopping it, you may get an “address already in use” error because the TCP connection is in the TIME_WAIT state. The SO_REUSEADDR socket option allows binding to an address that is in TIME_WAIT.

Blocking forever on accept() or recv(). These calls block by default. If no client connects or no data arrives, the program waits indefinitely. Production code should use timeouts, non-blocking I/O, or select/poll.