Due Dates

  • Part1, checkpoint: before 11:59pm, Wednesday Sept. 22 push to git repo and tag with Part1 tag.

    You may not use late days on lab checkpoints, but you should still complete Part1 if you do not complete it by the checkpoint due date.

  • Part2: before 11:59pm, Wednesday Sept. 29 push to git repo and tag with Part2 tag.

Lab2 Partners

This lab will be done with your Lab 2 Partner

See the documentation on the course webpage about working with partners. It is a brief guide for effective practices and my expectations for how CS students should be working together on labs.

Lab 2 Goals

  • Learn socket programming by implementing two versions of a TCP socket client-server program.

  • Implement a specific message protocol, and test correctness by seeing that your client/server can communicate with other group’s server/client.

  • Gain more practice with pthreads multi-threaded programming and synchronization.

  • More practice with C programming, C strings in particular.

Overview

The main focus of this lab is learning client-server socket programming and implementing a specific message protocol for clients and servers to talk to each other. You will implement two versions of a chat or talk client and server program to allow users on different machines to talk. You will be given a protocol for how clients and servers communicate, and your solution must implement the protocol such that your clients and servers can communicate with other group’s.

In this lab you will implement 2 different versions of the client and server.

  • The Part 1 version: allows for one-at-a-time chat session between a single client on one machine and a single server on another machine. When the current chat session ends, another client can connect to the server end and start a new chat session. The client and server alternate sending messages back and forth (in a question and answer type style).

    The Part 1 solution is due in one week. I encourage you to finish this part early and get started in on Part2.

  • The Part 2 version: allows for a chat session between multiple clients. In this version the server does not participate in the chat itself, but fosters allowing multiple clients to connect to the chat session. Clients participating in the group chat send their message to the server and the server shares it with all of the other clients participating in the chat session. In this version, clients can "speak" at anytime; there is no specified ordering or taking turns writing messages to the group. Individual clients can also come and go in a chat session.

    The Part 2 solution is due in two weeks.

This assignment is very loosely based on Unix talk, which was a very early Chat or IM system. (Note that you will NOT be implementing the Unix talk protocol in this lab; instead you will implement a different protocol that has some similar general functionality.)

Starting Point Code

  1. Clone your Lab 2 repo from the CS87 git org:

    git clone [your_Lab01_ssh_URL]

    If all was successful, you should see the following files when you run ls:

    Makefile README.md client.c cs87talk.c cs87talk.h server.c

    If this didn’t work, or for more detailed instructions on git see: Git Help Page

Starting Point Files

With the starting point are several files, many have some starting point code written for you. These files include:

  • cs87talk.[c,h]: definitions used by both the client and server, and common functions used by both the client and server side should go here. For example, both the client and the server send and receive messages on sockets. I recommend adding some generic message sending and receiving functions here, that can be used by your client and server code.

  • server.c: the server side should be implemented here (there is a lot of starting point code for you in here)

  • client.c: the client side should be implemented here (there is a lot of starting point code for you in here)

  • Makefile: builds client and server executables (cs87_client, cs87_server).

  • README.adoc: some notes to you about code.

Running

To test your client and server, first start the server (cs87_server) on one machine, then start a client (cs87_client) on another machine, specifying the server’s IP address and the chat session name as its command line arguments:

# on some machine start the server:
./cs87_server

# connect to the server on another machine
# list its IP address and your chat session name
# as command line options:
./cs87_client  130.58.68.67 tia

You can use the CS department machine specs page to find other machines to ssh into and run clients or servers.

See Tips and Resources for information on using nslookup, dig, and ifquery to find IP addresses.

Details: Message Protocol

A messaging protocol defines how two parties communicate for a particular service—​the protocol defines both the set of application-level (higher level) message types, the send-recv exchange in handling each message type, and the expected format of messages.

For this lab you will implement the cs87talk protocol that has three different types of messages, corresponding to three different types of communication between cs87talk clients and cs87talk servers. The three are:

  • HELLO: a client introduces itself to the server before talking. The server will tell the client if it can join the chat or not as part of this exchange.

  • MSG: an endpoint will send a message to another endpoint. A client can send a MSG to a server and a server can send a MSG to the client.

  • QUIT: a client tells a server it is done and leaving the talk session.

