3 of 4
Server Logic
msgsnd, read, open
FAILURE/DATA/END
What This Part Covers
Once the server forks a child process, the child calls serveRequest(). This function is the heart of the file-serving logic. It opens the requested file, sends its contents in chunks as RESP_MT_DATA messages, and signals the end of transfer with a RESP_MT_END message. If the file cannot be opened, it sends a RESP_MT_FAILURE message.
This part walks through every step of that logic, explains the design choices, and shows what the client-side sees in response.
Key Terms
serveRequest() receives a pointer to the requestMsg struct that the parent read from the server queue. From this, it gets two things:
- req->pathname — the file to open and serve
- req->clientId — the message queue ID to send responses to
The function follows this decision tree:
mtype = RESP_MT_FAILURE
data = “Couldn’t open”
exit(EXIT_FAILURE)Send each as RESP_MT_DATA
zero-length mtext
If open() fails (file not found, permission denied, etc.), the server child sends a single RESP_MT_FAILURE message with a short error description in the data field, then calls exit(EXIT_FAILURE).
fd = open(req->pathname, O_RDONLY);
if (fd == -1) {
resp.mtype = RESP_MT_FAILURE;
snprintf(resp.data, sizeof(resp.data),
"%s", "Couldn't open");
/* Send error text to client.
Size = strlen(data) + 1 to include the null terminator. */
msgsnd(req->clientId, &resp,
strlen(resp.data) + 1, 0);
exit(EXIT_FAILURE); /* child exits */
}
| Field | Value | Why |
|---|---|---|
resp.mtype |
RESP_MT_FAILURE |
Client checks this type first to detect errors |
resp.data |
“Couldn’t open” | Human-readable error text the client can print |
| size arg to msgsnd | strlen(data) + 1 |
Include null terminator so client can print as C string |
exit(EXIT_FAILURE) here. The key insight from the book is that in a real robust server you might want stdio flushing for error logging — the child’s exit path is different from the normal “work done” path.If the file opens successfully, the server reads it in chunks of RESP_MSG_SIZE bytes. Each chunk is sent as a separate message with mtype = RESP_MT_DATA.
resp.mtype = RESP_MT_DATA;
while ((numRead = read(fd, resp.data, RESP_MSG_SIZE)) > 0)
if (msgsnd(req->clientId, &resp, numRead, 0) == -1)
break; /* can't notify client — just stop */
/* Signal end of file with a zero-length mtext message */
resp.mtype = RESP_MT_END;
msgsnd(req->clientId, &resp, 0, 0);
Key points in this loop:
| Detail | Explanation |
|---|---|
read(fd, resp.data, RESP_MSG_SIZE) |
Reads up to RESP_MSG_SIZE bytes into the message data buffer. May return less than RESP_MSG_SIZE at end of file or for special files. |
msgsnd(..., numRead, 0) |
The size is the actual number of bytes read — not RESP_MSG_SIZE. This avoids sending garbage padding bytes. |
| No error handling on failure | If msgsnd() fails mid-transfer, there is no way to notify the client (no reverse channel at this point). The server just stops. The client will eventually time out or get stuck — a known limitation discussed in Exercise 46-5. |
After all data messages are sent, the server sends one final message with mtype = RESP_MT_END and a zero-length data field. This is the signal to the client that the entire file has been transferred.
resp.mtype = RESP_MT_END;
msgsnd(req->clientId, &resp, 0, 0);
/* ^
zero-length mtext */
Sequence of messages the client receives for a 20KB file (RESP_MSG_SIZE = 8192):
| # | mtype | data size | Meaning |
|---|---|---|---|
| 1 | RESP_MT_DATA (2) | 8192 bytes | First 8KB of file |
| 2 | RESP_MT_DATA (2) | 8192 bytes | Next 8KB of file |
| 3 | RESP_MT_DATA (2) | 3616 bytes | Remaining bytes |
| 4 | RESP_MT_END (3) | 0 bytes | Transfer complete |
RESP_MT_END) instead of a length-prefix or special data value is clean and simple. The client does not need to know the file size in advance — it just keeps receiving until it sees RESP_MT_END.#include <fcntl.h>
#include <unistd.h>
#include <sys/msg.h>
#include <string.h>
#include "svmsg_file.h"
static void
serveRequest(const struct requestMsg *req)
{
int fd;
ssize_t numRead;
struct responseMsg resp;
/* Try to open the file the client requested */
fd = open(req->pathname, O_RDONLY);
if (fd == -1) {
/* Tell client we couldn't open it */
resp.mtype = RESP_MT_FAILURE;
snprintf(resp.data, sizeof(resp.data),
"%s", "Couldn't open");
msgsnd(req->clientId, &resp,
strlen(resp.data) + 1, 0);
exit(EXIT_FAILURE);
}
/* File opened OK — stream contents to client */
resp.mtype = RESP_MT_DATA;
while ((numRead = read(fd, resp.data, RESP_MSG_SIZE)) > 0)
if (msgsnd(req->clientId, &resp, numRead, 0) == -1)
break;
/* Send end-of-file marker */
resp.mtype = RESP_MT_END;
msgsnd(req->clientId, &resp, 0, 0);
}
This is a subtle design decision. Once the server has successfully opened the file and started sending data, if read() or msgsnd() fails:
- The server has no second channel to notify the client that an error occurred mid-transfer.
- The only communication channel is the client’s message queue — and if
msgsnd()is failing, that channel itself is broken. - Adding complex error recovery would require a separate signaling mechanism.
The book acknowledges this limitation and suggests improvements in Exercise 46-4 (server improvements) and Exercise 46-5 (client improvements).
RESP_MT_END does not arrive within N seconds, assume failure), or use a separate error notification mechanism.The msgsnd() and msgrcv() size arguments refer only to the data part of the message — the mtype field is never counted:
struct responseMsg {
long mtype; /* NOT counted in size argument */
char data[8192]; /* THIS is what the size refers to */
};
/* Sending actual read bytes (not the full buffer): */
msgsnd(clientId, &resp, numRead, 0);
/* Sending zero-length end marker: */
msgsnd(clientId, &resp, 0, 0);
/* Receiving — use max possible size: */
ssize_t n = msgrcv(clientId, &resp,
sizeof(resp.data), 0, 0);
/* n == 0 means RESP_MT_END was received (zero data) */
msgrcv() returns 0 (zero bytes of data), it means a zero-length message was received. In this application that is the RESP_MT_END signal. However, the client actually checks mtype to distinguish DATA from END rather than relying on size alone.- What are the three response message types the server sends and in what situations?
- Why does
msgsnd()usenumReadas the size argument instead ofRESP_MSG_SIZE? - What does a zero-length
msgsnd()call signify in this application? - The server does not handle
read()errors during file transfer. Why? What are the consequences? - How does the client know when the file transfer is complete?
- The
mtypefield is never included in the size argument tomsgsnd(). Why? - Why does the server child call
exit(EXIT_FAILURE)after sending the failure message? - What would happen if the server sent all data in one huge message instead of multiple chunks?
- In
serveRequest(), what determines how many messages are sent for a given file? - If the client’s queue is full when the server tries to send a data message, what happens by default?
Continue Learning
Next: Client-Side Design — IPC_PRIVATE, atexit(), receiving responses
