What is This Application?
Section 46.8 of TLPI presents a complete, real-world client-server application built on System V message queues. A file server receives requests from clients asking for file contents, then streams the file back as a series of response messages. This application demonstrates many important design principles simultaneously:
- One message queue per client (IPC_PRIVATE)
- Well-known server queue key
- Message type constants (RESP_MT_DATA, RESP_MT_END, RESP_MT_FAILURE)
- Safe message size calculation using
offsetof() - Fork-based request handling
- Header file shared between server and client
Understanding this application means understanding how all the pieces of System V message queue programming fit together.
In any client-server application, both the client and server must agree on the format of messages they exchange. This agreement is encoded in a shared header file. The file server header defines:
- The IPC key for the server’s well-known queue (
SERVER_KEY) - The structure of request messages (
requestMsg) - The structure of response messages (
responseMsg) - Constants for the message size calculations
- Constants for response message types (
RESP_MT_*)
/* svmsg_file.h — shared between server and client */
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stddef.h> /* for offsetof() */
#include <limits.h> /* for PATH_MAX */
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
/* The IPC key used to create/find the server's message queue.
* This is a hexadecimal constant chosen to be unique on this system.
* Both server and client use this same key to msgget(). */
#define SERVER_KEY 0x1aaaaaa1
/* ================================================================
* REQUEST MESSAGE: Client → Server
* ================================================================ */
struct requestMsg {
long mtype; /* Required first field (unused — set to 1) */
int clientId; /* ID of the client's private response queue */
char pathname[PATH_MAX]; /* Null-terminated path of file to retrieve */
};
/* REQ_MSG_SIZE: size of the mtext part (excludes mtype).
*
* We CANNOT simply use sizeof(requestMsg) - sizeof(long) because
* the compiler may insert padding bytes between clientId and pathname.
* Using offsetof() gives the exact byte offset of each field,
* correctly accounting for any padding.
*/
#define REQ_MSG_SIZE (offsetof(struct requestMsg, pathname) \
- offsetof(struct requestMsg, clientId) \
+ PATH_MAX)
/* ================================================================
* RESPONSE MESSAGE: Server → Client
* ================================================================ */
#define RESP_MSG_SIZE 8192
struct responseMsg {
long mtype; /* One of RESP_MT_* values below */
char data[RESP_MSG_SIZE]; /* File content chunk OR error message */
};
/* Response message type constants — these are protocol states: */
#define RESP_MT_FAILURE 1 /* Server could not open the requested file */
#define RESP_MT_DATA 2 /* This message contains a chunk of file data */
#define RESP_MT_END 3 /* All file data has been sent — transfer done */
This is one of the most instructive details in the file server design. A naive programmer might write:
/* WRONG — may not account for struct padding */
#define REQ_MSG_SIZE (sizeof(struct requestMsg) - sizeof(long))
The problem is struct padding. The C compiler is allowed to insert invisible padding bytes between struct fields to satisfy alignment requirements. For example:
struct requestMsg {
long mtype; /* 8 bytes on 64-bit */
int clientId; /* 4 bytes */
/* POSSIBLE: 4 bytes of padding here for alignment of pathname */
char pathname[PATH_MAX]; /* 4096 bytes */
};
/* sizeof(struct requestMsg) might be 8 + 4 + 4(padding) + 4096 = 4112 */
/* sizeof(long) = 8 */
/* sizeof(requestMsg) - sizeof(long) = 4104 — INCLUDES the 4 bytes of padding! */
If we pass an oversized msgsz to msgsnd(), we send unnecessary garbage bytes. The correct approach using offsetof():
#include <stddef.h>
/* offsetof(struct requestMsg, pathname):
* = byte offset of 'pathname' from the start of the struct
* = 8 (mtype) + 4 (clientId) + 4 (padding) = 16
*
* offsetof(struct requestMsg, clientId):
* = byte offset of 'clientId' from the start of the struct
* = 8 (just mtype)
*
* REQ_MSG_SIZE = (16 - 8) + PATH_MAX = 8 + 4096 = 4104
* This is: sizeof(clientId) + any_padding + sizeof(pathname)
* Which is exactly the mtext part (everything after mtype)
*/
#define REQ_MSG_SIZE (offsetof(struct requestMsg, pathname) \
- offsetof(struct requestMsg, clientId) \
+ PATH_MAX)
/* Demonstration: */
#include <stdio.h>
int main(void) {
printf("offset of mtype : %zu\n", offsetof(struct requestMsg, mtype));
printf("offset of clientId : %zu\n", offsetof(struct requestMsg, clientId));
printf("offset of pathname : %zu\n", offsetof(struct requestMsg, pathname));
printf("REQ_MSG_SIZE : %zu\n", (size_t)REQ_MSG_SIZE);
printf("sizeof full struct : %zu\n", sizeof(struct requestMsg));
return 0;
}
<stddef.h> gives the byte offset of a member from the beginning of its containing struct. It is the only portable way to calculate field positions, especially when padding may exist. The TLPI code uses this to compute exactly the size of the “mtext” portion of the request struct, which starts at clientId (not mtype).The RESP_MT_* constants define a simple 3-state protocol for file transfer. The mtype field of each response message indicates what kind of message it is:
mtype=1
RESP_MT_FAILURE
mtype=2
RESP_MT_DATA (×N)
mtype=3, RESP_MT_END
| Constant | Value | Meaning | data field |
|---|---|---|---|
| RESP_MT_FAILURE | 1 | File could not be opened | Error message string |
| RESP_MT_DATA | 2 | This message contains file data | Up to 8192 bytes of file content |
| RESP_MT_END | 3 | Transfer complete, no more data | Empty (or ignored) |
The client reads messages in a loop. If it sees RESP_MT_DATA, it writes the data to stdout (or a file). If it sees RESP_MT_END, it exits the loop — transfer done. If it sees RESP_MT_FAILURE, it prints the error and exits.
/* svmsg_file_complete.h
* Full header for file server application.
* Include this in BOTH server and client source files.
*/
#ifndef SVMSG_FILE_H
#define SVMSG_FILE_H
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <stddef.h> /* offsetof() */
#include <limits.h> /* PATH_MAX */
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
/*
* Server's well-known IPC key.
* Both client and server call:
* msgget(SERVER_KEY, ...)
* to locate/create the server's request queue.
*
* The value 0x1aaaaaa1 is arbitrary — any unique hex constant works.
* In production, derive it with ftok() from a known file path.
*/
#define SERVER_KEY 0x1aaaaaa1
/*
* REQUEST MESSAGE
* Sent by client → server via the server's well-known queue.
*/
struct requestMsg {
long mtype; /* Unused as a type; set to 1 by convention */
int clientId; /* Queue ID of client's private response queue */
char pathname[PATH_MAX]; /* Absolute path of file to retrieve */
};
/*
* REQ_MSG_SIZE: exact size of the mtext portion (everything after mtype).
*
* We use offsetof() to safely skip over:
* - mtype itself (not part of mtext)
* - any padding the compiler places before clientId
*
* This ensures msgsnd() sends exactly the right number of bytes.
*/
#define REQ_MSG_SIZE \
(offsetof(struct requestMsg, pathname) \
- offsetof(struct requestMsg, clientId) \
+ PATH_MAX)
/*
* RESPONSE MESSAGE
* Sent by server child → client via the client's private queue.
* mtype indicates the protocol state (see RESP_MT_* below).
*/
#define RESP_MSG_SIZE 8192 /* Max bytes per response chunk */
struct responseMsg {
long mtype; /* RESP_MT_FAILURE, RESP_MT_DATA, or RESP_MT_END */
char data[RESP_MSG_SIZE]; /* File chunk or error string */
};
/*
* Response protocol states (used as mtype values):
*
* RESP_MT_FAILURE (1): Server could not open the file.
* data[] contains an error description.
*
* RESP_MT_DATA (2): data[] contains up to RESP_MSG_SIZE bytes
* of file content. Multiple DATA messages may follow.
*
* RESP_MT_END (3): No more data. Transfer complete.
* Client should stop reading.
*/
#define RESP_MT_FAILURE 1
#define RESP_MT_DATA 2
#define RESP_MT_END 3
#endif /* SVMSG_FILE_H */
#include <stdio.h>
#include <stddef.h>
#include <limits.h>
#include <sys/msg.h>
struct requestMsg {
long mtype;
int clientId;
char pathname[PATH_MAX];
};
#define REQ_MSG_SIZE \
(offsetof(struct requestMsg, pathname) \
- offsetof(struct requestMsg, clientId) \
+ PATH_MAX)
/* A struct with visible padding to illustrate the concept */
struct withPadding {
char a; /* 1 byte */
/* 3 bytes padding (on 32-bit) or 7 bytes padding (on 64-bit) */
int b; /* 4 bytes */
char c; /* 1 byte */
/* 3 bytes padding (to align d to 4-byte boundary) */
int d; /* 4 bytes */
};
int main(void) {
printf("=== requestMsg field offsets ===\n");
printf("offsetof(mtype) : %2zu bytes from struct start\n",
offsetof(struct requestMsg, mtype));
printf("offsetof(clientId) : %2zu bytes from struct start\n",
offsetof(struct requestMsg, clientId));
printf("offsetof(pathname) : %2zu bytes from struct start\n",
offsetof(struct requestMsg, pathname));
printf("sizeof(requestMsg) : %zu\n", sizeof(struct requestMsg));
printf("REQ_MSG_SIZE : %zu\n", (size_t)REQ_MSG_SIZE);
printf("Naive calculation : %zu (may be WRONG if padding exists!)\n",
sizeof(struct requestMsg) - sizeof(long));
printf("\n=== Struct padding illustration ===\n");
printf("sizeof(withPadding): %zu (expected ~12, actual may be larger)\n",
sizeof(struct withPadding));
printf("offset of 'b' : %zu\n", offsetof(struct withPadding, b));
printf("offset of 'c' : %zu\n", offsetof(struct withPadding, c));
printf("offset of 'd' : %zu\n", offsetof(struct withPadding, d));
return 0;
}
/* Sample output (64-bit Linux):
* ==========================================================
* offsetof(mtype) : 0 bytes from struct start
* offsetof(clientId) : 8 bytes from struct start
* offsetof(pathname) : 12 bytes from struct start (no padding here)
* sizeof(requestMsg) : 4108
* REQ_MSG_SIZE : 4100 (8 + 4096 — just clientId + pathname)
* Naive calculation : 4100 (same here, but may differ on other ABI)
* ===========================================================
*/
This shows how the client handles the three RESP_MT_* message types:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/msg.h>
/* Include svmsg_file.h here in real code */
#define RESP_MT_FAILURE 1
#define RESP_MT_DATA 2
#define RESP_MT_END 3
#define RESP_MSG_SIZE 8192
struct responseMsg {
long mtype;
char data[RESP_MSG_SIZE];
};
/*
* readFileFromServer()
*
* Reads all response messages from clientQid and writes
* file contents to stdout. Returns 0 on success, -1 on failure.
*/
int readFileFromServer(int clientQid) {
struct responseMsg resp;
ssize_t msgLen;
while (1) {
/* msgtyp = 0 → accept any message from this private queue */
msgLen = msgrcv(clientQid,
&resp,
sizeof(resp) - sizeof(long),
0, /* Accept any mtype */
0); /* Block until message available */
if (msgLen == -1) {
perror("msgrcv in client");
return -1;
}
switch (resp.mtype) {
case RESP_MT_DATA:
/* Write this chunk to stdout */
if (write(STDOUT_FILENO, resp.data, msgLen) != msgLen) {
perror("write");
return -1;
}
break;
case RESP_MT_END:
/* Transfer complete — stop reading */
fprintf(stderr, "\n[File transfer complete]\n");
return 0;
case RESP_MT_FAILURE:
/* Server reported an error */
resp.data[msgLen] = '\0'; /* Ensure null termination */
fprintf(stderr, "Server error: %s\n", resp.data);
return -1;
default:
/* Unexpected mtype — protocol violation */
fprintf(stderr, "Unexpected mtype: %ld\n", resp.mtype);
return -1;
}
}
}
int main(void) {
/* In real usage, create private queue, send request, then call: */
/* return readFileFromServer(clientQid); */
printf("Function defined successfully — integrate with full client.\n");
return 0;
}
The TLPI file server performs no authentication of clients. Any user who can run the client program can request any file that the server process has read access to. If the server runs as root, it could potentially serve any file on the system — including /etc/shadow.
A production server would need to: (1) verify the client’s identity using the UID from the IPC permission structure, (2) check that the requesting UID has permission to read the file, or (3) drop privileges and run with limited access. TLPI notes this limitation explicitly and leaves enhancement as an exercise.
Additionally, the server trusts the pathname field in the request without sanitization. A path like ../../etc/passwd (relative path traversal) or /etc/shadow (absolute) could be requested. Always validate pathnames in production servers.
Interview Questions — File Server Application Design
REQ_MSG_SIZE defines the exact size of the mtext portion of the request message — everything after the long mtype field. It is used as the msgsz argument in msgsnd().
offsetof() is used because the C compiler may insert invisible padding bytes between struct members for alignment. A naive calculation of sizeof(requestMsg) - sizeof(long) would include the mtype field’s own size and any padding, potentially giving a larger number than the actual payload size. offsetof(struct requestMsg, pathname) - offsetof(struct requestMsg, clientId) + PATH_MAX gives the exact size from the start of clientId to the end of pathname — the true mtext size.
mtype field in response messages:RESP_MT_FAILURE (1) — File could not be opened;
data[] contains an error description.RESP_MT_DATA (2) —
data[] contains a chunk of file content (up to RESP_MSG_SIZE bytes).RESP_MT_END (3) — Transfer complete; client should stop reading.
The client receives messages with msgtyp = 0 (accept any type) and switches on the mtype: for DATA it writes to stdout, for END it exits the loop, for FAILURE it prints the error.
ftok(pathname, projId) generates a key from a file’s inode number and a project ID, which makes it more portable and unique — but it requires both the server and client to agree on the same file path and project ID. A hardcoded constant like 0x1aaaaaa1 works fine for teaching examples as long as it doesn’t collide with another application’s key. In production, ftok() is preferred because hardcoded keys may conflict between different applications on the same system.PATH_MAX (defined in <limits.h>) is the maximum length of a file path on the system — typically 4096 bytes on Linux. It is used as the size of the pathname field in requestMsg to ensure the pathname buffer is large enough to hold any valid absolute path. Using PATH_MAX instead of an arbitrary constant makes the code portable — if PATH_MAX changes on a different system, the struct adapts automatically.read(fd, resp.data, RESP_MSG_SIZE). Each successful read produces one DATA message containing up to 8192 bytes. The client receives these in order and writes each chunk to its output. After the last read (when read() returns 0 indicating EOF), the server sends a single RESP_MT_END message to signal completion. The client’s loop exits when it sees RESP_MT_END.ds.msg_perm.uid of the client’s queue against the file’s ownership, or use Unix credentials passing.2. Path traversal: A client can request arbitrary paths including
/etc/shadow. Fix: validate that the requested path is within an allowed directory using realpath() and prefix checking.3. Null termination: The pathname in the request may not be null-terminated if the client is malicious. Fix: force
req.pathname[PATH_MAX-1] = '\0' before use.4. No rate limiting: A client can flood the server with requests, causing many forks. Fix: limit concurrent children or use a thread pool instead.
mtype in responseMsg serves as a protocol state indicator (FAILURE/DATA/END), not as a routing key. Routing is already handled by using separate queues — the server sends to the client’s specific queue, so only that client will read it.
In the single-queue design, mtype in response messages equals the client’s PID, and its primary role is routing — the client filters messages by its own PID using msgrcv(msgtyp = getpid()).
This dual use of mtype — routing in single-queue vs. typing/classifying in per-client-queue — is an important distinction to understand.
Part 5: Listing Queues Single Queue Design Per-Client Queue Design EmbeddedPathashala Home
