601.229 (F19): HW6: Internet calculator

Update 11/18 — note about calc_eval tolerating line ending characters, Automated testing of your calculator implementation section

Overview

In this assignment, you will develop a calculator program that accepts connections from clients over the Internet.

Get started by downloading hw6.zip.

Goals of the assignment

The main goal of the assignment is to provide an opportunity to create a network-based application.

Although this will be a relatively simple program, it is representative of a larger class of network-enabled systems:

Grading rubric

The functionality component of the assignment (worth 70% of the total grade) will be determined as follows:

Part 1: Calculator implementation

Important: Part 1 of the homework is purely local computation, with no I/O required (other than a provided demo program.) You will need to complete it as a basis for Part 2. So, get started on it right away!

For Part 1, you will need to implement the following functions:

struct Calc *calc_create(void);
void calc_destroy(struct Calc *calc);
int calc_eval(struct Calc *calc, const char *expr, int *result);

The calc_create function creates an instance of the struct Calc data type and returns a pointer to it.

The calc_destroy function destroys an instance of the struct Calc data type, by deallocating its memory.

The calc_eval function evaluates an expression stored as a C character string in expr, saving the result of evaluating the expression in the variable pointed to by result. If the evaluation succeeds, calc_eval should return 1. If the evaluation fails because expr is invalid, calc_eval should return 0.

The types of invalid expressions that should be detected are:

Your calculator implementation should do operations exclusively using the int data type.

The following kinds of expressions should be supported:

An operand is either a literal integer or a variable name. A variable name (var) is a sequence of one or more alphabetic characters (A-Z or a-z.)

An op is one of the following operators: + (addition), - (subtraction), * (multiplication), / (division).

Space characters should be ignored. Your calculator should assume that all tokens (operands, operators, and =) will be separated by at least one space character. So, for example,

a + 4

is a valid expression, but

a+4

is not a valid expression.

A struct Calc object should have a dictionary mapping variable names to their values. The = operation assigns a value to a variable, creating an entry in the dictionary if one didn’t exist previously. When a variable name is used as an operand in an expression, the value of the variable should be looked up in the dictionary.

You can implement your struct Calc data type in either C or C++. If you use C++, you can make a class that inherits from an empty struct Calc. For example:

struct Calc {
};

class CalcImpl : public Calc {
    // code...
};

Important: do not define the actual struct Calc data type in calc.h; it should be defined only in the implementation module (calc.cpp or calc.c). struct Calc is an opaque data type, meaning that no implementation details are exposed to code using the type.

If you use C++, make sure that your calc_ functions have extern "C" linkage, so that they can be called from C code. For example, you might define them as follows:

extern "C" struct Calc *calc_create(void) {
    return new CalcImpl();
}

extern "C" void calc_destroy(struct Calc *calc) {
    CalcImpl *obj = static_cast<CalcImpl *>(calc);
    delete obj;
}

extern "C" int calc_eval(struct Calc *calc, const char *expr, int *result) {
    CalcImpl *obj = static_cast<CalcImpl *>(calc);
    return obj->evalExpr(expr, *result);
}

These example functions (which you are welcome to use) work by creating an instance of CalcImpl class and using an evalExpr member function to perform expression evaluation.

Important: your calc_eval function should tolerate line ending characters (\r and/or \n) at the end of the expression string.

Unit tests

The calcTest program contains a fairly complete set of unit tests for the calc_ functions. You can build and run it using the commands

make calcTest
./calcTest

If the tests succeed, you should see the following output:

testEvalLiteral...passed!
testAssignment...passed!
testComputation...passed!
testComputationAndAssignment...passed!
testUpdate...passed!
testInvalidExpr...passed!
All tests passed!

Interactive tests

The calcInteractive program allows the user to interactively enter expressions (one per line). Each expression is evaluated and the result printed. Expressions that are invalid result in the error message Error. Entering the command quit causes the calculator to exit.

Compile and run calcInteractive using the following commands:

make calcInteractive
./calcInteractive

Example transcript (user input in bold):

1 + 1
2
a = 1 + 1
2
a * 5
10
a = a + 7
9
a
9
b = a / 3
3
b
3
4 / 0
Error
+ 4
Error
quit

Calculator implementation hints

If you implement your calculator in C++ you could use the following function to break an input expression into tokens:

std::vector<std::string> tokenize(const std::string &expr) {
    std::vector<std::string> vec;
    std::stringstream s(expr);

    std::string tok;
    while (s >> tok) {
        vec.push_back(tok);
    }

    return vec;
}

Maybe more hints here eventually…

Part 2: Calculator server

The second part of your task is to implement a calculator server that listens for client TCP connections, reads a sequence of expressions, and evaluates each expression.

You should implement your calculator server in calcServer.c. The program should take a single command line argument, which specifies a TCP port. The server program should listen for incoming connections on the specified port, and then communicate with the client in the same way that the calcInteractive program does. You can (and should!) adapt the chat_with_client function to use in your server implementation. Note that in addition to recognizing the special quit command (which should cause the server to end the session with the currently-connected client), the server chat_with_client function should also recognize a shutdown command, which causes the server process to exit.

Note that the server should use a single instance of struct Calc for all client connections. That means that if a variable is assigned a value by one session, the variable will have the same value in a subsequent session. This sharing of variables between sessions will be important for the next assignment!

Note that there is no expectation that the server will support concurrent connections: it is only expected to handle one client at a time. This is a limitation you will address in the next assignment.

Running and testing the server

Build the server program with the command

make calcServer

