shutdown() on TCP Sockets Sockets: Advanced Topics

 

shutdown() on TCP Sockets
Chapter 61 · Sockets: Advanced Topics · Part 2 of 5
3
shutdown() modes
Half
Duplex Close
⚠️
SHUT_RD Caveat

Why shutdown() Exists

The regular close() call closes both the reading and writing channels of a socket at once — a full-duplex close. But sometimes you want more control. What if you want to say “I’m done sending, but I can still receive”? That is exactly what shutdown() lets you do.

Imagine you are copying a file over a network. Once you finish sending, you want to signal “no more data from me” but still wait for the server to send back a checksum or confirmation. With close() you would kill both directions. With shutdown(SHUT_WR) you close only the write side while keeping the read side open.

Key Terms to Know

shutdown() SHUT_WR SHUT_RD SHUT_RDWR Half-Duplex Close Full-Duplex Close FIN_WAIT1 CLOSE_WAIT File Descriptor EOF

shutdown() Function — Signature and Parameters
#include <sys/socket.h>

int shutdown(int sockfd, int how);
/* Returns 0 on success, -1 on error */

The how parameter controls which direction gets closed:

how value Meaning TCP Effect Use Case
SHUT_RD Close read side only No TCP FIN sent. Behavior varies by OS. Avoid for TCP. UNIX domain sockets only
SHUT_WR Close write side only Sends FIN to peer. Initiates active close. Read side stays open. Done sending, still need to receive
SHUT_RDWR Close both sides Sends FIN. Similar to active close via close(), but works even if multiple FDs refer to the socket. Force close regardless of dup() copies

close() vs shutdown() — Key Difference

close() — Full Close
Your process
READ ✗
Socket
WRITE ✗
  • Decrements file descriptor reference count
  • Only sends FIN when count reaches 0
  • If dup() or fork() was used, FIN may NOT be sent yet
  • Both read and write channels closed
shutdown(SHUT_WR) — Half Close
Your process
READ ✓
Socket
WRITE ✗
  • Ignores file descriptor reference count
  • Sends FIN immediately regardless of dup()/fork()
  • Peer sees EOF on their read side
  • You can still read() data that peer sends back

Critical difference with dup() / fork(): If you call dup(sockfd) to create a second file descriptor pointing to the same socket, then calling close() on the original does NOT send FIN — the FD count just drops from 2 to 1. The socket stays open. But shutdown() sends the FIN immediately regardless of how many FDs exist for that socket.

SHUT_WR in Detail — The Most Useful Case

When you call shutdown(sockfd, SHUT_WR):

  • TCP sends a FIN segment to the peer
  • The peer’s read() returns 0 (EOF) — they know you are done sending
  • Your local TCP moves into FIN_WAIT1, then FIN_WAIT2
  • The peer moves into CLOSE_WAIT
  • You can still call read() and receive data the peer sends before it also closes

State flow after shutdown(SHUT_WR)
ESTABLISHED → shutdown(SHUT_WR) → FIN_WAIT1 → peer ACK → FIN_WAIT2 → peer FIN → TIME_WAIT → 2MSL → CLOSED

⚠️ SHUT_RD on TCP — Why You Should Avoid It

shutdown(sockfd, SHUT_RD) is supposed to close only the read side of a TCP socket. In theory, after calling it, your read() calls should return EOF. But the actual behaviour is inconsistent across operating systems and the TCP spec does not define it clearly.

OS / Behaviour What happens after SHUT_RD on TCP
Linux read() returns 0 (EOF) after existing data is drained. BUT: if peer continues to write, you can still read that data on Linux! No FIN is sent to the peer.
BSD (macOS, FreeBSD) read() always returns 0. But if peer keeps writing, the receive buffer fills up, and peer’s write() eventually blocks. No FIN sent.
UNIX domain sockets Peer gets SIGPIPE and EPIPE if it writes after you call SHUT_RD. This is the one case where SHUT_RD behaves predictably.
Rule: Do not use SHUT_RD with TCP sockets in portable programs. The behaviour varies and is not well-defined by TCP. Use it only with UNIX domain sockets where it works as expected.

Code Example: Using shutdown(SHUT_WR) for Half-Close

This is a classic pattern used in programs like netcat — read from stdin and send to server, then when stdin reaches EOF, signal the server you are done sending but still receive the server’s response.

half_close_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 9090
#define BUF_SIZE 1024

