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()
. Note that there is a very small
chance that the SIGINT
will be delivered when cowchat is not blocked on accept()
or fgets()
. You do not have to handle this case.
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.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 connection through a named domain socket. Each server can maintain a named domain socket that it will select()
on for âmoovâdâ connections.
Recall that each server has a fixed-size array of the number of clients it can maintain. We can maintain a registry file of cowchat servers that maps each serverâs port number to its current number of connected clients. Youâll have to implement proper synchronization to make sure that the target server has a vacancy before a client can be transported to it.
We leave the rest of the design and implementation open-ended.
Include a description of your design and any simplifying
assumptions you make in your README.txt
. The goal of this
part is to give you the opportunity to design a system from a
set of underspecified objectives. As such, thereâs no one
correct solution to this part. We intend to grade leniently;
any reasonable design and implementation will receive
substantial credit.
The cowchat server will have to take in additional command line arguments so that it can find the registry file, the directory containing all the named domain sockets for the servers in the registry, and any other synchronization objects in the file system.
You will need some synchronization mechanism to control access to the registry. Note that the cowchat servers are unrelated processes, so choose accordingly.
On startup, a cowchat server add itself to the registry. It should assume that the registry file and
other shared objects have been initialized already. Include any initialization programs/scripts in your
submission that should be run prior to starting up cowchat servers. Document the steps to initialize your
environment and run your cowchat servers in your README.txt
.
Ensure there is room on the target server to receive an incoming transfer. If there is no room to receive an incoming transfer, then the âmoovâ command should fail before initiating the transfer and report some error message to the client. If the âmoovâ command is accepted, ensure the transfer doesnât fail. You can assume there is no network or system failure.
On exit, the cowchat server should delete itself from the registry file and delete its named domain socket.
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-10-09