Each part of the protocol begins with one party sending to the other party a 1 byte message tag. The tag value is used to indicate which of the three parts of the protocol the client and server should run (the expected send-recv sequence between the client and server for the interaction type is indicated by the tag).

Protocols Definitions

Each of the three parts of the cs87talk protocol are defined below. All message types begin by the sender sending a 1 byte message tag indicating the type of message.

As you implement these, make sure to look at the cs87talk.h file for type and constant definitions that you should use in your implementation.

HELLO:

This message type is initiated by the client when it wants to start a talk session with a server (the client should initiate this immediately after the TCP connection has been established and before trying to send talk messages to the server). The send and receive behavior is defined by the following:

client                                server
======                                ======
1. send HELLO_MSG tag -------------> recv 1 byte tag
2. send len -----------------------> recv 1 byte len
3. send name ----------------------> recv a len bytes name string
4. recv 1 byte tag  <--------------- send 1 byte reply tag
                                     (HELLO_OK or HELLO_ERROR)

The HELLO message starts with the client sending 3 values to the server, in this order:

  1. the HELLO_MSG tag as a 1 byte value

  2. a 1 byte len value, which is the number of bytes in the name string

  3. a len bytes message containing the name string value (the string passed as a command line argument to the client program).

On receipt, the server decides whether or not to accept the client’s HELLO request (the decision algorithm is different for Part1 and Part2), and sends a response to the client indicating its decision.

The client then receives the 1 byte tag response from the server, indicating if the server is letting it join the talk session (HELLO_OK:yes, HELLO_ERROR:no).

It the server responds HELLO_ERROR it should terminate its connection with the client after it sends the HELLO_ERROR message. If it replies HELLO_OK, it will update state about this connection, and create a message buffer for this client with the client’s name string as a prefix. All messages set by this client will be prefixed by the string "name: " (where name is the client’s name string) to mark that this message was "said" by this client.

MSG:

This type of message is initiated either from a client to a server or from a server to a client, and is used to send talk messages between the two. Its defined by:

initiator                           other end
=========                           =========
1. send MSG_DATA  ---------------> recv 1 byte tag value
2. send len ---------------------> recv 1 byte len value
3. send data --------------------> recv len byte message

If the server is the "other end" it should store the received message in a string with the a "name:" prefix (the name is obtained from the HELLO protocol) to identify the sender. For example, if the server is communicating with Freya, and Freya initiates a MSG_DATA message, sending "hello there" as the data part, the server will store the received message in a string: "Freya: hello there".

If the client is the "other end" it just receives the message of len bytes and creates a string from the len byte message (be sure to null terminate '\0' before printing out); the client side doesn’t add a prefix to received messages. For example if the server sends the client a MSG_DATA message, with "hello there" as the data part, the client will store the received message in a string "hello there".

QUIT:

This message type is initiated by the client when it wants to drop out of the chat session. The client sends the QUIT message tag to the server notifying it that it is leaving:

client                                server
======                                ======
1. send QUIT ----------------------> recv 1 byte tag value

After sending QUIT, the client should close its end of the socket and exit.

The server then cleans up any state associated with this connection, including closing its end of the socket that was dedicated to this communication.

After receiving a QUIT, the server process should not exit.

  • For Part 1: it should be ready to accept a new client connection for the next chat session.

  • For Part 2: this client has just left the group chat, which will continue with any other clients left in the chat session.

Details: Part 1

Both Part 1 and Part 2 use the same messaging protocol, but the behavior of the client and server is different in the two implementations.

Part 1 is an implementation where one client at a time connects to a server, and the server and client have a dedicated talk session.

In this version, a client connects to a server with the HELLO message, and then the client and server talk back and forth, each taking turns sending a message to the other. The server has a dedicated talk session with this client until the client terminates the connection (either it closes its end of the socket or it sends a QUIT message). The client process should exit after this, but the server should not exit. Instead, the server is now available for another client to connect to it to have a talk session.

