Client-Side Design Sending Requests & Receiving Responses

 

Client-Side Design
IPC_PRIVATE, atexit(), Sending Requests & Receiving Responses
Chapter 46 · TLPI · EmbeddedPathashala
Part
4 of 4
Topic
Client Logic
Key Calls
msgget, atexit
Pattern
Request-Reply

What This Part Covers

The client program is simpler than the server but introduces important patterns: creating a private reply queue, ensuring cleanup with atexit(), composing a request message, and correctly receiving the stream of response messages from the server.

This part also discusses limitations of the simple client and how a real production client would improve on them.

Key Terms

IPC_PRIVATE msgget() atexit() msgctl IPC_RMID removeQueue() msgsnd() request msgrcv() response RESP_MT_FAILURE check RESP_MT_DATA loop RESP_MT_END sentinel strncpy pathname

Step 1 — Client Startup: Creating a Private Queue

The first thing the client does is create its own message queue using IPC_PRIVATE. Unlike the server’s well-known queue (identified by SERVER_KEY), this queue has no fixed name — only the client knows its ID.

/* Look up the server's queue by its well-known key */
serverId = msgget(SERVER_KEY, S_IWUSR);
if (serverId == -1)
    errExit("msgget - server message queue");

/* Create our own private reply queue — no other process knows it */
clientId = msgget(IPC_PRIVATE, S_IRUSR | S_IWUSR | S_IWGRP);
if (clientId == -1)
    errExit("msgget - client message queue");
Call Key Flags Purpose
msgget(SERVER_KEY, ...) SERVER_KEY S_IWUSR — write only Find the server’s existing queue. Client only needs to write to it (send requests).
msgget(IPC_PRIVATE, ...) IPC_PRIVATE S_IRUSR|S_IWUSR|S_IWGRP Create a new private queue. Client reads from it; server child writes to it.
S_IWGRP on the client queue: The server runs as a different user or group. The S_IWGRP bit allows the server child process to send response messages into the client’s queue even though it has a different UID.

Step 2 — Registering Cleanup with atexit()

atexit() registers a function that the C runtime calls automatically when the process exits — whether via return from main(), a call to exit(), or even abnormal termination through some signal handlers.

static int clientId; /* global so atexit handler can see it */

static void
removeQueue(void)
{
    if (msgctl(clientId, IPC_RMID, NULL) == -1)
        errExit("msgctl");
}

/* In main(), after creating clientId: */
if (atexit(removeQueue) != 0)
    errExit("atexit");

Why is this necessary? System V message queues persist in the kernel until explicitly deleted — they are not closed like file descriptors. If the client exits without deleting its queue:

  • The queue remains in the kernel’s IPC namespace.
  • It occupies a slot in the system’s message queue table (limited by MSGMNI).
  • Pending messages in it waste kernel memory.
  • You can see leftover queues with ipcs -q and delete them with ipcrm -q <id>.
Why atexit() and not just delete at end of main()? If the client crashes or calls exit() from an error path, control might never reach the end of main(). atexit() ensures cleanup happens regardless of how the process exits.
Limitation: atexit() handlers do NOT run when the process is killed by SIGKILL or SIGSTOP, or if the process calls _exit(). For those cases, the queue leaks. This is a known limitation discussed in TLPI Exercise 46-5.

Step 3 — Sending the Request to the Server

The client builds a requestMsg and sends it to the server’s queue.

struct requestMsg req;

req.mtype    = 1;         /* Any positive value — server reads all types */
req.clientId = clientId;  /* Tell server where to send the reply */

/* Copy pathname safely — prevent buffer overflow */
strncpy(req.pathname, argv[1], sizeof(req.pathname) - 1);
req.pathname[sizeof(req.pathname) - 1] = '\0'; /* ensure null termination */

/* Send request to server's queue */
if (msgsnd(serverId, &req, REQ_MSG_SIZE, 0) == -1)
    errExit("msgsnd");
Field Value Why
req.mtype 1 Server uses msgrcv(..., 0, 0) which receives any type. Value 1 is arbitrary.
req.clientId clientId Server child will reply to this queue ID.
req.pathname argv[1] Filename to serve, safely copied with strncpy + manual null termination.
size arg REQ_MSG_SIZE Macro: sizeof(requestMsg) - sizeof(long) — excludes mtype.

Step 4 — Receiving Responses from the Server

After sending the request, the client waits for responses on its private queue. The first message it receives determines what happens next:

msgrcv(clientId, &resp, …) — first message
mtype == RESP_MT_FAILURE
Print resp.data
(error text)
exit(EXIT_FAILURE)
mtype == RESP_MT_DATA
Write resp.data to stdout
Loop: receive more messages
mtype == RESP_MT_END
→ Stop loop, exit OK
struct responseMsg resp;
ssize_t msgLen, totBytes;

/* Receive the FIRST response — might be failure or first data chunk */
msgLen = msgrcv(clientId, &resp, sizeof(resp.data), 0, 0);
if (msgLen == -1)
    errExit("msgrcv");

/* Check if server reported failure */
if (resp.mtype == RESP_MT_FAILURE) {
    fprintf(stderr, "%s\n", resp.data); /* print error from server */
    exit(EXIT_FAILURE);
}

