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 fundamental interface for network communication in virtually every modern operating system. They provide a standardized way for processes 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.

Understanding sockets is essential for building distributed systems. While higher-level abstractions like HTTP libraries and remote procedure calls hide much of the complexity, 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. This, of course, was an interoperability nightmare.

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 key insight behind the socket API was to treat network communication like file I/O.

Unix had already established the paradigm 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.

This design decision meant that existing Unix tools and programming patterns could be easily adapted for network communication.

For instance, create a socket to read and write from a remote system, map the socket file descriptor to the standard input and standard output and the rest of the program can run unmodified.

It also meant that programmers could use familiar concepts: open a connection, read and write data, close the connection 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) for Unix. STREAMS had a cleaner architecture in some respects, but the socket API had a big advantage: it was already widely deployed. By the time STREAMS was deployed 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

One amazing aspect of the socket API is its consistency across operating systems. The core functions and data structures are essentially the same whether you are programming on Linux, macOS, FreeBSD, or Windows. There are differences between the Unix-derived systems and Windows implementation but it’s like the difference between British English and American English vs. British 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.

There are some differences to be aware of 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)

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

And that’s the long and short of it: Bob’s your uncle, all done and dusted.


The Socket Abstraction

A socket is simply 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.

One thing to keep in mind is that sockets were created as general-purpose interface for communication, not specifically for the Internet. While this made them flexible (you can create local sockets or specify different versions of IP), it can make the setup phases look like they require more information.

When you create a socket, you specify three things:

  1. Address family: This determines the type of addresses the socket will use. AF_INET is for IPv4 addresses, AF_INET6 is for IPv6. There are other families, like AF_UNIX (or AF_LOCAL) for communication between processes on the same machine without going through the network stack or AF_BLUETOOTH for using a bluetooth network.

  2. Socket type: This determines 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: This is 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.

The socket itself does not contain an address when first created. It is just 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

We’ll now go through the programming steps of communicating with TCP and with UDP. We’ll start with the system calls and C examples.

Even if you don’t plan to write any networking code in C (you might at some point, when performance matters), it’s useful to see exactly what you get from the operating system. With the C examples, we’re accessing the system calls directly. Other frameworks, like Java and Python, create abstractions that simplify the code but also hide some of the details.

These were written and tested on a Raspberry Pi5 Debian Linux system but should work on any Linux system and will likely compile on a Mac too (but sometimes I found that porting some code on the Mac might need an extra header file; I didn’t check).

TCP Socket Programming

TCP (Transmission Control Protocol) provides reliable, ordered, connection-oriented communication. “Reliable” here means the TCP stack retransmits lost segments, reorders arrivals, does 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 program has two sockets in play on the server side:

  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 is important because it transforms the socket from 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 specifically for that connection and returns it. The original socket continues listening for additional connections. This is why servers can handle 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 operting system gives us to system services (allocating memory, accessing files, creating processes, etc.). The set of socket system calls is 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. This client is hardcoded to connect to 127.0.0.1 (a special loopback address representing the local host and 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 in some ways. There is no three-way handshake, no connection state to maintain, and no need for listen() or accept(). A UDP server simply 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.

Note that 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 is hardwired to get 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

Like the TCP example, this sends messages to localhost at 127.0.0.1 and 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()}")

You can make this code even shorter because Python’s socket object gives you a method to create the TCP socket and connection in one step, incorporating the socket, bind, and connect system calls:

import socket

HOST = '127.0.0.1'
PORT = 8080

with 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()}")

Note how much less painful this is to code than C. 10 vs. 56 lines for the client and 10 vs. 69 lines for the server.

The reality is that all the ugly C code was just setting things up. In real life, that ends up being a small part 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

Depending on the language and the type of session you’re setting up, you might run into some common errors.

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. This bug can be particularly confusing because the numbers look correct in your code. Java and Python handle this in the implementation of their methods, so byte ordering isn’t something you need to deal with.

Not handling partial reads. TCP is a byte stream; UDP is a message stream. When you call recv() or read(), you are not guaranteed to receive all the data you asked for. The call 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. If you’re using UDP, send() will fail if you try to send a message that’s 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. This is because the TCP connection is in the TIME_WAIT state. Use the SO_REUSEADDR socket option to allow 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, your program will wait indefinitely. Consider using timeouts, non-blocking I/O, or select/poll for production code.


Summary

For TCP servers: create a socket, bind it to an address, call listen(), accept connections, and use send()/recv() to communicate. For TCP clients: create a socket, connect to the server, and use send()/recv().

For UDP: create a socket, optionally bind it (required for servers), and use sendto()/recvfrom() to exchange datagrams. There is no connection establishment.

The same patterns apply whether you are programming in C, Python, Java, or any other language with socket support. The differences are mostly syntactic.