Specifically, the behavior should be:

  1. client connects to a server using the TCP protocol. Run the client with the server’s IP and and the client’s chat name as command line args:

    ./cs87_client 130.58.68.67 tia
  2. the client initiates the HELLO message part of the protocol with the server

    • if the server replies HELLO_ERROR, the client exits (no talking will take place)

    • if the server responds with HELLO_OK, then the client and server will start a talk session

  3. the client and server will have a talk session that consists of some number of alternating message exchanges. The first MSG should be sent from client to server, then next from server to client, then by client to server, and so on.

  4. the talk session is ended by either the client sending a QUIT message or the server detecting that the client’s end of the socket has been closed.

An individual message in (3) should be a line of texted entered by the user running the client (or server) side. I suggest using readline to read in a line of user input and return it as a string (and remember you are responsible for freeing the return string when done with it…​run valgrind). If the user enters a string that is longer than the max message size, just truncate the message sent to be just the first max len chars.

Sample Output

Here is sample output from two different Part 1 talk sessions with a talk server. Your client and server program can print out any prompts and messages to the user (they don’t have to be identical to mine), but they should have this same alternating behavior and the server should handle only a single client connection and talk session at a time. Also, each end should print out the message they received from the other (shown in bold in this example). Only after the current talk session ends, can the server accept the next talk session from another client (note where that is in the server output).

Details: Part 2

For Part2, you will change your Part 1 solution to implement a Multi-client chat server. Make sure you have added a git tag for Part1 before changing your code to implement Part2 functionallity.

The Part 2 talk server changes the role of the server, allows multiple clients to simultaneously connect to the same server to all participate in a group chat, allows clients to send messages to others at any time (there is no predefined order or alternating behavior). In addition, new clients can enter an existing chat at any time, and current clients can leave at any time, and the chat continues on forever.

Part 2 Requirements

  • The server will allow multiple clients to be connected at once.

  • The server is no longer a participant in the chat: it just facilitates some number of clients to have a group chat: the server doesn’t read in input from a user, nor does it echo any of the messages it receives from clients. Its only output should be a message whenever a new client enters the chat and whenever a client leaves (or a connection ends).

  • The server should be multi-threaded: on each client connection (accept), the sever should create a new thread dedicated to that connection; when the client quits, the thread should clean-up any server state associated with its client, and then the thread should exit.

  • The main server thread, after an accept and creating a worker thread for the connection, should go back to the main accept loop, ready to accept another connection from another client.

  • The server should accept no more than 10 simultaneous client connections. If there are 10 clients connected and a new one tries to connect, the server thread should reply HELLO_ERROR to this client and terminate the connection. When a connected client leaves, then the next connect request should succeed. #define some constant for this max size (10), and use it in your code.

  • When a server thread receives a message from one of its client, it will send the message to all other clients.

    • The message sent will have the client’s name prefix.

    • The server should keep an array of structs of client info type you defined in Part1 so that the server thread can send a message on all client’s sockets. Think about if you need any synchronization.

  • The client should be multi-threaded with 2 threads:

    • one thread handles receiving MSG messages from the server.

    • the other thread handles sending messages that it reads in from the user to the server.

      sockets are bi-directional communication channels, which means that one client thread can send a message on the socket to the server, while another thread receives a message from the server on the same socket.
  • Clients can connect and disconnect at any point

  • Clients can send messages whenever they read in a line entered by the user: there is no message ordering among the clients.

Sample Output

Here is sample output from a multi-client chat session. Note that the server is not a participant in the chatting, but instead forwards messages from one client to all others (and note how the the name prefix is included in the mesages from the server). Also note that clients enter and leave and the chat continues.

A note about client-side I/O

Because you will have mulitple client threads sharing the same stdin and stdout, you will see messages from other clients being printed out in the same terminal window as the prompt and the input message the human user is typing in. This is fine.

Requirements

  • Use the TCP sockets client-server connection protocol. Here is a picture of the TCP client-server connection protocol:

