Socket operations can be performed in either blocking or non-blocking mode, each with its own advantages and challenges. Understanding these modes is crucial for designing efficient and responsive networked applications.
By default, socket operations in most systems are blocking. This means that when a program calls a socket function like accept(), connect(), send(), or recv(), the function does not return until the operation completes or an error occurs.
In this example, the accept(), recv(), and send() calls are all blocking. The program will wait at each of these calls until the operation completes or an error occurs.
Non-blocking sockets allow operations to return immediately, even if they cannot be completed right away. This enables a program to perform other tasks while waiting for socket operations to complete.
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <fcntl.h>#include <errno.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#define PORT 8080#define BUFFER_SIZE 1024// Set socket to non-blocking modeint set_nonblocking(int sockfd) { int flags = fcntl(sockfd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); return -1; } if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL O_NONBLOCK"); return -1; } return 0;}int main() { int server_fd, client_fd = -1; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); char buffer[BUFFER_SIZE]; // Create socket server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } // Set socket to non-blocking mode if (set_nonblocking(server_fd) < 0) { close(server_fd); exit(EXIT_FAILURE); } // Initialize server 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); // Bind socket if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("Bind failed"); close(server_fd); exit(EXIT_FAILURE); } // Listen for connections if (listen(server_fd, 5) < 0) { perror("Listen failed"); close(server_fd); exit(EXIT_FAILURE); } printf("Server listening on port %d (non-blocking mode)...\n", PORT); // Main loop while (1) { // Try to accept a connection (non-blocking) if (client_fd < 0) { client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // No connection available, do other work printf("No connection available, doing other work...\n"); sleep(1); continue; } else { perror("Accept failed"); close(server_fd); exit(EXIT_FAILURE); } } // Set client socket to non-blocking mode if (set_nonblocking(client_fd) < 0) { close(client_fd); client_fd = -1; continue; } printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } // Try to receive data (non-blocking) ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE - 1, 0); if (bytes_received < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // No data available, do other work printf("No data available, doing other work...\n"); sleep(1); continue; } else { perror("Receive failed"); close(client_fd); client_fd = -1; continue; } } else if (bytes_received == 0) { // Connection closed by client printf("Client disconnected\n"); close(client_fd); client_fd = -1; continue; } buffer[bytes_received] = '\0'; printf("Received: %s\n", buffer); // Try to send response (non-blocking) const char *response = "Message received"; ssize_t bytes_sent = send(client_fd, response, strlen(response), 0); if (bytes_sent < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // Cannot send now, try again later printf("Cannot send now, will try again...\n"); sleep(1); continue; } else { perror("Send failed"); close(client_fd); client_fd = -1; continue; } } printf("Response sent\n"); // Close the connection after one message close(client_fd); client_fd = -1; } close(server_fd); return 0;}
In this example, the accept(), recv(), and send() calls are all non-blocking. When an operation would block, it returns immediately with an error code (EAGAIN or EWOULDBLOCK), allowing the program to perform other tasks and try again later.
These error codes (which are typically the same value) indicate that the operation cannot be completed immediately and would block if the socket were in blocking mode.
ssize_t bytes_sent = send(sockfd, buffer, length, 0);if (bytes_sent < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // Operation would block, try again later return 0; } else { // Real error perror("send failed"); return -1; }}
Even with blocking sockets, you can set timeouts to prevent operations from blocking indefinitely:
// Set receive timeout to 5 secondsstruct timeval tv;tv.tv_sec = 5;tv.tv_usec = 0;setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));// Now recv() will return with an error if no data is received within 5 secondsssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);if (bytes_received < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // Timeout occurred printf("Receive timeout\n"); } else { // Real error perror("recv failed"); }}
Both blocking and non-blocking sockets have their place in network programming. Blocking sockets are simpler to use but can limit responsiveness, while non-blocking sockets offer greater flexibility at the cost of increased complexity.
For simple applications or those with dedicated threads per connection, blocking sockets are often sufficient. For high-performance servers handling many connections, non-blocking sockets combined with event notification mechanisms like select(), poll(), or epoll() provide better scalability.
In the next section, we'll explore these event notification mechanisms in detail and learn how to implement efficient multiplexing with non-blocking sockets.
Test Your Knowledge
Take a quiz to reinforce what you've learned
Exam Preparation
Access short and long answer questions for written exams