cowchat

HW3: cowchat 🐄

Submission

We will be using GitHub for distributing and collecting your assignments. At this point you should already have a repository created on your behalf in the cs4157-hw GitHub org. Follow the instructions on the class listserv to get access to that repository.

To obtain the skeleton files that we have provided for you, you need to clone your private repository. Your repository page should have a button titled “< > Code”. Click on it, and under the “Clone” section, select “SSH” and copy the link from there. For example:

$ git clone git@github.com:cs4157-hw/hw3-<id>-<your-team-name>.git

The TAs will use scripts to download, extract, build, and display your code. It is essential that you DO NOT change the names of the skeleton files provided to you. If you deviate from the requirements, the grading scripts will fail and you will not receive credit for your work.

You need to have at least 5 git commits total, but we encourage you to have many more. Your final submission should be pushed to the main branch.

As always, your submission should not contain any binary files. Your program must compile with no warnings, and should not produce any memory leaks or errors when run under valgrind. This requirement applies to all parts of the assignment.

At a minimum, README.txt should contain the following info:

The description should indicate whether your solution for the part is working or not. You may also want to include anything else you would like to communicate to the grader, such as extra functionality you implemented or how you tried to fix your non-working code.

Answers to written questions, if applicable, must be added to the skeleton file we have provided.

Overview

In this assignment, you will implement a chat server called cowchat.

In parts 1-2, cowchat will provide two-way communication with a single client, much like nc -l. The server will fork a child process so that the two-way communication can be implemented using blocking I/O.

Starting from part 3, we will enable multi-client communication and other features. Instead of forking, cowchat will use select() to serve multiple clients.

Part 1: Cow’s First Moo

Reading

Tasks

This part is intended to be a warm up for the assignment. Focus on understanding the provided skeleton code: programming using the Sockets API and handling I/O errors. The actual code you need to write for this part is minimal.

Complete the main() function of part1/cowchat.c to establish a two-way communication with a client. The provided skeleton code already creates a server socket and accepts a client connection.

cowchat server will provide similar two-way communication semantics as nc -l does:

  1. Reads lines from its stdin and sends them to the client
  2. Receives lines from the client and prints them to its stdout

You’ll use nc <host> <port> as a client to connect to the cowchat server.

After accepting a client connection, your cowchat should call fork() to handle each side of the two-way communication described above:

Upon receiving EOF on stdin (i.e., Ctrl-D on the server’s terminal), the cowchat parent process will wait for the child process to terminate. The child process will terminate when the client exits (i.e., Ctrl-C on the client’s terminal). We will modify this two-step termination behavior in part 2.

Although strictly not necessary, it is good practice to close any file descriptors as soon as they are no longer needed. Please follow this practice for all parts of this assignment.

Part 2: Grazeful Exit

Tasks

In part 1, we had to terminate the parent process and the child process separately. In this part, we will modify the code such that terminating either process will ensure that the other process terminates. You should start by copying your part1 directory to part2.

We will achieve this using SIGINT as follows:

Incidentally, handling SIGINT as described above will also allow graceful exit when you press Ctrl-C on the server’s terminal. Don’t forget to handle SIGINT when cowchat is blocked on accept().

Part 3: Feeding Udder Clients

We will now expand cowchat to communicate with multiple clients at the same time. Messages sent by any client will be forwarded to all other participants and printed to the server’s stdout. Messages read from the server’s stdin should also be forwarded to all other participants. Clients can connect and disconnect at any time.

Instead of forking as we did in parts 1-2, cowchat will be a single process that uses select() to multiplex between multiple blocking I/O calls – i.e., reading from stdin, accepting new clients, and receiving messages from clients. You should start by copying your part2 directory to part3.

We’ve put our solution executable in /opt/asp/bin for your reference.

Part 4: Cow Talks Dairy

In this part, we will add a special command to our cowchat server. If a client sends /cowsay <msg>, the server will broadcast the output of cowsay msg to all of the connected clients and print it to its stdout. Try running cowsay moo moo im a cow on SPOC if you aren’t familiar with this program. The part4 solution executable in /opt/asp/bin implements this functionality.

Start by copying your part3 to part4 and then do the following:

Part 5: Mooving Around (optional)

This part is optional and will not be graded.

