- Published on
Network Programming in C using Sockets Part 2 of 6 Advanced C Topics
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
Ever wondered how applications communicate across the internet or a local network? The magic often happens through sockets. This article dives into the fundamentals of network programming in C using the standard Berkeley Sockets API, empowering you to build applications that can send and receive data across networks. Get ready to create your first client-server application!
Table of Contents
What is Socket Programming?
At its core, network programming involves writing programs that communicate with other programs over a network (like the internet or a local LAN). A socket is one endpoint of a two-way communication link between two programs running on the network. The Sockets API provides a set of functions and data structures that allow programmers to create and manage these communication endpoints.
The most common model used in network programming is the client-server model:
- Server: A program that typically runs continuously, listens for incoming connection requests from clients on a specific network address and port, and provides some service (e.g., serving web pages, managing a database, running a game).
- Client: A program that initiates a connection to a server to request a service or exchange data. Your web browser is a client connecting to web servers.
Core Concepts
Before diving into code, let's grasp some essential concepts:
- IP Addresses & Ports:
- An IP Address (Internet Protocol Address) uniquely identifies a device (computer, server) on a network (e.g.,
192.168.1.100
for IPv4 or2001:0db8:85a3::8a2e:0370:7334
for IPv6). - A Port Number is a 16-bit number (0-65535) that identifies a specific application or service running on that device. Think of the IP address as the building address and the port number as the specific apartment number within that building. Standard services have well-known ports (e.g., HTTP uses port 80, HTTPS uses 443, SSH uses 22).
- An IP Address (Internet Protocol Address) uniquely identifies a device (computer, server) on a network (e.g.,
- Protocols (TCP vs. UDP):
- TCP (Transmission Control Protocol): Connection-oriented, reliable, stream-based. It guarantees that data arrives in order and without errors (or it notifies you of failure). It establishes a connection before data transfer begins. Think of it like a phone call. We'll focus on TCP (
SOCK_STREAM
) in this guide. - UDP (User Datagram Protocol): Connectionless, unreliable, datagram-based. It's faster but doesn't guarantee delivery or order. Think of it like sending postcards – they might get lost or arrive out of sequence. Used for things like video streaming or DNS lookups where speed is critical and occasional loss is acceptable (
SOCK_DGRAM
).
- TCP (Transmission Control Protocol): Connection-oriented, reliable, stream-based. It guarantees that data arrives in order and without errors (or it notifies you of failure). It establishes a connection before data transfer begins. Think of it like a phone call. We'll focus on TCP (
- Byte Order (Endianness): Computers store multi-byte numbers differently (Little-Endian vs. Big-Endian). Networks typically use Big-Endian ("Network Byte Order"). The Sockets API provides functions to convert between host byte order and network byte order:
htons()
: Host to Network Short (16-bit, for ports)htonl()
: Host to Network Long (32-bit, for IPv4 addresses)ntohs()
: Network to Host Shortntohl()
: Network to Host Long- Always use these when putting addresses and ports into socket structures or retrieving them!
- Socket Descriptors: When you create a socket, the operating system returns an integer, similar to a file descriptor (
int socket_fd
). You use this descriptor in subsequent socket function calls to refer to that specific socket.
The TCP Server Workflow
A typical TCP server performs the following steps:
socket()
- Create a Socket: Get a socket descriptor from the OS.bind()
- Assign Address: Assign a specific IP address and port number to the created socket so clients know where to connect.listen()
- Listen for Connections: Mark the socket as a passive socket that will be used to accept incoming connection requests. Define a queue limit (backlog) for pending connections.accept()
- Accept Connections: Wait (block) until a client tries to connect. When a connection occurs,accept()
creates a new socket descriptor dedicated solely to communicating with that specific client. The original listening socket remains open to accept further connections.read()
/recv()
&write()
/send()
- Communicate: Use the new socket descriptor returned byaccept()
to exchange data with the connected client.close()
- Close Connection: Close the client-specific socket descriptor when communication is finished. The server might then loop back toaccept()
another connection or close the main listening socket when shutting down.
The TCP Client Workflow
A typical TCP client performs these steps:
socket()
- Create a Socket: Get a socket descriptor from the OS.connect()
- Establish Connection: Actively try to connect to the server's IP address and port number. The client needs to know these beforehand.write()
/send()
&read()
/recv()
- Communicate: Exchange data with the server using the socket descriptor.close()
- Close Connection: Close the socket descriptor when communication is finished.
Essential Data Structures and Headers
You'll primarily need these headers:
#include <stdio.h> // Standard I/O
#include <stdlib.h> // Standard library (exit)
#include <string.h> // String manipulation (bzero, strlen)
#include <unistd.h> // Unix standard functions (read, write, close)
#include <sys/socket.h> // Core socket functions and data structures
#include <netinet/in.h> // Structures for internet addresses (sockaddr_in)
#include <arpa/inet.h> // Functions for manipulating IP addresses (inet_pton)
#include <errno.h> // For error number variables (like errno)
The key structure for IPv4 addresses is struct sockaddr_in
:
struct sockaddr_in {
short sin_family; // Address family, AF_INET for IPv4
unsigned short sin_port; // Port number (needs to be in Network Byte Order!)
struct in_addr sin_addr; // IP address (needs to be in Network Byte Order!)
char sin_zero[8]; // Padding, set to zero
};
struct in_addr {
unsigned long s_addr; // 32-bit IPv4 address
};
Simple TCP Server Example (Iterative)
This server listens on port 8080, accepts one connection at a time, reads a message, sends a reply, and closes the connection.
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void error_exit(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
int main() {
int listen_fd, conn_fd; // Listening socket, connection socket
struct sockaddr_in serv_addr, cli_addr;
socklen_t cli_len;
char buffer[BUFFER_SIZE];
int n;
// 1. Create socket
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
error_exit("ERROR opening socket");
}
printf("Socket created successfully.\n");
// Initialize server address structure
memset(&serv_addr, 0, sizeof(serv_addr)); // Use memset for portability
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Listen on any available interface
serv_addr.sin_port = htons(PORT); // Set port (Network Byte Order)
// 2. Bind socket to address and port
if (bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
close(listen_fd);
error_exit("ERROR on binding");
}
printf("Binding successful on port %d.\n", PORT);
// 3. Listen for incoming connections
if (listen(listen_fd, 5) < 0) { // 5 is the backlog queue size
close(listen_fd);
error_exit("ERROR on listen");
}
printf("Server listening...\n");
while (1) { // Loop to accept multiple connections sequentially
cli_len = sizeof(cli_addr);
// 4. Accept a connection
// accept() blocks until a client connects
conn_fd = accept(listen_fd, (struct sockaddr *)&cli_addr, &cli_len);
if (conn_fd < 0) {
perror("ERROR on accept"); // Don't exit, just report and try again
continue; // Continue to next iteration to accept another connection
}
// Get client IP address and port for logging
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Connection accepted from %s:%d\n", client_ip, ntohs(cli_addr.sin_port));
// --- Communication with the connected client ---
memset(buffer, 0, BUFFER_SIZE);
// 5. Read data from client
n = read(conn_fd, buffer, BUFFER_SIZE - 1); // Read up to BUFFER_SIZE-1 bytes
if (n < 0) {
perror("ERROR reading from socket");
} else if (n == 0) {
printf("Client disconnected.\n");
}
else {
buffer[n] = '\0'; // Null-terminate the received string
printf("Received message: %s\n", buffer);
// 5. Write response to client
const char *response = "Message received by server.";
n = write(conn_fd, response, strlen(response));
if (n < 0) {
perror("ERROR writing to socket");
} else {
printf("Response sent.\n");
}
}
// 6. Close the connection socket for this client
close(conn_fd);
printf("Connection closed.\n\n");
} // End of while loop
// Close the listening socket (optional here as we loop forever, but good practice)
// close(listen_fd);
// return 0;
}
Simple TCP Client Example
This client connects to 127.0.0.1
(localhost) on port 8080, sends a message, reads the reply, and exits.
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define SERVER_IP "127.0.0.1" // Loopback address for testing on the same machine
#define PORT 8080
#define BUFFER_SIZE 1024
void error_exit(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
int main() {
int sock_fd;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE];
int n;
// 1. Create socket
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
error_exit("ERROR opening socket");
}
printf("Socket created successfully.\n");
// Initialize server address structure
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT); // Server port (Network Byte Order)
// Convert IPv4 address from text to binary form
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
close(sock_fd);
error_exit("Invalid address/ Address not supported");
}
// 2. Connect to the server
printf("Connecting to server %s:%d...\n", SERVER_IP, PORT);
if (connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
close(sock_fd);
error_exit("ERROR connecting");
}
printf("Connected successfully.\n");
// --- Communication ---
// 3. Send message to server
const char *message = "Hello from C client!";
printf("Sending message: %s\n", message);
n = write(sock_fd, message, strlen(message));
if (n < 0) {
close(sock_fd);
error_exit("ERROR writing to socket");
}
// 3. Read response from server
memset(buffer, 0, BUFFER_SIZE);
printf("Waiting for server response...\n");
n = read(sock_fd, buffer, BUFFER_SIZE - 1);
if (n < 0) {
close(sock_fd);
error_exit("ERROR reading from socket");
} else if (n == 0) {
printf("Server closed connection unexpectedly.\n");
}
else {
buffer[n] = '\0'; // Null-terminate
printf("Server replied: %s\n", buffer);
}
// 4. Close the socket
close(sock_fd);
printf("Connection closed.\n");
return 0;
}
Compiling and Running
Save the code above as server.c
and client.c
. Compile them using gcc (no special libraries needed for basic sockets on Linux/macOS beyond the standard C library):
gcc server.c -o server
gcc client.c -o client
Now, run them:
Open a terminal window and run the server:
./server
You should see output indicating it's listening.
Open a second terminal window and run the client:
./client
The client will connect, send its message, receive the reply, print it, and exit.
Look back at the server terminal. You should see messages indicating a connection was accepted, the message received, the response sent, and the connection closed. The server will then go back to listening for the next connection. You can run the client again.
Error Handling
Network programming is prone to errors (network down, server busy, host unreachable, etc.). Always check the return values of socket functions.
- Most return
-1
on error and set the globalerrno
variable. - Use
perror("Descriptive message")
to print the system error message corresponding toerrno
. - Handle errors gracefully – close opened sockets, free resources, and decide whether to retry, abort, or log the error.
Beyond the Basics: Next Steps
This iterative server can only handle one client at a time. Real-world servers need to handle multiple clients concurrently. This typically involves:
- Multi-threading: Create a new thread (using pthreads, as discussed in our previous article) for each accepted client connection.
- Multi-processing: Create a new process (
fork()
) for each client connection. - Non-blocking I/O and Multiplexing: Use functions like
select()
,poll()
, orepoll()
(on Linux) to manage multiple connections within a single thread without blocking.
Other advanced topics include using UDP (SOCK_DGRAM
), implementing secure communication using SSL/TLS (OpenSSL), handling partial reads/writes, and designing robust application-level protocols.
Conclusion
You've taken your first steps into the exciting world of network programming in C! You now understand the client-server model, the roles of IP addresses and ports, the basics of TCP communication, and the core socket API functions (socket
, bind
, listen
, accept
, connect
, read
, write
, close
). While our examples were simple, they form the foundation for building complex networked applications, from web servers and chat applications to distributed computing systems. Remember to handle errors diligently and consider concurrency models for real-world applications.
Suggested Reading:
- (Link to Previous C Series): Refresh your C fundamentals with our Getting Started with C Series and Intermediate C Concepts.
- (Link to Multi-threading Article): Learn how to handle multiple clients concurrently using Multi-threading in C with POSIX Threads.
- (Link to Debugging Article): Debugging networked applications can be tricky. See Debugging C Programs Effectively with GDB.