System V Message Queues Client-Server File Transfer

 

System V Message Queues
Part 1 — Client-Server File Transfer | Chapter 46 · TLPI
46.8
Section
2
Programs
MQ
IPC Type

What is this about?

In Section 46.8 of TLPI, a classic client-server application is built using System V message queues. The server opens files on behalf of clients and sends the file data back through the message queue. This teaches you the real-world pattern of using message types to route responses back to the correct client.

Think of it like a courier service: every client drops a request in a shared mailbox (server queue). The server picks it up, does the work, and delivers the reply to the client’s private mailbox using the client’s queue ID.

Key Terms
msgget() msgsnd() msgrcv() mtype IPC_CREAT IPC_PRIVATE clientId RESP_MT_FAILURE RESP_MT_DATA ftok()

How the Client-Server Design Works

The system uses two types of message queues:

Queue Owner Purpose
Server Queue (well-known key) Server Receives file requests from all clients
Client Queue (IPC_PRIVATE) Each Client Receives file data / error responses

The message flow looks like this:

CLIENT
Creates own queue
(IPC_PRIVATE)
clientId → server

1. Request (filename + clientId)

SERVER
Reads file
Chunks data
Sends to clientId

2. File data (chunks)

CLIENT QUEUE
RESP_MT_DATA
or
RESP_MT_FAILURE
Key Insight: The client passes its own queue ID (clientId) inside the message body, not in mtype. This is intentional — the server uses clientId to know where to send the response. mtype is used only to categorize the message type, not to address queues.

Message Types in This Application

The application defines specific numeric message types to distinguish between different kinds of messages:

Constant Value Meaning
REQ_MT_OPEN 1 Client → Server: open this file
RESP_MT_FAILURE 1 Server → Client: could not open file
RESP_MT_DATA 2 Server → Client: here is a chunk of file data
RESP_MT_END 3 Server → Client: all data has been sent (EOF)
Why separate types? The client loops calling msgrcv() until it sees a message with mtype != RESP_MT_DATA. This tells it that the stream has ended (either successfully or with failure). No special “end of stream” sentinel value in the data is needed.

Client-Side Logic (Section 46.8 walkthrough)

The client code from the book does the following after sending the request:

1Read first response — wait for server’s first message
2Check for failure — if mtype == RESP_MT_FAILURE, print error and exit
3Loop over data messages — keep calling msgrcv() while mtype == RESP_MT_DATA
4Count bytes and messages — print statistics when loop ends

Relevant snippet from the book (client receive side):

/* Receive first response from server */
msgLen = msgrcv(clientId, &resp, RESP_MSG_SIZE, 0, 0);
if (msgLen == -1)
    errExit("msgrcv");

/* Was it a failure response? */
if (resp.mtype == RESP_MT_FAILURE) {
    printf("%s\n", resp.data);   /* print server error message */
    msgctl(clientId, IPC_RMID, NULL);
    exit(EXIT_FAILURE);
}

/* Loop: receive all file-data messages until stream ends */
totBytes = msgLen;
for (numMsgs = 1; resp.mtype == RESP_MT_DATA; numMsgs++) {
    msgLen = msgrcv(clientId, &resp, RESP_MSG_SIZE, 0, 0);
    if (msgLen == -1)
        errExit("msgrcv");
    totBytes += msgLen;
}

printf("Received %ld bytes (%d messages)\n", (long)totBytes, numMsgs);
Watch out: The loop condition checks resp.mtype == RESP_MT_DATA after receiving each message. The loop exits when the final message (the end marker) is received — not before. This means the end-of-stream message itself is counted once, but its msgLen is also added to totBytes.

Complete Working Example — SysV MQ Client-Server

Below is a self-contained example inspired by the book’s design. The server listens for file requests and streams back the file contents in chunks. The client creates its private queue, sends the request, and collects the data.

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

