Why Half-Close Exists
A stream socket connection is bidirectional — data can flow in both directions at the same time. When you call close(), you shut down both directions at once. But sometimes you need to say: “I’m done sending, but I still want to receive.”
This is exactly what shutdown() lets you do. It gives you fine-grained control over which half of the connection to close.
A classic example: a client sends a request, then calls shutdown(SHUT_WR) to signal “no more data from me”, then reads the server’s response until EOF. The server knows the client is done when it reads EOF, processes the request, sends the response, and closes its end. This is called a half-close.
The shutdown() System Call
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/* Returns 0 on success, -1 on error */
sockfd is a connected socket file descriptor. how controls which channel to close:
Visualising the Two Channels
Process
→
←
Process
Process
✕
←
reads EOF,
then responds
shutdown() vs close() — Key Difference
This is a very important distinction that confuses many people:
dup()‘d the socket, close() on one copy does NOT close the connection./* Demonstrating the difference */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* ... connect ... */
int fd2 = dup(sockfd);
close(sockfd);
/*
* Connection is STILL OPEN because fd2 still references it.
* The peer receives nothing — no FIN yet.
*/
/* vs. */
shutdown(sockfd, SHUT_WR);
/*
* FIN is sent to peer IMMEDIATELY, even though fd2 exists.
* The peer will read EOF on their next read().
*/
Complete Example: Half-Close Pattern
This example simulates a simple “send-all-then-receive-all” protocol. The client sends data, does a half-close, then reads the server’s echo response.
client_halfclose.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>
#define PORT 9000
int main(void)
{
int sockfd;
struct sockaddr_in servaddr;
char wbuf[] = "Hello from client — this is the full request body";
char rbuf[256];
ssize_t n;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) { perror("socket"); exit(1); }
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("connect"); exit(1);
}
/* Step 1: Send our request data */
if (write(sockfd, wbuf, strlen(wbuf)) == -1) {
perror("write"); exit(1);
}
printf("Client sent: %s\n", wbuf);
/*
* Step 2: Half-close the write end.
* This sends a FIN to the server, signalling "no more data from me".
* The server will read EOF and know the request is complete.
* We can still read from sockfd.
*/
if (shutdown(sockfd, SHUT_WR) == -1) {
perror("shutdown"); exit(1);
}
printf("Client: write half closed (FIN sent)\n");
/*
* Step 3: Read server's response until we get EOF from server.
* We know server will close its end after processing.
*/
while ((n = read(sockfd, rbuf, sizeof(rbuf) - 1)) > 0) {
rbuf[n] = '\0';
printf("Client received: %s\n", rbuf);
}
if (n == -1) { perror("read"); exit(1); }
printf("Client: server closed connection (EOF received)\n");
close(sockfd);
return 0;
}
server_halfclose.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 9000
int main(void)
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buf[1024];
ssize_t n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
perror("bind"); exit(1);
}
listen(listenfd, 5);
printf("Server listening on port %d\n", PORT);
connfd = accept(listenfd, NULL, NULL);
if (connfd == -1) { perror("accept"); exit(1); }
printf("Client connected\n");
/*
* Read all data from client.
* When client does shutdown(SHUT_WR), we will read EOF (n == 0).
*/
while ((n = read(connfd, buf, sizeof(buf))) > 0) {
printf("Server received %zd bytes\n", n);
}
printf("Server: client closed write end (EOF)\n");
/* Now send response back */
const char *resp = "Server echo: request received and processed OK";
write(connfd, resp, strlen(resp));
printf("Server sent response\n");
/*
* close() here sends FIN to client.
* Client's pending read() will return 0 (EOF).
*/
close(connfd);
close(listenfd);
return 0;
}
Build and run:
gcc -o server server_halfclose.c
gcc -o client client_halfclose.c
# Terminal 1:
./server
# Terminal 2:
./client
Note on SHUT_RD with TCP
For UNIX domain sockets: after SHUT_RD, if the peer tries to write, it gets SIGPIPE and EPIPE error.
For TCP sockets: SHUT_RD is largely meaningless at the application level. The TCP layer will continue to receive incoming packets (ACKing them at the TCP level) and discard them. The peer does NOT automatically get an error. This is a TCP protocol limitation — SHUT_RD on TCP cannot signal the remote side. Only SHUT_WR (which sends a FIN) has well-defined cross-network semantics.
