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.
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.
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:
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:
handle_stdin()
helper functionhandle_client()
helper functionUpon 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.
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:
SIGINT
.SIGINT
so that both processes
terminate.handle_stdin()
and handle_client()
to
distinguish between SIGINT
interruption and other errors.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()
.
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
.
int clients[MAX_CLIENTS];
// Initialize client socket array. Empty slots are indicated by -1
for (int i = 0; i < MAX_CLIENTS; i++) {
clients[i] = -1;
}
Define MAX_CLIENTS
at the top of cowchat.c
to some small value for ease of
testing, e.g., 4.
select()
âs read file descriptor set
if there are available slots in the client socket array so that new
connections are not accepted until there are slots available in the array.
Connections are enqueued up to the limit specified in the setup call to
listen()
.cowchat
runs in an infinite loop. cowchat
will exit cleanly when:
Ctrl-D
on the serverâs terminal)SIGINT
(i.e., Ctrl-C
on the serverâs terminal)Connection errors encountered when communicating with clients or client
disconnections should not terminate cowchat
; you should instead close the
client connection and free up the corresponding slot in the client socket
array.
Weâve put our solution executable in /opt/asp/bin
for your reference.
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:
cowsay
command.
The following code block demonstates how to do this:
const char *cow_command = "/cowsay ";
const size_t cow_len = strlen(cow_command);
if (strncmp(line, cow_command, cow_len) == 0) {
// The line starts with /cowsay followed by a space
} else {
// The line didn't match
}
execlp()
to execute the cowsay
program. The rest of the line after the cow_command
substring should be
treated as the argument to cowsay
. As such, youâll have to make sure the
argument is null-terminated. One way to do this is to simply call recv()
with MAX_LINE_SIZE - 1
bytes so you can always write a null-terminator into
line[MAX_LINE_SIZE]
.cowsay
process will be redirected to a pipe. However, the
cowchat
server canât block on reading from this pipe so that it can continue
to serve other clients. Instead, the cowchat
server will declare a single
persistent pipe cow_pipe
before the main for (;;) loop. It will add the
read-end of the cow_pipe
to the selectâs read set and use write-end of the
cow_pipe
for all cowsay
processes that it forks. If select()
indicates
that the read-end of the cow_pipe
has data, cowchat
should read a chunk
(i.e., 4096 bytes) from the pipe and broadcast it to all clients and write it
to its stdout. Note that it is possible for cowsay
output larger than 4096
bytes to be interleaved if several clients attempt to use /cowsay
at the
same time â this is OK.cowsay
child processes become zombies. Make sure that
cowchat
calls waitpid()
immediately after one or more child cowsay
process have terminated.
waitpid()
inside the main for (;;) loop?
Obviously we cannot let waitpid()
block until a cowsay
child process
terminates for the same reason we donât block on reading from the
cow_pipe
â we want cowchat
to be able to immediately serve clients.
You will need to call waitpid()
in a non-blocking way. (Hint: look into
the WNOHANG
flag.) But even if you make it non-blocking, can you make
your cowchat
parent process call it immediately after a cowsay
child
process terminates? What if the cowchat
is being blocked on select()
?select()
is special in that, even if the SA_RESTART
option is
specified, the select()
function is not restarted under most UNIX systems.
Make sure you handle this behavior properly.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.
Here are some suggestions based on our implementation. You donât have to follow these.
mmap()
âd into memory by the
cowchat
servers. The file is formatted as an array of the following structure:
struct registry_entry {
unsigned short port;
unsigned short num_connected;
}
Since 0 is an invalid port number, you can use port number 0 to indicate an empty slot in the registry.
/dev/shm
.num_connected
before initiating the transfer.
num_connected
again before
accept()
-ing a new client connection because a source server can reserve
the target serverâs last empty slot for transfer while the target server
was select()
-ing on its server-listening socket. The new client
connection will remain queued up until there is space on the target
server.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