/* File opened OK — loop receiving data chunks until RESP_MT_END */
totBytes = msgLen;
for (;;) {
    if (resp.mtype == RESP_MT_END)
        break;                  /* transfer complete */

    write(STDOUT_FILENO, resp.data, msgLen); /* print chunk to stdout */

    msgLen = msgrcv(clientId, &resp,
                     sizeof(resp.data), 0, 0);
    if (msgLen == -1)
        errExit("msgrcv");
    totBytes += msgLen;
}

Known Limitations of This Simple Client

The book explicitly notes this client does not handle several real-world failure scenarios:

Scenario Problem How to Fix (Exercise 46-5)
Server dies mid-transfer Client blocks forever in msgrcv() waiting for more DATA or END that never comes. Add a timeout using alarm() or select() with the queue, or use MSG_NOERROR + periodic polling.
Server never processes the request Client blocks in msgrcv() indefinitely. Add a timeout before the first receive.
Client killed by SIGKILL atexit() does not run — queue leaks. Install a handler for SIGTERM/SIGINT that calls msgctl(IPC_RMID) before exiting.
Multiple clients, one queue ID reuse If clientId is reused after a crash, a new client might receive stale messages from a previous run. Drain the queue on startup, or use a versioned message type.

Code Example — Complete Client main()
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include "svmsg_file.h"

static int clientId;

static void
removeQueue(void)
{
    msgctl(clientId, IPC_RMID, NULL);
}

int
main(int argc, char *argv[])
{
    struct requestMsg  req;
    struct responseMsg resp;
    int    serverId;
    ssize_t msgLen, totBytes;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s pathname\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* === SETUP === */
    serverId = msgget(SERVER_KEY, S_IWUSR);
    if (serverId == -1) exit(EXIT_FAILURE);

    clientId = msgget(IPC_PRIVATE,
                       S_IRUSR | S_IWUSR | S_IWGRP);
    if (clientId == -1) exit(EXIT_FAILURE);

    atexit(removeQueue); /* ensure queue deleted on exit */

    /* === SEND REQUEST === */
    req.mtype    = 1;
    req.clientId = clientId;
    strncpy(req.pathname, argv[1], sizeof(req.pathname) - 1);
    req.pathname[sizeof(req.pathname) - 1] = '\0';

    msgsnd(serverId, &req, REQ_MSG_SIZE, 0);

    /* === RECEIVE FIRST RESPONSE === */
    msgLen = msgrcv(clientId, &resp,
                     sizeof(resp.data), 0, 0);

    if (resp.mtype == RESP_MT_FAILURE) {
        fprintf(stderr, "Server error: %s\n", resp.data);
        exit(EXIT_FAILURE);
    }

    /* === LOOP RECEIVING DATA === */
    totBytes = 0;
    for (;;) {
        if (resp.mtype == RESP_MT_END) break;
        write(STDOUT_FILENO, resp.data, msgLen);
        totBytes += msgLen;
        msgLen = msgrcv(clientId, &resp,
                         sizeof(resp.data), 0, 0);
        if (msgLen == -1) exit(EXIT_FAILURE);
    }

    fprintf(stderr, "Received %ld bytes\n", (long)totBytes);
    exit(EXIT_SUCCESS);
}

IPC_PRIVATE vs Named Key — When to Use Each
Aspect IPC_PRIVATE Named Key (e.g. ftok/SERVER_KEY)
Discoverability Only by sharing the returned ID explicitly Any process that knows the key can connect
Use case Private reply queues, parent-child IPC Well-known service endpoints
Multiple queues Each call creates a brand new queue Same key always returns same queue (if it exists)
Cleanup Must delete explicitly — never auto-deleted Same — must delete explicitly
In this app Client reply queues Server request queue

Summary of This Part
IPC_PRIVATE queue creation atexit() queue cleanup removeQueue() handler Request message composition strncpy safe copy First response check: FAILURE vs DATA msgrcv loop until RESP_MT_END Client known limitations

Interview Questions — Client Design
  1. Why does the client create its queue with IPC_PRIVATE instead of a fixed key?
  2. What does atexit() do and why is it necessary for the client’s private queue?
  3. In what situations will the atexit() handler NOT run?
  4. Why does the client include clientId in the request message instead of using a fixed agreed-upon reply queue?
  5. Why does the client check mtype == RESP_MT_FAILURE on the very first received message before starting the data loop?
  6. What happens if the client tries to receive but the server has already deleted the queue?
  7. Why is the clientId variable declared as static global instead of local to main()?
  8. What is the permission S_IWGRP for on the client’s message queue?
  9. The client uses strncpy and manually sets the last byte to '\0'. Why is both necessary?
  10. How would you add a timeout to the client’s msgrcv() call to prevent blocking forever?
  11. If two instances of this client run simultaneously requesting the same file, is there any conflict? Explain.
  12. What does msgrcv(clientId, &resp, sizeof(resp.data), 0, 0) receive — all message types or a specific one?

Chapter 46 Complete!

See all interview questions consolidated, or go back to start.

All Interview Questions → ← Back to Part 1

Leave a Reply

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