tcp protocol
  • Write your solution in C, using the starting point code.

  • Use the definitions in cs87_talk.h in your program. For example, use the type tsize_t rather than unsigned char, and use the #defines for message tags:

    tsize_t tag;
    tag = HELLO_OK;
    ret = send(socket, &tag, sizeof(tsize_t), MSG_NOSIGNAL);
  • For Part 1, the server should never reply HELLO_ERROR (in Part 2 this is a valid option for the server to return).

  • Make sure to close sockets when done or when an error is detected.

  • Your solutions should be robust. Check return values from all system calls, and handle appropriately.

  • Use send and recv functions to send and receive messages on the TCP socket. For recv the flags field should be 0, for send pass MSG_NOSIGNAL to avoid getting sent a SIGPIPE signal when the other end of the connection closes its end of the socket.

  • You should set the socket options SO_LINGER to off and SO_REUSEADDR on the server’s listen socket (see Tips and Resources).

  • Any message read in from the user that is longer than BUFMAX should be truncated before being sent (make sure a sender never sends a message greater than or equal to BUFMAX bytes). Similarly for the NAMEMAX limit.

    BUFMAX is currently defined to be 256, thus it is not possible to send a message size value that is larger than 255 (since message size is represented in a single byte). However, your code should work regardless of what BUFMAX is defined to be, up to 256. For example, if it is redefined to be 32, your code for checking and only sending up to BUFMAX bytes should still work.
  • You should define a struct on the server side for keeping track of a client’s information (this will be necessary for Part 2). The struct should minimally hold:

    • the client’s socket file descriptor (returned by call from accept).

    • a buffer for the client’s message. You can statically declare an array of size BUFMAX. I suggest copying the client’s "name:" to the front of this buffer so it will be a prefix of all messages from this client.

      This means that a client message length plus the name prefix length longer than BUFMAX will result in the client message being truncated to fit in this buffer (so the max message size will be smaller than BUFMAX if you do this). For example, if the prefix is "tia:" then there are 4 fewer characters for a message. If the prefix is "kevin:" then there are 6 fewer. You should also leave the last bucket in this buffer for storing a null terminating character for the string '\0'. '\0' shouldn’t be passed in the message. As a result, a client can send a long message that the server may have to truncate to send to other clients because the server adds the sending client’s name as prefix to each message it sends out, and the name prefix uses up some of the bytes of this message.

    • where the start of buffer is for message data (i.e. the first index into the message buffer for the start of the mesage after the name prefix).

  • Because you are all implementing the same protocol, your client should be able to connect to another group’s server and have a chat. Your server should be able to accept a connection from another group’s client and have a chat-- all Part1 solutions should be compatable, and all Part2 solutions should be compatable. We will test this out in lab next week, but try it out with other groups in the lab as you work on this too.

  • Check return values from all system and function calls and handle errors. For system calls (like send, recieve, socket, etc.), you should call perror to print out an error specific message. Here is one example of how I might call perror and exit if a call to setsockopt fails:

    int reuse_true = 1;
    retval = setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse_true,
                         sizeof(reuse_true) );
    if (retval < 0) {
        perror("Setting socket option SO_REUSEADDR failed");
        exit(1);
    }

Extra Features

These are NOT required, but are some ideas for extra things to add to your Part2 solution to make your chat program a little more user friendly:

  • When a new client connects to a group chat session, the server send all other clients a message saying that they have joined (like "Freya has joined the chat").

  • When a client leaves the group chat session, the server sends all other clients a message saying that the user has left the chat session (like "Freya has left the chat")

  • When a client sends a chat message to the server, the server sends the message to every other client except for the client whose message it is. For example, if Jo, Flo, and Bo are in a chat and Bo types in a message, the server sends Bo’s message just to Jo and Flo.

  • Add to the protocol a new message type for a client to ask the server for all the names of people in the chat session. The user on the client side would have to type in a specific string to trigger this protocol (like typing in "goodbye" triggers the QUIT protocol), this would send a specific message tag to the server that would have it send back to the client the names of all other chat members.

  • Add to the protocol a new message type for a client to ask the server to send it all of the last messages from each chat member. Again, the user on the client side would trigger this by entering some unique string to distinguish this request from a message to share with other clients in the chat session.

  • If you want to try to implement a solution with nicer looking output, you can try using the ncurses library to split the terminal window and direct stdin to one half and stdout and stderror to the other half (this is not trivial to do). You could also try something easier and print out stdout output in a different color if it is coming from the prompt printing thread vs. from the thread that receives and prints out messages from other clients forwarded from the server. Here is some information about ncurses: Cool C libraries