/* Message type constants */
#define REQ_MT_OPEN     1   /* client -> server: open request     */
#define RESP_MT_FAILURE 1   /* server -> client: open failed      */
#define RESP_MT_DATA    2   /* server -> client: data chunk       */
#define RESP_MT_END     3   /* server -> client: end of file      */

#define DATA_CHUNK      512
#define SERVER_KEY      0x12345678

/* Request message layout */
struct request_msg {
    long   mtype;                /* REQ_MT_OPEN                    */
    int    clientId;             /* client's queue id              */
    char   pathname[256];        /* file to open                   */
};

/* Response message layout */
struct response_msg {
    long   mtype;                /* RESP_MT_DATA / FAILURE / END   */
    char   data[DATA_CHUNK];     /* file chunk or error text       */
};

/* Send file contents to client in DATA_CHUNK sized messages */
static void serve_request(int clientId, const char *path)
{
    int fd;
    ssize_t n;
    struct response_msg resp;

    fd = open(path, O_RDONLY);
    if (fd == -1) {
        /* Notify client of failure */
        resp.mtype = RESP_MT_FAILURE;
        snprintf(resp.data, DATA_CHUNK, "Cannot open: %s", strerror(errno));
        msgsnd(clientId, &resp, sizeof(resp.data), 0);
        return;
    }

    /* Read and send file in chunks */
    while ((n = read(fd, resp.data, DATA_CHUNK)) > 0) {
        resp.mtype = RESP_MT_DATA;
        if (msgsnd(clientId, &resp, n, 0) == -1) {
            perror("msgsnd data");
            break;
        }
    }

    close(fd);

    /* Send end-of-file marker */
    resp.mtype = RESP_MT_END;
    memset(resp.data, 0, DATA_CHUNK);
    msgsnd(clientId, &resp, 1, 0);   /* 1-byte body so mtype is valid */
}

int main(void)
{
    int             serverId;
    struct request_msg req;

    /* Create well-known server queue */
    serverId = msgget(SERVER_KEY, IPC_CREAT | IPC_EXCL | 0600);
    if (serverId == -1) {
        perror("msgget server");
        exit(EXIT_FAILURE);
    }

    printf("Server started (qid=%d). Waiting for requests...\n", serverId);

    for (;;) {
        /* Block until a request arrives */
        if (msgrcv(serverId, &req, sizeof(req) - sizeof(long), 0, 0) == -1) {
            perror("msgrcv");
            break;
        }

        printf("Request for '%s' from clientId=%d\n",
               req.pathname, req.clientId);

        /* Fork so server can handle next client immediately */
        if (fork() == 0) {
            serve_request(req.clientId, req.pathname);
            exit(EXIT_SUCCESS);         /* child exits after serving */
        }
    }

    /* Cleanup */
    msgctl(serverId, IPC_RMID, NULL);
    return 0;
}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/types.h>

#define REQ_MT_OPEN     1
#define RESP_MT_FAILURE 1
#define RESP_MT_DATA    2
#define RESP_MT_END     3
#define DATA_CHUNK      512
#define SERVER_KEY      0x12345678

struct request_msg {
    long  mtype;
    int   clientId;
    char  pathname[256];
};

struct response_msg {
    long  mtype;
    char  data[DATA_CHUNK];
};

