shutdown() and Socket Half-Close Closing just one direction of a bidirectional socket connection

 

Chapter 61 · Part 2 of 7
shutdown() and Socket Half-Close
Closing just one direction of a bidirectional socket connection

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:

Value of ‘how’
What it closes
Effect on peer
SHUT_RD
Reading half
Peer data is silently discarded; peer gets SIGPIPE/EPIPE on next write (UNIX domain only)
SHUT_WR
Writing half
Peer reads EOF after all queued data is consumed
SHUT_RDWR
Both halves
Same as SHUT_RD then SHUT_WR

Visualising the Two Channels

Before shutdown() — both channels open
Client
Process

write channel (SHUT_WR closes this)

read channel (SHUT_RD closes this)

Server
Process
After client calls shutdown(sockfd, SHUT_WR)
Client
Process

write channel — CLOSED

read channel — still open

Server
reads EOF,
then responds

shutdown() vs close() — Key Difference

This is a very important distinction that confuses many people:

close() decrements the file descriptor’s reference count. The socket channels are actually shut down only when the reference count reaches zero. If you’ve dup()‘d the socket, close() on one copy does NOT close the connection.
shutdown() operates on the underlying open file description directly — it closes the channel regardless of how many file descriptors point to the socket. Even if you have 3 dup()’d copies, shutdown(SHUT_WR) sends FIN to the peer immediately.
/* 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

SHUT_RD behaves differently for TCP vs UNIX domain sockets.

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.

Interview Questions & Answers

Q1. What is the difference between close() and shutdown()?
close() decrements the file descriptor reference count and closes the socket only when it reaches zero. shutdown() closes the specified channel(s) immediately on the open file description regardless of how many fd copies exist. shutdown() is also more precise — it can close just one direction.
Q2. What does SHUT_WR actually send over the network?
It causes a TCP FIN segment to be sent to the peer, signalling that the local side will send no more data. The peer’s next read() will return 0 (EOF) once all buffered data has been consumed.
Q3. After shutdown(SHUT_WR), can you still read from the socket?
Yes. shutdown(SHUT_WR) only closes the write direction. The read direction remains open. You can continue to read data sent by the peer until the peer also closes their end.
Q4. If I dup() a socket fd and close() one copy, is the connection closed?
No. close() on one dup’d copy just decrements the reference count. The connection remains open as long as any fd pointing to it exists. Only when the last fd is close()’d does the OS send FIN. If you want to signal EOF to the peer regardless, use shutdown().
Q5. Name a real-world use case for the half-close pattern.
HTTP/1.0: the client sends the full request (no Content-Length header needed), then calls shutdown(SHUT_WR). The server reads until EOF (knowing the full request has arrived), processes it, sends the response, and closes. Also used in tools like ssh and rsh for piping stdin to a remote command.
Q6. What happens if you call write() after shutdown(SHUT_WR)?
write() will fail with EPIPE error and the process will receive SIGPIPE signal (which by default terminates the process). You should either catch SIGPIPE or set MSG_NOSIGNAL on the write call to avoid the signal.
Q7. Why is SHUT_RD not useful for TCP sockets?
TCP is a transport protocol that acknowledges received segments at the network level. Even after SHUT_RD, the kernel’s TCP stack continues receiving and ACKing packets from the peer (they’re just discarded). The peer has no way of knowing you’ve stopped reading. Only SHUT_WR (which sends a TCP FIN) has meaningful cross-network semantics.

Leave a Reply

Your email address will not be published. Required fields are marked *