Welcome to the final article in our Socket Programming in C series! So far, we have explored the fundamental concepts of network communication and learned how to build basic client-server applications using C sockets. We covered the core functions: socket()
, bind()
, listen()
, accept()
, connect()
, send()
, recv()
, and close()
(or closesocket()
on Windows).
While these functions are the building blocks, real-world network applications demand more. They need to be robust, efficient, and capable of gracefully handling the inevitable failures that occur in dynamic network environments. This article will equip you with two crucial sets of tools to achieve this:
Advanced Socket Options: To fine-tune socket behavior, optimize performance, and manage resource usage.
Robust Error Handling: To anticipate, detect, and gracefully recover from network failures, unexpected client behavior, or system issues.
This knowledge will elevate your C socket programming skills, enabling you to write more professional and reliable network applications.
setsockopt()
and getsockopt()
The setsockopt()
function allows you to set various options for a socket, influencing its behavior in ways that aren't possible with the basic socket creation. Conversely, getsockopt()
retrieves the current value of a specific option. These functions provide a powerful mechanism to control your socket's behavior at a deeper level.
#include <sys/socket.h> // For POSIX systems
#include <winsock2.h> // For Windows systems
// For setting options
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// For getting options
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
sockfd
: The integer socket descriptor on which to set or get the option.
level
: Specifies the protocol level at which the option resides. Common levels include:
SOL_SOCKET
: For socket-level options that apply to the socket itself.IPPROTO_TCP
: For TCP-specific options.IPPROTO_IP
: For IP-level options.IPPROTO_IPV6
: For IPv6-level options.optname
: The name of the option to set or get. These are predefined constants (e.g., SO_REUSEADDR
, TCP_NODELAY
).
optval
: A pointer to a buffer. For setsockopt()
, this buffer contains the value you want to set. For getsockopt()
, the option's current value will be stored here.
optlen
: The size of the optval
buffer in bytes. For getsockopt()
, it's a pointer to a socklen_t
that specifies the size of the buffer initially and is updated to the actual size of the option value upon return.
Both functions return 0
on success, and -1
(POSIX) or SOCKET_ERROR
(Windows) on failure. Always check the return value!
SO_REUSEADDR
(Level: SOL_SOCKET
)Problem it solves: A very common frustration for network programmers is the "Address already in use" error when trying to restart a server application immediately after it has been shut down. This happens because, after a socket is closed, the operating system often keeps the local address and port in a TIME_WAIT
state for a period (typically 30-120 seconds) to ensure that any delayed packets for the previous connection are cleared from the network. SO_REUSEADDR
allows you to reuse a local address that is in the TIME_WAIT
state.
Value Type: An int
(1
for true/enable, 0
for false/disable).
// Example: Setting SO_REUSEADDR on a listening socket
int optval = 1;
if (setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
// Error handling
perror("setsockopt(SO_REUSEADDR)");
// Or on Windows: WSAGetLastError();
}
Usage: This option should typically be set on the listening socket before calling bind()
.
SO_SNDBUF
and SO_RCVBUF
(Level: SOL_SOCKET
)Purpose: These options control the size of the send and receive buffers associated with a socket. Data sent via send()
is copied into the send buffer, and data received via recv()
is read from the receive buffer. Larger buffers can potentially improve performance for high-throughput applications or over high-latency networks by allowing more data to be in transit without blocking, but they also consume more system memory.
Value Type: An int
(the desired buffer size in bytes).
// Example: Setting SO_SNDBUF and SO_RCVBUF
int send_buffer_size = 65536; // 64 KB
int recv_buffer_size = 131072; // 128 KB
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &send_buffer_size, sizeof(send_buffer_size)) == -1) {
perror("setsockopt(SO_SNDBUF)");
}
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buffer_size, sizeof(recv_buffer_size)) == -1) {
perror("setsockopt(SO_RCVBUF)");
}
// You can also retrieve the current buffer sizes
socklen_t optlen;
int actual_send_buffer_size;
optlen = sizeof(actual_send_buffer_size);
if (getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &actual_send_buffer_size, &optlen) == 0) {
printf("Actual Send Buffer Size: %d bytes\n", actual_send_buffer_size);
}
Note: The operating system might not set the buffer size to exactly what you request; it might round it up or down to align with system memory pages. getsockopt()
can confirm the actual size.
TCP_NODELAY
(Level: IPPROTO_TCP
)Purpose: This option is specific to TCP sockets and controls Nagle's algorithm. Nagle's algorithm is a mechanism designed to improve the efficiency of TCP/IP networks by reducing the number of small packets. It buffers small amounts of data before sending them, waiting for more data to accumulate or for an acknowledgment of previously sent data. While good for overall network throughput, it can introduce small delays (latency) for interactive applications that send small, frequent bursts of data (like telnet clients, gaming, or remote desktop). Setting TCP_NODELAY
disables Nagle's algorithm, causing data to be sent immediately.
Value Type: An int
(1
for true/enable, 0
for false/disable).
// Example: Disabling Nagle's algorithm for lower latency
int optval = 1; // 1 to enable TCP_NODELAY (disable Nagle's)
if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval)) == -1) {
perror("setsockopt(TCP_NODELAY)");
}
Usage: Use judiciously. Disable Nagle's only if you prioritize low latency over network efficiency for small packets.
SO_KEEPALIVE
(Level: SOL_SOCKET
)Purpose: Enables the transmission of keep-alive messages on an idle TCP connection. If the connection remains idle for a defined period (which is system-dependent but typically hours), the operating system will send a small probe packet to the peer. If the peer doesn't respond (e.g., due to a crash or network outage), the connection will be terminated, and subsequent socket operations will return an error (e.g., `ETIMEDOUT` or `ECONNRESET`). This helps prevent "half-open" or "zombie" connections.
Value Type: An int
(1
for true/enable, 0
for false/disable).
// Example: Enabling TCP Keep-Alive
int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1) {
perror("setsockopt(SO_KEEPALIVE)");
}
Note: While `SO_KEEPALIVE` enables the feature, the actual timing parameters (idle time, interval between probes, number of probes) are typically configured at the OS level. Linux offers more granular `TCP_KEEPIDLE`, `TCP_KEEPINTVL`, `TCP_KEEPCNT` options at the `IPPROTO_TCP` level for per-socket control.
SO_RCVTIMEO
and SO_SNDTIMEO
(Level: SOL_SOCKET
)Purpose: These options allow you to set a timeout value for blocking recv()
and send()
operations, respectively. If no data is available to be read (for recv()
) or the buffer cannot accept data (for send()
) within the specified timeout period, the function will return an error (typically `EAGAIN` or `EWOULDBLOCK` on POSIX, or `WSAETIMEDOUT` on Windows), rather than blocking indefinitely.
Value Type: A struct timeval
(which contains tv_sec
for seconds and tv_usec
for microseconds).
// Example: Setting a 5-second receive timeout
struct timeval tv;
tv.tv_sec = 5; // 5 seconds
tv.tv_usec = 0; // 0 microseconds
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) {
perror("setsockopt(SO_RCVTIMEO)");
}
// Example: Setting a 10-second send timeout
tv.tv_sec = 10;
if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) == -1) {
perror("setsockopt(SO_SNDTIMEO)");
}
Usage: Crucial for building responsive applications that don't hang if a peer becomes unresponsive. It's often used as an alternative or in conjunction with non-blocking I/O and multiplexing functions like `select()` or `poll()`.
Network operations are inherently unreliable. Connections can drop, hosts can go down, firewalls can block, and system resources can become exhausted. Writing robust socket programs requires anticipating, detecting, and gracefully recovering from these failures. Neglecting error checking is a sure path to unstable and crashing applications.
On POSIX-compliant systems (like Linux, macOS), network function failures set a global integer variable called errno
. You can then use functions to get human-readable descriptions of these error codes.
perror()
The simplest way to print an error message based on errno
. It prints the string you provide, followed by a colon, a space, and then the system-defined error message for the current errno
value, followed by a newline.
#include <errno.h> // For errno
#include <stdio.h> // For perror
// ... after a failed socket call, e.g., connect()
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
perror("Error connecting to server");
// Handle the error (e.g., exit, retry, log)
}
strerror()
This function takes an error number (like `errno`) as input and returns a pointer to a string describing the error. This is useful if you want to integrate the error message into a more complex logging system or a custom error reporting mechanism.
#include <errno.h> // For errno
#include <string.h> // For strerror
#include <stdio.h>
// ... after a failed socket call
if (send(sockfd, data, data_len, 0) == -1) {
fprintf(stderr, "Failed to send data: %s\n", strerror(errno));
// Handle the error
}
Windows Winsock uses its own error codes, which are retrieved using WSAGetLastError()
. To get a human-readable string, you typically use the Windows API function FormatMessage()
.
WSAGetLastError()
Returns the last error code that occurred for the calling thread's Winsock API call.
#include <winsock2.h> // For WSAGetLastError
#include <stdio.h>
// ... after a failed socket call
int lastError = WSAGetLastError();
if (lastError != 0) {
fprintf(stderr, "Winsock error code: %d\n", lastError);
// Use FormatMessage to get the description
}
FormatMessage()
This is a more general Windows API function that can retrieve error descriptions for various system errors, including Winsock errors.
#include <winsock2.h>
#include <windows.h> // For FormatMessage, LPSTR, etc.
#include <stdio.h>
void print_winsock_error(const char* message) {
int error_code = WSAGetLastError();
LPSTR error_msg = NULL; // LPSTR is a char* on Windows
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
error_code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&error_msg,
0, NULL);
fprintf(stderr, "%s: %s (Error Code: %d)\n", message, (error_msg != NULL ? error_msg : "Unknown error"), error_code);
if (error_msg != NULL) {
LocalFree(error_msg); // Free the buffer allocated by FormatMessage
}
}
// Usage:
// if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
// print_winsock_error("WSAStartup failed");
// return 1;
// }
// if (connect(sockfd, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) {
// print_winsock_error("Error connecting to server");
// // Handle error
// }
EACCES
/ WSAEACCES
(Permission Denied): Often occurs when trying to bind()
to a privileged port (ports less than 1024) without sufficient permissions (e.g., not running as root/administrator).
EADDRINUSE
/ WSAEADDRINUSE
(Address Already In Use): Discussed previously. Indicates that the local address/port combination is already in use, usually by a socket in TIME_WAIT
state. Use `SO_REUSEADDR`.
ECONNREFUSED
/ WSAECONNREFUSED
(Connection Refused): The target machine actively refused the connection. This usually means no server is listening on the specified port, a firewall blocked the connection, or the server backlog queue is full.
ETIMEDOUT
/ WSAETIMEDOUT
(Connection Timed Out): The connection attempt failed because the remote host did not respond within a specified time limit. The server might be down, the IP address could be wrong, or there's a severe network outage.
EPIPE
/ WSAESHUTDOWN
(Broken Pipe / Socket Shutdown): Occurs when trying to write data to a socket whose read end has been closed by the peer. It signals that the connection is no longer valid for sending.
ECONNRESET
/ WSAECONNRESET
(Connection Reset by Peer): The remote host unexpectedly closed the connection. This could happen if the peer's application crashed, or if a firewall sent a "reset" packet. Subsequent reads/writes will fail.
EAGAIN
/ EWOULDBLOCK
(Resource Temporarily Unavailable): If using non-blocking sockets, this means that a `send()` or `recv()` call would block, but because the socket is non-blocking, it returns immediately with this error. You should then retry the operation later (e.g., using `select()` or `poll()`).
EINTR
(Interrupted System Call): A system call (like `accept()`, `read()`, `write()`) was interrupted by a signal handler. It means the operation didn't complete but didn't necessarily fail. You can often safely retry the call.
shutdown()
vs. close()
/ closesocket()
You have used close()
(or closesocket()
on Windows) to terminate socket connections. While this works, it performs an abrupt release of resources. For TCP, a more controlled shutdown is often desirable to ensure all data in transit is delivered and received. This is where shutdown()
comes in.
close()
/ closesocket()
This function marks the socket descriptor as invalid and releases any system resources associated with it. If there's still data in the socket's send buffer, the OS *tries* to send it. However, if the peer has already closed its receiving end, this can lead to unacknowledged data being lost. It performs a full close of both read and write halves.
shutdown()
shutdown()
allows for a graceful, partial, or full closure of a TCP connection. It notifies the peer that you are no longer sending or receiving data, or both, while keeping the socket descriptor valid until `close()` is called.
int shutdown(int sockfd, int how);
Parameters:
sockfd
: The socket descriptor.how
: Specifies the type of shutdown:
SHUT_RD
(POSIX) / SD_RECEIVE
(Windows): Disables further receives on the socket. Any data already in the receive buffer can still be read.SHUT_WR
(POSIX) / SD_SEND
(Windows): Disables further sends on the socket. Any data currently in the send buffer is sent, followed by a FIN (Finish) packet. The peer will receive an EOF (end-of-file) when it tries to read.SHUT_RDWR
(POSIX) / SD_BOTH
(Windows): Disables both sends and receives. Equivalent to calling `shutdown` with `SHUT_RD` and `SHUT_WR` sequentially.Best Practice for Graceful Closure (Server Side):
When a server wants to stop sending data to a client but still wants to receive any remaining data from the client (e.g., a "goodbye" message or final data chunks), a common pattern is:
shutdown(sockfd, SHUT_WR)
: This sends a FIN packet to the client, indicating that the server will not send any more data. The client's recv()
will eventually return 0, signaling EOF.recv()
until it returns 0: This ensures the server reads all remaining data the client might send before it closes its side.close(sockfd)
: Finally, close the socket descriptor to release resources.This sequence allows for a clean negotiation of connection termination, preventing data loss.
Let us revisit a simple echo server and client, enhancing them with some of the advanced socket options and robust error handling we have discussed. This example will illustrate how to integrate these concepts into practical C code.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h> // For modern Winsock functions
#pragma comment(lib, "ws2_32.lib") // Link with ws2_32.lib
#define PRINTERROR(msg) print_winsock_error(msg)
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define PRINTERROR(msg) perror(msg)
#endif
// Universal close for sockets
#ifdef _WIN32
#define CLOSE_SOCKET(s) closesocket(s)
#else
#define CLOSE_SOCKET(s) close(s)
#endif
void print_error_and_exit(const char* message) {
#ifdef _WIN32
int error_code = WSAGetLastError();
LPSTR error_msg = NULL;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&error_msg, 0, NULL);
fprintf(stderr, "%s: %s (Error Code: %d)\n", message, (error_msg != NULL ? error_msg : "Unknown error"), error_code);
if (error_msg != NULL) LocalFree(error_msg);
WSACleanup();
#else
perror(message);
#endif
exit(EXIT_FAILURE);
}
server.c
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
// Define POSIX-like constants for consistency
#define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <sys/time.h> // For struct timeval
#endif
// Include the common error handling and close functions
// (In a real project, this would be in a separate .h file)
// For this example, we'll assume it's copy-pasted or included from the above snippet.
#ifdef _WIN32
void print_winsock_error(const char* message); // Forward declaration
#define PRINTERROR(msg) print_winsock_error(msg)
#define CLOSE_SOCKET(s) closesocket(s)
void print_error_and_exit(const char* message);
#else
#define PRINTERROR(msg) perror(msg)
#define CLOSE_SOCKET(s) close(s)
void print_error_and_exit(const char* message);
#endif
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int listen_sockfd, client_sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
int bytes_read;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
print_error_and_exit("WSAStartup failed");
}
#endif
// 1. Create socket
listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd == -1) {
print_error_and_exit("Failed to create socket");
}
// Set SO_REUSEADDR option
int optval = 1;
if (setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&optval, sizeof(optval)) == -1) {
print_error_and_exit("setsockopt(SO_REUSEADDR) failed");
}
printf("SO_REUSEADDR set on listening socket.\n");
// Set a receive timeout for the listening socket (optional, for accept() timeout)
struct timeval tv;
tv.tv_sec = 60; // 60 seconds timeout for accept()
tv.tv_usec = 0;
if (setsockopt(listen_sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) == -1) {
print_error_and_exit("setsockopt(SO_RCVTIMEO) for accept failed");
}
printf("SO_RCVTIMEO set on listening socket (for accept timeout).\n");
// Configure server address
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // Listen on all available interfaces
server_addr.sin_port = htons(PORT);
// 2. Bind socket to address and port
if (bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
print_error_and_exit("Failed to bind socket");
}
// 3. Listen for incoming connections
if (listen(listen_sockfd, 5) == -1) { // 5 is the backlog queue size
print_error_and_exit("Failed to listen on socket");
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// 4. Accept a client connection
client_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_sockfd == -1) {
#ifdef _WIN32
if (WSAGetLastError() == WSAETIMEDOUT) {
printf("Accept timed out, trying again...\n");
continue; // Continue to next loop iteration
}
#else
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("Accept timed out, trying again...\n");
continue; // Continue to next loop iteration
}
#endif
print_error_and_exit("Failed to accept client connection");
}
printf("Connection accepted from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// Set TCP_NODELAY on client socket for responsiveness
optval = 1;
if (setsockopt(client_sockfd, IPPROTO_TCP, TCP_NODELAY, (const char*)&optval, sizeof(optval)) == -1) {
PRINTERROR("setsockopt(TCP_NODELAY) failed on client socket");
} else {
printf("TCP_NODELAY set on client socket.\n");
}
// Set SO_KEEPALIVE on client socket
optval = 1;
if (setsockopt(client_sockfd, SOL_SOCKET, SO_KEEPALIVE, (const char*)&optval, sizeof(optval)) == -1) {
PRINTERROR("setsockopt(SO_KEEPALIVE) failed on client socket");
} else {
printf("SO_KEEPALIVE set on client socket.\n");
}
// Set a receive timeout for data on the client socket
tv.tv_sec = 10; // 10 seconds timeout for recv()
tv.tv_usec = 0;
if (setsockopt(client_sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) == -1) {
PRINTERROR("setsockopt(SO_RCVTIMEO) for client recv failed");
} else {
printf("SO_RCVTIMEO set on client socket.\n");
}
// Handle client communication
while ((bytes_read = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[bytes_read] = '\0';
printf("Received from client: %s", buffer); // Assuming client sends newline
// Echo back to client
if (send(client_sockfd, buffer, bytes_read, 0) == -1) {
PRINTERROR("Failed to send data to client");
break; // Exit loop on send error
}
}
// Check why recv loop exited
if (bytes_read == 0) {
printf("Client disconnected gracefully.\n");
} else if (bytes_read == -1) {
#ifdef _WIN32
int error = WSAGetLastError();
if (error == WSAETIMEDOUT) {
printf("Client recv timed out. Closing connection.\n");
} else if (error == WSAECONNRESET) {
printf("Client connection reset by peer.\n");
} else {
print_winsock_error("Error receiving from client");
}
#else
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("Client recv timed out. Closing connection.\n");
} else if (errno == ECONNRESET) {
printf("Client connection reset by peer.\n");
} else if (errno == EBADF) { // Socket descriptor not valid, likely already closed by peer
printf("Socket was closed by client or invalid descriptor.\n");
}
else {
perror("Error receiving from client");
}
#endif
}
// Graceful shutdown: Tell client we're done sending, then close.
printf("Shutting down client_sockfd for sending...\n");
if (shutdown(client_sockfd, SHUT_WR) == -1) {
PRINTERROR("shutdown(SHUT_WR) failed on client_sockfd");
}
// Optional: Read any last bytes from client until EOF
while ((bytes_read = recv(client_sockfd, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[bytes_read] = '\0';
printf("Received final bytes from client before full close: %s", buffer);
}
if (bytes_read == -1) {
PRINTERROR("Error during final recv before close");
}
printf("Closing client socket.\n");
CLOSE_SOCKET(client_sockfd);
}
// Close listening socket (server rarely reaches here in infinite loop)
printf("Closing listening socket.\n");
CLOSE_SOCKET(listen_sockfd);
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}
// Windows-specific error printing helper
#ifdef _WIN32
void print_winsock_error(const char* message) {
int error_code = WSAGetLastError();
LPSTR error_msg = NULL;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&error_msg, 0, NULL);
fprintf(stderr, "%s: %s (Error Code: %d)\n", message, (error_msg != NULL ? error_msg : "Unknown error"), error_code);
if (error_msg != NULL) LocalFree(error_msg);
}
#endif
client.c
)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
#define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH
#else
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <sys/time.h>
#endif
// Include the common error handling and close functions
#ifdef _WIN32
void print_winsock_error(const char* message); // Forward declaration
#define PRINTERROR(msg) print_winsock_error(msg)
#define CLOSE_SOCKET(s) closesocket(s)
void print_error_and_exit(const char* message);
#else
#define PRINTERROR(msg) perror(msg)
#define CLOSE_SOCKET(s) close(s)
void print_error_and_exit(const char* message);
#endif
#define SERVER_IP "127.0.0.1" // Loopback address for local testing
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE];
int bytes_received;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
print_error_and_exit("WSAStartup failed");
}
#endif
// 1. Create socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
print_error_and_exit("Failed to create socket");
}
// Set TCP_NODELAY on client socket for responsiveness
int optval = 1;
if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (const char*)&optval, sizeof(optval)) == -1) {
PRINTERROR("setsockopt(TCP_NODELAY) failed on client socket");
} else {
printf("TCP_NODELAY set on client socket.\n");
}
// Set SO_RCVTIMEO for client's recv calls
struct timeval tv;
tv.tv_sec = 5; // 5 seconds timeout
tv.tv_usec = 0;
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)) == -1) {
PRINTERROR("setsockopt(SO_RCVTIMEO) failed on client socket");
} else {
printf("SO_RCVTIMEO set on client socket.\n");
}
// Configure server address
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
#ifdef _WIN32
serv_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // Windows specific
#else
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
print_error_and_exit("Invalid address/ Address not supported");
}
#endif
// 2. Connect to server
printf("Attempting to connect to %s:%d...\n", SERVER_IP, PORT);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
print_error_and_exit("Failed to connect to server");
}
printf("Connected to server.\n");
while (1) {
printf("Enter message to send (type 'quit' to exit): ");
if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
printf("Error reading input or EOF.\n");
break;
}
// Remove newline character if present
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "quit") == 0) {
printf("Quitting...\n");
break; // Exit loop
}
// Send data to server
if (send(sockfd, buffer, strlen(buffer), 0) == -1) {
PRINTERROR("Failed to send data");
break; // Exit loop on send error
}
// Receive echo from server
bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
printf("Received from server: %s\n", buffer);
} else if (bytes_received == 0) {
printf("Server disconnected.\n");
break;
} else { // bytes_received == -1
#ifdef _WIN32
int error = WSAGetLastError();
if (error == WSAETIMEDOUT) {
printf("Receive timed out. No response from server.\n");
} else if (error == WSAECONNRESET) {
printf("Server connection reset by peer.\n");
break;
} else {
print_winsock_error("Error receiving data");
}
#else
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("Receive timed out. No response from server.\n");
} else if (errno == ECONNRESET) {
printf("Server connection reset by peer.\n");
break;
} else {
perror("Error receiving data");
}
#endif
}
}
// Graceful shutdown: Tell server we're done sending, then close.
printf("Shutting down client_sockfd for sending...\n");
if (shutdown(sockfd, SHUT_WR) == -1) {
PRINTERROR("shutdown(SHUT_WR) failed on client_sockfd");
}
// Optional: Read any last bytes from server until EOF before full close
while ((bytes_received = recv(sockfd, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[bytes_received] = '\0';
printf("Received final bytes from server before full close: %s\n", buffer);
}
if (bytes_received == -1) {
PRINTERROR("Error during final recv before close");
}
printf("Closing client socket.\n");
CLOSE_SOCKET(sockfd);
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}
// Windows-specific error printing helper
#ifdef _WIN32
void print_winsock_error(const char* message) {
int error_code = WSAGetLastError();
LPSTR error_msg = NULL;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&error_msg, 0, NULL);
fprintf(stderr, "%s: %s (Error Code: %d)\n", message, (error_msg != NULL ? error_msg : "Unknown error"), error_code);
if (error_msg != NULL) LocalFree(error_msg);
}
#endif
// Generic error and exit function (for POSIX and Windows main calls)
#ifndef _WIN32 // Only define if not Windows (already defined in server/client for consistency)
void print_error_and_exit(const char* message) {
perror(message);
exit(EXIT_FAILURE);
}
#endif
On Linux/macOS (using GCC):
gcc server.c -o server -Wall
gcc client.c -o client -Wall
Then, in one terminal:
./server
And in another terminal:
./client
On Windows (using MinGW-w64 or Visual Studio's cl.exe):
Using MinGW-w64 (GCC for Windows):
gcc server.c -o server.exe -lws2_32 -Wall
gcc client.c -o client.exe -lws2_32 -Wall
Using Visual Studio's `cl.exe` (from Developer Command Prompt):
cl server.c ws2_32.lib
cl client.c ws2_32.lib
Then, in one command prompt:
server.exe
And in another command prompt:
client.exe
Experiment with stopping the server abruptly, or disconnecting the client mid-send to see the error handling in action. Observe the `SO_REUSEADDR` effect by quickly restarting the server.
Congratulations! You have reached the end of our Socket Programming in C series. You have progressed from the fundamental concepts of network communication to understanding how to build robust, efficient, and reliable socket applications.
This concluding article introduced you to the power of setsockopt()
and getsockopt()
for fine-tuning socket behavior, optimizing performance, and managing the lifecycle of your connections. More importantly, we have emphasized the critical importance of comprehensive error handling, equipping you with the tools (perror()
, strerror()
, WSAGetLastError()
, FormatMessage()
) and strategies to gracefully deal with the myriad of issues that can arise in network programming.
Armed with this knowledge, you are now well-prepared to tackle more complex network programming challenges. Remember that practice is key. Experiment with different options, simulate error conditions, and build more sophisticated applications. The world of networked applications is vast and exciting, and your in-depth understanding of C sockets is a powerful foundation.
We hope this series has been a valuable resource in your journey to master C and network programming. Happy coding!
How to move your Email accounts from one hosting provider to another without losing any mails?
How to resolve the issue of receiving same email message multiple times when using Outlook?
Self Referential Data Structure in C - create a singly linked list
Mosquito Demystified - interesting facts about mosquitoes
Elements of the C Language - Identifiers, Keywords, Data types and Data objects
How to pass Structure as a parameter to a function in C?
Rajeev Kumar is the primary author of How2Lab. He is a B.Tech. from IIT Kanpur with several years of experience in IT education and Software development. He has taught a wide spectrum of people including fresh young talents, students of premier engineering colleges & management institutes, and IT professionals.
Rajeev has founded Computer Solutions & Web Services Worldwide. He has hands-on experience of building variety of websites and business applications, that include - SaaS based erp & e-commerce systems, and cloud deployed operations management software for health-care, manufacturing and other industries.