int main(int argc, char *argv[])
{
    int              serverId, clientId;
    struct request_msg req;
    struct response_msg resp;
    ssize_t          msgLen;
    long             totBytes = 0;
    int              numMsgs  = 0;

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

    /* Get server queue by well-known key */
    serverId = msgget(SERVER_KEY, 0);
    if (serverId == -1) {
        perror("msgget: server queue not found");
        exit(EXIT_FAILURE);
    }

    /* Create private client queue */
    clientId = msgget(IPC_PRIVATE, 0600);
    if (clientId == -1) {
        perror("msgget: client queue");
        exit(EXIT_FAILURE);
    }

    /* Build and send request */
    req.mtype    = REQ_MT_OPEN;
    req.clientId = clientId;
    strncpy(req.pathname, argv[1], sizeof(req.pathname) - 1);

    if (msgsnd(serverId, &req, sizeof(req) - sizeof(long), 0) == -1) {
        perror("msgsnd");
        msgctl(clientId, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    /* Receive first response */
    msgLen = msgrcv(clientId, &resp, DATA_CHUNK, 0, 0);
    if (msgLen == -1) {
        perror("msgrcv");
        msgctl(clientId, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    /* Check for failure */
    if (resp.mtype == RESP_MT_FAILURE) {
        printf("Error from server: %s\n", resp.data);
        msgctl(clientId, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    /* Collect all data messages */
    totBytes = msgLen;
    for (numMsgs = 1; resp.mtype == RESP_MT_DATA; numMsgs++) {
        msgLen = msgrcv(clientId, &resp, DATA_CHUNK, 0, 0);
        if (msgLen == -1) {
            perror("msgrcv loop");
            break;
        }
        totBytes += msgLen;
    }

    printf("Received %ld bytes in %d messages\n", totBytes, numMsgs);

    /* Remove private client queue */
    msgctl(clientId, IPC_RMID, NULL);
    return 0;
}

How to build and run:

# Compile
gcc -o server server.c
gcc -o client client.c

# Terminal 1 – run server
./server

# Terminal 2 – run client
./client /etc/hostname

# Expected output (client side):
Received 12 bytes in 1 messages

# Cleanup: kill server, it removes queue on exit
# Or manually:
ipcs -q              # list message queues
ipcrm -q <msqid>    # remove by ID

Exercise 46-3 Explained — Why clientId is in the Body

Exercise 46-3 from the book asks: “Why does the client pass the identifier of its message queue in the body of the message (in the clientId field) rather than in mtype?”

The reason: mtype must be a positive integer used to categorize the message. msgget() returns a queue ID that is a non-negative integer — it could be 0, and mtype cannot be zero or negative. Also, mtype is used by the server to call msgrcv() with a type filter. If it were the clientId, the server could not easily filter all client requests with a single type.

By putting clientId in the message body, it is safe regardless of value (including 0), and the server can read all requests using mtype=0 (read any message) while still knowing exactly where to send the reply.

Interview Questions
Q1. How does a server identify which client to respond to in a SysV MQ client-server design?
The client creates its own private message queue using IPC_PRIVATE and sends its queue ID (clientId) inside the request message body. The server reads this field and uses it as the destination queue ID when calling msgsnd() to send the response.
Q2. What is IPC_PRIVATE and when should you use it?
IPC_PRIVATE is a special key value (0) that guarantees creation of a brand-new, unique IPC object. It is used when you want a queue that only the creating process (and its children/known processes) can use. In a client-server design, each client creates its own private response queue with IPC_PRIVATE.
Q3. How does the client know the server has finished sending file data?
The server sends a final message with mtype = RESP_MT_END (a value different from RESP_MT_DATA). The client loops while resp.mtype == RESP_MT_DATA, so when the end marker arrives, the loop exits naturally.
Q4. Why does the client delete its message queue after receiving all data?
System V IPC objects persist in the kernel until explicitly removed (or the system reboots). If the client does not call msgctl(clientId, IPC_RMID, NULL), the queue remains in the kernel consuming resources even after the client exits. This is a resource leak and must be cleaned up explicitly.
Q5. What happens if the client crashes after sending the request but before reading responses?
The server child fills the client’s message queue with data. Since the client no longer exists to read messages, the queue fills up and msgsnd() in the server blocks indefinitely. This is the “disappeared client” problem — Exercise 46-4(e) addresses it by adding a timeout via alarm() or sigaction() around msgsnd(), and then deleting the stale client queue and exiting.
Q6. How would you make the server handle multiple clients concurrently?
The standard technique (as shown in the book) is to fork() a child process for each incoming request. The parent immediately returns to the msgrcv() loop to accept the next request. Each child handles one client independently and exits when done. Alternatively, threads can be used, but fork is simpler for file-serving because each child gets its own file descriptor.

Leave a Reply

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