3

I am currently learning sockets programming using C in a Linux environment. As a project I am attempting to write a basic chat server and client.

The intention is to have the server fork a process for each client that connects.

The problem that I am having is reading data in one child and writing it to all of the connected clients.

I have tried to accomplish this by looping on a call to select in the child that waits for data to arrive on the socket or read end of the pipe. If it arrives on the socket the idea is that it writes to the write end of the pipe which causes select to return the read end of the pipe as ready for reading.

As this pipe is shared between all the children, each child should then read the data on the pipe. This does not work as the data pipe, it appears, cannot be read by each child process at the same time and the children that "miss" the data block in the call to read.

Below is the code in the child process that does this:

for( ; ; )
{
  rset = mset;
  if(select(maxfd+1, &rset, NULL, NULL, NULL) > 0)
  {
    if(FD_ISSET(clientfd, &rset))
    {
      read(clientfd, buf, sizeof(buf));
      write(pipes[1], buf, strlen(buf));
    }
    if(FD_ISSET(pipes[0], &rset))
    {
      read(pipes[0], buf, sizeof(buf));
      write(clientfd, buf, sizeof(buf));
    }
  }
}

I am presuming the method that I am currently using simply will not work. Is it going to be possible for messages received from a client to be written to all of the other connected clients via IPC?

Thanks

synic
  • 43
  • 1
  • 1
  • 4

2 Answers2

3

To get around the problem of a child reading from the pipe more data than it should (and in-turn making another child get "stuck" trying to read from an empty pipe), you should probably look into using either POSIX message queues or a single pipe between the parent and an individual child processes rather than a single global pipe to communicate between the parent and child processes. As it stands right now, when the server writes to the pipe to communicate with its children, it's not really able to control exactly which child will read from the pipe at any given time, since the scheduling of processes by the OS is non-deterministic. In other words, without some type of synchronizing mechanism or read/write barriers, if the server writes to the pipe, there is nothing in your code stopping one child from "skipping" a read, and a second child from doing a double-read, leaving another child that should have gotten the broadcasted data from the server starved, and therefore blocked.

A simple way around this could again be to have a single private pipe shared between the parent and an individual child. Thus in the server the child processes can read from the client, send that data back to the parent process, and the parent can then, using the entire list of pipe descriptors that it's accumulated for all the children, write back to each individual child process the broadcast message which is then sent back to each client. No child ever gets "starved" of data since there is no possibility of a double-read by another child-process. There is only a single reader/writer on each pipe, and the communication is deterministic.

If you don't want to handle multiple pipes for each child in the server's parent process, you could use a global message queue using POSIX message queues (found in mqueue.h). With that approach, if a child grabs a message that it's not suppose to have (i.e., you would need to pass around a struct that contained some type of ID value), it would place the message back in the queue and attempt to read another message ... that's not quite as efficient speed-wise as the direct pipe approach, but it would allow you to write-back a message that was not designated for the current child without the interleaving complications that would take place with a global pipe or FIFO mechanism.

Jason
  • 31,834
  • 7
  • 59
  • 78
  • BTW, the other problem with having multiple readers on a pipe is that reads on pipes are not atomic ... in other words if you have a `read` call, you may not get the entire message on a single call to `read`. If there is contention with multiple readers, that can create data corruption issues. While `write` operations up to size `PIPE_BUF` are atomic, the fact that calls to `read` are not means that multiple readers on a single pipe can be quite a pain if you are reading a multi-byte or variable-length data-string. – Jason Aug 22 '11 at 20:32
  • Ah ok so if one process calling read on an FD has to loop round and call read again to get the rest of the data, is it possible that in this gap another process could "steal" the read and lock the other out ? – synic Aug 22 '11 at 20:39
  • Well, there are two possibilities ... 1) two processes are trying to read from a pipe. The OS schedules the first process, which does a read. Then since both processes are on a loop, it skips the second process and just allows the first process to read again, "stealing" the data from the second, and now the second process is stuck. 2) The first process does a `read` but doesn't get all the data. So it's data is corrupted. Then the second process does a `read` but corrupted data since it's not getting its message, but is getting part of the first process' message, and part of it's own. – Jason Aug 22 '11 at 20:45
  • If scenario #1 sounds confusing, consider the fact that between the time you call `if(FD_ISSET(pipes[0], &rset))` and `read(pipes[0], buf, sizeof(buf));` in the child process, the OS could put the process to sleep. The OS can also keep the process asleep for as long as it wants. So between those two lines, if there are a bunch of other processes that are trying to read from the same pipe, you could end up with those processes reading all the data in the pipe, and then when the OS wakes the current process back up, it just blocks on `read` until new messages are written to the pipe. – Jason Aug 22 '11 at 20:48
  • Thanks for the info, using a single pipe between all of the processes is unmanageable and not in the least reliable then. I didn't even know of message queues to be honest so I'll take look at using them as the alternative. – synic Aug 22 '11 at 21:06
  • Don't miss the part in my post where you can also create a pipe per child ... so every pipe only has a single reader/writer, and that removes the issues with in-determinant reads. You may find that an easier concept to work with than message queues – Jason Aug 22 '11 at 21:50
3

Each byte of data written to a pipe will be read exactly once. It isn't duplicated to every process with the read end of the pipe open.

If you want the data duplicated to multiple destination processes, you have to explicitly duplicate the data. For example, you could have one "master" process that has a pipe to and from every "slave" process. When a slave wants to broadcast a message to the other slaves, it sends it to the master process, which loops around and writes it once to each pipe going to the other slaves.

caf
  • 233,326
  • 40
  • 323
  • 462