Submitting

Before the Due Date, one of you or your partner should push your solution to github from one of your local repos to the GitHub remote repo. Be sure to do a make clean before you git add anything:

make clean
git add *.c *.h
git commit
git push
git status

See the git help page "Troubleshooting" section for git help.

Additionally, hand in a printout of your report.pdf in class.

adding a git tag

Because you are going to implement your Part 2 solution in the same repo as your Part 1, you should add a git tag to your submitted solution to a particular part.

To do this:

# list all current tags associated with a repo
git tag -l

# create a new tag named Part1 with a tag message (optional)
# and share a tag: push the tag to the origin to share it:
git tag -a Part1 -m "soln to Part1"
git push origin Part1

# checking out a specific tagged version of the code
# (checkout Part1 version of code into a local repo named my_part1)"
git checkout -b my_part1 Part1

# to list other data along with the commit for a specific tag
# (this will show the commit number, date and who created the tag):
git show Part1

You must name it exactly Part1 (and Part2 for part2), otherwise your professor will be very sad.

Take a look at the "Advanced Features" sections of the git help for more help with git.

Tips and Resources

socket programming

  • I have some Socket Programming Links.

    Beej’s Guide is a good staring point and has code examples (sections 5 and 6 are particularly useful).

  • The Steven’s Unix Network Programming (2nd Edition Vol. 1). Chapters 3 and 4 are very helpful. We have a couple copies of this in one or more CS Lab spaces. Please don’t remove these from the CS labs.

  • You should use send and recv to send and receive messages on sockets.

    send and recv should be called in a loop until all the bytes of the message have been received. If a call to send or recv returns with fewer bytes than were send/recived, you need to call again to send/recvie the remaining bytes of the message, starting from within the buffer you are sending/receiving. For example, you can specify the next point to receive in a buffer by passing recv the address of the bucket:

    ret = recv(sock, &(buf[i]), amt_left, 0);
  • set sockopts on the listen socket: SO_REUSEADDR and SO_LINGER. If you set the sockoption SO_LINGER to off, then the socket will close immediately upon a process exit (This is done for you already in the starting point code, just understand what it is doing and don’t remove it):

      struct linger linger_val;
    
      linger_val.l_onoff = 0;
      linger_val.l_linger = 0;
    
      setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, (void *)(&amp;linger_val),
                (socklen_t) (sizeof(struct linger));
  • Read the man pages for system calls like socket, connect, accept, listen, send, recv, etc.

    man 2 socket

    Note RETURN values and be sure to check for error returns and handle appropriately in your code. You should use perror to print error return messages from system calls like these, and printf for non-system call functions that may return error values (like error return values from function you write).

  • use perror to print out error messages from failed system calls:

    int *sock_fd;
    
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(sock_fd == -1) {
      perror("socket create failed. Exiting\n");
      exit(1);
    }

    Look at the RETURNS part of the man page of system calls to see what they return on success or error.

  • run /sbin/ifquery eth0 to get your own IP address.

    $ /sbin/ifquery eth0  # look at the address entry for the IP address
  • nslookup or dig to get another machines IP address

    $ nslookup lime
     Address: 130.58.68.165
    
    $ dig +short lime.cs.swarthmore.edu
     130.58.68.165

C, strings, readline

  • Chapter 2 of Dive into Systems covers C programming, structs, arrays and strings, pointers, pass-by-pointer parameters, switch statment, dynamic memory allocation, and more. And here are some other C programming references.

    • In particular, take a look at C strings and the C string library functions parts. This program involves C string manipulation for reading in a message from the user, constructing a message to pass from one party to another, and for printing out messages recieved. The strncpy function may be useful. You also should not be sending the terminating \0 in messages, thus you will need to add it to the end of any string you want to print.

  • readline library.

  • fflush: to force printf to output printf output is buffered and may not show up to the terminal until the program has executed many instructions past the printf stmt. To force printf output to stdout, you can call fflush:

    printf("hello there");
    fflush(stdout);    // force all buffered printf output to stdout
  • valgrind and gdb guides. Also see Chapter 3 of Dive into Systems on debugging C programs.

pthreads

misc help pages