int main() {
    int sock_fd;
    struct sockaddr_in addr;
    char buf[BUF_SIZE];
    int n;

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

    if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("connect failed");
        exit(1);
    }
    printf("Connected to server.\n");

    /* Send some data */
    const char *msg = "GET /index.html HTTP/1.0\r\n\r\n";
    write(sock_fd, msg, strlen(msg));
    printf("Sent request to server.\n");

    /*
     * HALF-CLOSE: We are done sending.
     * Sends FIN to server — server's read() returns EOF.
     * But we keep our read side open to receive the response.
     */
    if (shutdown(sock_fd, SHUT_WR) == -1) {
        perror("shutdown SHUT_WR failed");
        exit(1);
    }
    printf("Sent FIN (SHUT_WR). Still listening for server response...\n");

    /*
     * Now read whatever the server sends back.
     * This works because our read side is still open.
     */
    while ((n = read(sock_fd, buf, BUF_SIZE - 1)) > 0) {
        buf[n] = '\0';
        printf("From server: %s", buf);
    }

    if (n == 0) {
        printf("\nServer closed connection (EOF). Done.\n");
    } else {
        perror("read error");
    }

    /* Full close — both sides now */
    close(sock_fd);
    return 0;
}

What this demonstrates:

Client                          Server
------                          ------
connect() ──────────────────────> accept()
write(request) ─────────────────> read()
shutdown(SHUT_WR) ───FIN────────> read() returns 0 (EOF)
                                  Server knows client is done sending
                    <───data──── write(response)
read(response) ◄────────────────
                    <───FIN───── close()
read() returns 0 (EOF)
close()

Demonstration: close() vs shutdown() with dup()

#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void demo_close_vs_shutdown(int sock_fd) {
    /* dup() creates a second file descriptor for same socket */
    int dup_fd = dup(sock_fd);
    printf("Original fd: %d, Dup fd: %d\n", sock_fd, dup_fd);

    /*
     * close(sock_fd) here:
     * - Decrements reference count from 2 to 1
     * - NO FIN is sent because dup_fd still refers to socket
     * - The peer does NOT see EOF
     */
    close(sock_fd);
    printf("close(sock_fd): FD count is now 1. No FIN sent yet.\n");

    /*
     * shutdown(dup_fd, SHUT_WR) here:
     * - Ignores reference count entirely
     * - FIN is sent immediately
     * - Peer sees EOF NOW
     */
    shutdown(dup_fd, SHUT_WR);
    printf("shutdown(dup_fd, SHUT_WR): FIN sent regardless of dup.\n");

    close(dup_fd); /* Final cleanup */
}

When to Use Each Option — Decision Guide
Use close()

When you are done with the socket completely — no more reading or writing. This is the normal case for simple clients and servers.

Use shutdown(SHUT_WR)

When you need to signal “done sending” (send EOF / FIN) but still need to receive the peer’s response. Also use when dup() or fork() was used and you need FIN sent immediately.

Use shutdown(SHUT_RDWR)

When you want to forcibly close a socket and send FIN immediately, even if other FDs (from dup/fork) refer to it. Unlike close(), this triggers FIN regardless of refcount.

Avoid shutdown(SHUT_RD) with TCP

Behaviour is undefined and OS-dependent for TCP. Only use with UNIX domain sockets where it works consistently.

Interview Questions & Answers

Q1. What is the difference between close() and shutdown() on a socket?

close() decrements the file descriptor’s reference count and only sends FIN when the count reaches zero. shutdown() ignores the reference count and immediately affects the socket’s communication channel. close() always closes both directions; shutdown() can close just one direction (half-close).

Q2. If you call dup(sock_fd) and then close(sock_fd), does the peer receive a FIN?

No. dup() creates a second file descriptor pointing to the same socket object. The reference count becomes 2. close(sock_fd) drops it to 1. Since the count is not zero, TCP does not send FIN. The peer does not see EOF. To force FIN immediately, use shutdown(sock_fd, SHUT_WR).

Q3. What happens on the peer side when you call shutdown(SHUT_WR)?

A FIN segment is sent to the peer. The peer’s read() call returns 0 (EOF), signalling that no more data will arrive from your side. However, the peer can still send data back to you because only the write half of your connection was shut down.

Q4. Why is shutdown(SHUT_RD) unreliable for TCP?

TCP has no protocol mechanism to tell the peer “stop sending”. SHUT_RD only affects the local socket’s receive behaviour. On Linux, read() returns EOF but the peer can still send data and it can still be read. On BSD systems, the buffer fills and the peer’s writes eventually block. No RST or FIN is sent. The behaviour is implementation-defined, making it non-portable.

Q5. Describe a real-world use case for shutdown(SHUT_WR).

The classic use case is an HTTP/1.0 client. The client sends a complete HTTP request, then calls shutdown(SHUT_WR) to signal the end of the request (server gets EOF). The client then reads the full HTTP response. Without shutdown(), you would have to either close the socket entirely (losing the response) or implement your own end-of-request signalling in the application protocol.

Q6. After calling shutdown(SHUT_WR), can you still call write() on the socket?

No. Calling write() after shutdown(SHUT_WR) will return -1 with errno set to EPIPE, and the process will also receive a SIGPIPE signal (which by default terminates the process). You must handle or ignore SIGPIPE if you want the error returned rather than a signal.

Continue Learning

Next: Why TIME_WAIT exists and what the 2MSL timer does

← Part 1: TCP Termination Part 3: TIME_WAIT State →

Leave a Reply

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