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.
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:
(IPC_PRIVATE)
Chunks data
Sends to clientId
or
RESP_MT_FAILURE
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.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) |
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.The client code from the book does the following after sending the request:
mtype == RESP_MT_FAILURE, print error and exitmsgrcv() while mtype == RESP_MT_DATARelevant 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);
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.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 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.
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.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.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.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.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.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.