Here is an example of how the server program should be run:

./calcServer 47374

The command-line argument to calcServer is the TCP port on which the server should listen for connections from clients. You will need to choose a TCP port number that is 1024 or greater (ports 0–1023 require superuser privileges.)

To connect to the server, use the telnet program. For example, to connect to a calcServer listening on port 47374, use the command

telnet localhost 47374

The telnet program will allow you to interact with the server program, more or less exactly the same way you interacted with the calcInteractive program in the terminal. Here is a transcript showing interaction with the server program in two separate sessions (user input in bold):

$ telnet localhost 47374
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
2 + 3
5
a = 42
42
quit
Connection closed by foreign host.
$ telnet localhost 47374
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
a + 1
43
shutdown
Connection closed by foreign host.

A couple things to note about this example:

Also note that you will probably want to have two terminals running, one to run the server and one to run telnet.

Automated testing of your calculator implementation

You can implement automated tests for your server implementation as follows.

Start by downloading test_server.sh:

wget https://jhucsf.github.io/fall2019/hw/test_server.sh

Make it executable:

chmod a+x test_server.sh

Create a text file containing lines to send to the server:

echo "a = 2" > test_input.txt
echo "b = 3" >> test_input.txt
echo "a + b" >> test_input.txt
echo "quit" >> test_input.txt

Run the automated test:

./test_server.sh 30000 test_input.txt actual_output.txt

The output file should contain the responses generated by your server:

$ cat actual_output.txt
2
3
5

You should assume that there will be autograder tests that will test test_server.sh to test your server, so make sure this works. Also note that as written, test_server.sh initiates only one connection to your server. Your server should, in general, handle multiple connections from clients (in sequence.) You may wish to create modified versions of test_server.sh which test multiple connections.

Server implementation techniques

Using the functions defined in csapp.h and csapp.c will make adding network support significantly easier. These functions are described in the BO textbook, and are generally useful for Unix systems programming.

The Open_listenfd function can be used to open a server socket, which is a special file descriptor that the server will use to listen for connections from clients.

The Accept function (which is simply a wrapper for the accept system call) causes the server to wait for a client connection request. It returns a client socket file descriptor, which the server can use to communicate with the client process. You can pass the second and third arguments of Accept as NULL, since they are used only to allow the server to determine the client’s network address.

The client socket file descriptor is bidirectional, meaning it can be written to (to send data to the client) and also read from (to receive data from the client.) The chat_with_client function from calcInteractive.c can be reused more or less verbatim in the server program: the only change you’ll need to make is adding support for the shutdown command.

The server’s main function should have a loop in which is repeatedly waits for client connections and uses chat_with_client to communicate with each accepted client. The main loop should terminate (and the server program should exit) when a client issues the shutdown command.)

Make sure that when the server is done communicating with the client, it closes the client socket file descriptor (otherwise the connection will stay open!)

Deliverables

Submit a zipfile containing your complete project. The recommended way to do this is to run the command make solution.zip. This will create a file called solution.zip with all of the required files. Important: all of the files in the zipfile must be at the top level, not a subdirectory. For example, if your zipfile is called solution.zip and you run the command unzip -l solution.zip to list its contents, you should see something like the following output:

Archive:  solution.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     1600  2019-11-06 09:00   calcInteractive.c
     2155  2019-11-06 09:02   calcServer.c
     3054  2019-11-06 09:33   calcTest.c
    24035  2019-11-06 07:41   csapp.c
     3319  2019-11-05 11:52   tctest.c
     4528  2019-11-05 12:55   calc.cpp
      604  2019-11-06 07:37   calc.h
     6621  2019-11-06 08:01   csapp.h
     3954  2019-11-05 11:47   tctest.h
     1208  2019-11-06 09:38   Makefile
---------                     -------
    51078                     10 files

Make sure that the Makefile you submit can build calcTest, calcInteractive, and calcServer targets. Note that it is very likely that the autograder will replace calcTest.c with a customized version containing some additional tests. We highly recommend that you don’t modify the Makefile in the project skeleton code, other than setting appropriate dependencies for for calc.o according to whether you implemented the calculator functionality in C or C++.

Upload your zipfile to Gradescope as HW6. Make sure to include your name and email address in every file you turn in (well, in every file for which it makes sense to do so anyway!)

Grading

For reference, here is a short explanation of the grading criteria; some of the criteria don’t apply to all problems, and not all of the criteria are used on all assignments.

Packaging refers to the proper organization of the stuff you hand in, following both the guidelines for Deliverables above as well as the general submission instructions for assignments on Piazza.

Style refers to C/C++/assembly programming style, including things like consistent indentation, appropriate identifier names, useful comments, suitable documentation, etc. Simple, clean, readable code is what you should be aiming for. Make sure you follow the style guide posted on Piazza!

Design refers to proper modularization (functions, modules, classes, etc.) and an appropriate choice of algorithms and data structures.

Performance refers to how fast/with how little memory your programs can produce the required results compared to other submissions.

Functionality refers to your programs being able to do what they should according to the specification given above; if the specification is ambiguous, ask for clarification! (It also refers to you simply doing the required work, which may not be programming alone.)

If your programs cannot be built you will get no points whatsoever. If your programs cannot be built without warnings using the required compiler options given on Piazza we will take off 10% (except if you document a very good reason). If your programs cannot be built using make we will take off 10%. If valgrind detects memory errors in your programs, we will take off 10%. If your programs fail miserably even once, i.e. terminate with an exception of any kind or dump core, we will take off 10% (for each such case).