It’s possible to run multiple instances of cowchat on the same host. Each one is uniquely identified by the port number it is running on. If a client connected to cowchat running on port A wants to join the cowchat running on port B, they’ll first have to quit their current cowchat session on A and connect to the session on B.

We’ll now introduce a new special command to make moving between cowchat sessions on the same host easier. If a client sends /moovme <port>, the source server will transport its open socket descriptor connected with the client to the target server uniquely identified by <port>. This can be accomplished by passing the open file descriptor through a domain socket.

We leave the design and implmentation of this feature open-ended. We encourage you to think about how to maintain a registry of cowchat servers identified by their port numbers and how many connected clients each server has. Recall that each server has a fixed-size array of the number of clients they can maintain. You’ll have to implement proper synchronization to make sure that the target server has vacancy before a client can be transported to it.

Implementation Suggestions

Here are some suggestions based on our implementation. You don’t have to follow these.

Sending open file descriptors through domain sockets

The source server invokes the following function to transfer an open socket descriptor, clnt_sock. It calls connect() to set the target server’s domain socket as the destination.

void transport_client_socket(int clnt_sock, char *target_dom_sock_name)
{
    struct sockaddr_un	un;

    if (strlen(target_dom_sock_name) >= sizeof(un.sun_path)) {
        errno = ENAMETOOLONG;
        die("name too long");
    }

    int fd, len;

    // create a UNIX domain datagram socket
    if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
        die("socket failed");

    // fill in the socket address structure with server's address
    memset(&un, 0, sizeof(un));
    un.sun_family = AF_UNIX;
    strcpy(un.sun_path, target_dom_sock_name);
    len = offsetof(struct sockaddr_un, sun_path) + strlen(target_dom_sock_name);

    if (connect(fd, (struct sockaddr *)&un, len) < 0) {
        die("connect");
    }

    send_connection(clnt_sock, fd);

    close(fd);
}

Here is send_connection() which is called by transport_client_socket():

void send_connection(int clnt_sock, int dom_sock)
{
    struct msghdr msg;
    struct iovec iov[1];

    union {
        struct cmsghdr cm;
        char control[CMSG_SPACE(sizeof(int))];
    } ctrl_un = {0};
    struct cmsghdr *cmptr;

    msg.msg_control = ctrl_un.control;
    msg.msg_controllen = sizeof(ctrl_un.control);

    cmptr = CMSG_FIRSTHDR(&msg);
    cmptr->cmsg_len = CMSG_LEN(sizeof(int));
    cmptr->cmsg_level = SOL_SOCKET;
    cmptr->cmsg_type = SCM_RIGHTS;
    *((int *) CMSG_DATA(cmptr)) = clnt_sock;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    iov[0].iov_base = "FD";
    iov[0].iov_len = 2;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    if (sendmsg(dom_sock, &msg, 0) != 2)
        perror("send_connection");
}

When a target server’s domain socket is set for reading by select(), it should call recv_connection() to receive the incoming open socket descriptor:

int recv_connection(int dom_sock)
{
    struct msghdr msg;
    struct iovec iov[1];
    ssize_t n;
    char buf[64];

    union {
        struct cmsghdr cm;
        char control[CMSG_SPACE(sizeof(int))];
    } ctrl_un;
    struct cmsghdr *cmptr;

    msg.msg_control = ctrl_un.control;
    msg.msg_controllen = sizeof(ctrl_un.control);

    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    iov[0].iov_base = buf;
    iov[0].iov_len = sizeof(buf);
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    for (;;) {
        n = recvmsg(dom_sock, &msg, 0);
        if (n == -1) {
            if (errno == EINTR)
                continue;
            die("Error in recvmsg");
        }
        // Messages with client connections are always sent with
        // "FD" as the message. Silently skip unsupported messages.
        if (n != 2 || buf[0] != 'F' || buf[1] != 'D')
            continue;

        if ((cmptr = CMSG_FIRSTHDR(&msg)) != NULL
            && cmptr->cmsg_len == CMSG_LEN(sizeof(int))
            && cmptr->cmsg_level == SOL_SOCKET
            && cmptr->cmsg_type == SCM_RIGHTS)
            return *((int *) CMSG_DATA(cmptr));
    }
}

Last Updated: 2024-03-06