Strategy for developing a web server
CS 273 (OS), Fall 2019
Carry out the following tasks, in an order consistent with the stated
prerequisites.
Note: The task labels
A, B, etc., below are for convenience in
cross-referencing, and do not themselves indicate strict ordering.
Changes:
This style indicates any content added since a task was no longer under construction.
This style indicates any content deleted since a task was no longer under construction.
Index of tasks
A. Working directories
(Prereq: submission ofproj_team.html
and creation of team shared repo by RAB)B. Git development branches
(Prereq: Task A)C. Start
server.c
andclient.c
(Prereq: Task B)D. Start
Makefile
(Prereq: Task C; involvesserver.c, client.c
)E. Start server's
work.c
(Prereq: Task D; involvesserver.c do_work()
)F. Start client's
request.c
(Prereq: Task D; involvesclient.c do_request()
)G. Request IDs and
workdat
data structure
(Prereq: Task E; involvesserver.c work.c work.h do_work()
)H. Round-trip client and server
(Prereq: Task E and Task F; involveswork.c request.c do_work() do_request()
)I. Parse HTTP request (initial version)
(Prereq: Task B; involveswork.c Makefile
)J. Incorporate parser into server
(Prereq: Task I; involveswork.c Makefile
)K. Add logging (
log.c
)
(Prereq: Task G; involvesserver.c work.c
)L. Error checking in parser
(Prereq: Task J; involvesparse.c work.c client.c
)M. Send file to client
(Prereq: Task L; involvesparse.c work.c Makefile client_tests
)N. Implement 501 (Not Implemented)
(Prereq: Task L; involveswork.c Makefile
)O. Implement 400 (Bad Request)
(Prereq: Task L; involveswork.c parse.c Makefile
)P. Implement 404 (Not Found)
(Prereq: Task M; involveswork.c Makefile
)Q. Worker thread
(Prereq: Task G; involveswork.c do_work.c do_work() workdat Makefile
)R. Multiple worker threads
(Prereq: Task Q; involvesserver.c work.c main() do_work() Makefile
)S. Browser test
(Prereq: Task M)Under construction:
T. Parallel threads demo (Extra feature)
(Prereq: Task R and Task H; involveswork.c wdp_array.c worker() add_wdp() check_threads()
)
- A. Working directories
(Prereq: submission of
proj_team.html
and creation of team shared repo by RAB) -
Start by creating a clone
~/OS/pp-threads
in your~/OS
working directory for your team's shared stogit repository.cd ~/OS git clone git@stogit.cs.stolaf.edu:os-f19/repo.git pp-threads
Here, replacerepo
by your team repository name (created after you submitproj_team.html
and receive a repository-creation notice from RAB) .This will make a new subdirectory
~/OS/pp-threads
that its own git structure:Inside the subdirectory
~/OS/pp-threads
, your git operations such aspull
andpush
will interact with your team'srepo
.Inside
~/OS
or its subdirectories other thanpp-threads
, git operations would interact with your usual personal course repository.
Link to index - B. Git development branches (Prereq: Task A)
-
We recommend that you use git branches to carry out your team project work.
push
tested working code only to the master branch (e.g.,git pull origin master
)Create new branches for developing new code features, and merge each development branch with
master
as those new features whenever a new incremental step in development is completed and tested.By merging frequently, you avoid lengthy and complex integration of large code modifications with other code changes your teammates (or you!) may have made recently.
Exception: Do Not Break the
master
Branch! Instead, thoroughly test that themaster
branch code works after you merge your code.
Reminder of git branch commands (reference: Git book, sections 3.1 and 3.2
git branch mybranch
Create a new branch
mybranch
in your local working directory.git checkout mybranch
Switch to the branch
mybranch
.Note: Perform
git checkout
operations only when all your changes have already been recorded in commits, so you won't lose any work. You can check for any changes in your code since your last commit usinggit status
git checkout -b mybranch
This is equivalent to
git branch mybranch git checkout mybranch
Only use-b
whenmybranch
doesn't exist yet.git pull origin mybranch
git push origin mybranch
Use mybranch instead of
master
when you want topull/push
with that branch.Note:
Omit thepull
operation the first time only with a new branch, since that branch doesn't exist yet on stogit. Thereafter, usepull
thenpush
as usual.git merge mybranch
Merge the changes in mybranch into the current branch. To merge your branch's changes into
master
, enter these two commands:git checkout master git merge mybranch
Here is a team exercise with git development branches, for teams of 2 or 3 students referred to as Student 1, Student 2, and (if any) Student 3. Perform these steps in the indicated order.
In Student 1's
~/OS/pp-threads
working directory, Student 1creates a simple
hello.c
program that performs a single printf of a string including Student 1's username, and Student 1 tests that code.Student 1 then creates a commit with the new file
hello.c
, and performspull
/push
to commit to the shared team project respository (master
branch).
Then in Student 2's
~/OS/pp-threads
working directory, that Student 2 performsperforms
git pull origin master
in order to receive Student 1'shello.c
Creates a new branch
stu2
for modifying that filehello.c
git branch stu2 git checkout stu2
You can check your current branch usinggit branch
The asterisk * in the output fromgit branch
indicates your current branch. Alternatively, you can issuegit status
which should also indicate the current branch in output.Enter
ls
, and verify thathello.c
exists in the branchEdit the program
hello.c
to add a secondprintf()
instruction, in order to print a line after the original output line. Student 2 should include their username in their added output line, then should proceed to test the modified program.Create a commit for the change to
hello.c
, then performgit push origin stu2
Note: This is one time when apull
does not precede a gitpush
operation. Here, apull
operation for the branchstu2
would fail, because that branch doesn't exist on stogit yet.Now, Student 2 perform a
merge
to integrate their changes into themaster
branch.git checkout master git merge stu2 git pull origin master git push origin master
Note: Thegit merge
command will create a new merge commit, and will start up an editor forStudent 2
to document that merge commit. Exiting that editor immediately will serve our purposes here and enable themerge
commit to complete.
In a 3-member team, Student 3 should carry out the same steps as Student 2, except:
add a
printf()
to print one line of output before the original line created by Student 1 (include Student 3's username in the added output);The name of the branch should be
stu3
.
Now Student 1 should perform
Enter the command
git pull origin master
to retrieve the changes tohello.c
, and test those changes.Create a development branch
stu1
, and edithello.c
to add more characters to the end of the original line entered by Student 1 in that branch.Perform a git push origin stu1 operation
Merge your branch
stu1
into themaster
branch, as indicated above for Student 2.
You can reuse branches or make new ones according to what makes sense to you.
Once you have successfully
merge
d a branch tomaster
, that branch andmaster
have the sameHEAD
(latest commit), so it's safe to delete that branch if you want to. Or, you could continue using that branch for your next new additions to the code.You can delete a branch using
git branch -d branchname git push origin --delete branchname
The first of these commands deletes the local branch (on your working directory), and the second command deletes the remote branch (on stogit).
Link to index - C. Start
server.c
andclient.c
(Prereq: Task B) -
Start a development branch for your work on this task, e.g.,
git checkout -b mybranch
wheremybranch
is a new branch name for your workCopy
sender.c
andreceiver.c
from yourhw4
directory, and rename the copiesclient.c
andserver.c
.Modify
client.c
, moving the following code frommain()
into the definition of a functiondo_request()
with two arguments, a socket descriptor (int
) and a program name (string). Then insert a calldo_request(sd, prog)
inmain()
where that code used to be.char *buff = NULL; /* message buffer */ size_t bufflen = 0; /* current capacity of buff */ size_t nchars; /* number of bytes recently read */ ... printf("%d characters sent\n", ret); if ((ret = close(sd)) < 0) { printf("%s ", prog); perror("close()"); return 1; }
Note: You will also need to define an integer variableret
in your new functiondo_request()
sinceret
is needed for that code.Modify
server.c
, moving the following code frommain()
into the definition of a functiondo_work()
with two arguments, a socket descriptor (int
) and a program name (string). Then insert a calldo_work(clientd, prog)
inmain()
where that code used to be.char buff[MAXBUFF]; /* message buffer */ int ret; /* return value from a call */ if ((ret = recv(clientd, buff, MAXBUFF-1, 0)) < 0) { printf("%s ", prog); perror("recv()"); return 1; } buff[ret] = '\0'; // add terminating nullbyte to received array of char printf("Received message (%d chars):\n%s\n", ret, buff); if ((ret = close(clientd)) < 0) { printf("%s ", prog); perror("close(clientd)"); return 1; }
Compile the two programs
server.c
andclient.c
and test/debug them, using the same networked testing as in HW4.When your code is working, push your work to your branch of the shared repository
git add client.c server.c git commit -m "Task C - start server.c and client.c git push origin mybranch
(wheremybranch
should be replaced by the name of your development branch).
Note: We are omittingpull
this time only becausemybranch
is a new branch.Now merge your correct code into
master
.git checkout master git merge mybranch git pull origin master git push origin master
Link to index - D. Start
Makefile
(Prereq: Task C; involvesserver.c, client.c
) -
Switch to your development branch
git checkout
mybranch
Add the following
Makefile
to yourpp-threads
directory.# Makefile for pp-threads project %.o: %.c gcc -c $*.c all: server client server: server.o gcc -o server server.o client: client.o gcc -o client client.o
Explanation:
# Makefile ...
is documentation%.o: %.c gcc -c $*.c
is a default rule for compiling a C program. This could be modified later as needed.Note: The indentations above must be tab characters (which could be followed by spaces).
all: server client
is the first targetall
and its prerequisitesserver client
.
The first target is the default when you entermake
without specifying a target file to be made.server: server.o gcc -o server server.o
is a Makefile rule for creating the executableserver
:The target is
server
The prerequisite is
server.o
The action is
gcc -o server server.o
If you enter the command
make server
then the prerequisiteserver.o
will be created first (using the default rule for compiling a C program) if necessary, then the actiongcc -o server server.o
will be performed if necessary."if necessary" operations are only performed if their target is out of date with their prerequisites (or a prerequisite of a prerequsite).
For example,server.o
is generated only if its (implicit) prerequisiteserver.c
is newer thanserver.o
; andserver
is generated only if its prerequitesserver.o
is newer thanserver
.
Test the
Makefile
as follows.Exit any editors that are using your code files
client.c
andserver.c
, in order to prevent confusion due to the following operations.make
This should recompile the target executablesserver
andclient
if necessary. Most likely, it will report that they are "up to date" without any recompilation, unless you have made changes since your last compile.touch server.o make
Thetouch
command changes the modified date of a file to the current time, but doesn't change characters or other aspects of that file.
Here,touch server.o
makesserver.o
newer thanserver
, so themake
command should relink to create a new version ofserver
.touch server.c
Thismake
should cause one compile (ofserver.c
) and one link (of the resultingserver.o
), producing a new version ofserver
.touch client.o make
Should cause one link operation, producing a new version of the executableclient
touch client.c
Should cause one compile and one link.
Once your
Makefile
is tested and debugged, send your changes to stogit.git add Makefile git commit -m "Task D - start Makefile" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Note: You could omitgit pull origin mybranch git push origin mybranch
but then there the branch changes would only occur in your local working directory, and would not be known to stogit.We recommend posting all of your work on branches to stogit, in case someone else on your team needs to examine those changes for a later task.
Link to index - E. Start server's
work.c
(Prereq: Task D; involvesserver.c do_work()
) -
We now move
do_work()
fromserver.c
into a separate code modulework.c
with header filework.h
, so thatserver.c
focuses on obtaining new client connections, whereaswork.c
handles requests received from such clients.Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Note: The
merge
operation above merges changes on the stogit server's version ofmaster
(origin/master
) with your development branch. Like allmerge
operations, we expect a merge commit.Note: Another approach to updating
mybranch
involvesgit rebase
. Avoid usinggit rebase
for updating your development branch, because it generates an invented commit history that might cause confusion. . (See the second answer to this stackoverflow article for more information.)Move the definition of the function
do_work()
fromserver.c
to a new filework.c
. Copy the#include
directives from the beginning ofserver.c
to the beginning of the new filework.c
.Create a new file
work.h
with a function declaration fordo_work()
, and add a new include directive#include "work.h"
at the end of the existing#include
directives in each source fileserver.c
andwork.c
. Here is example code forwork.h
:/* header file for work.c */ void do_work(int sock, char *progname);
Modify this file as needed to fit the definition of yourdo_work()
function. For example, if your functiondo_work()
returns a value, use a matching return type inwork.h
.Modify
Makefile
to compilework.c
and link the resultingwork.o
when creating the executableserver
, and to update dependencies includingwork.h
. The code below shows the needed changes.# Makefile for pp-threads project %.o: %.c gcc -c $*.c all: server client server: server.o work.o gcc -o server server.o work.o server.o: server.c work.h work.o: work.c work.h client: client.o gcc -o client client.o
Notes:
In the rule for the target
server
, be sure to addwork.o
as both a prerequisite and as a file to be linked.We added rules for
server.o
andwork.o
in order to show the new prerequisitework.h
.Before, we used
make
's default rule, which is equivalent toserver.o: server.c
The action for these rules is to compile using the default compilation rule
%.o: %.c gcc -c $*.c
If we sometime need to compile differently for some target, we can add an action to that target's rule in order to override this default.
Now issue the command
make
, and debug compilation and linking as needed. Then test run the program using the tests of Task C, and debug as needed to obtain working code.Test your updated
Makefile
Exit any editors using
server.c
,work.c
, orwork.h
, to avoid confusion about modification times.make
This will most likely report that your code is "up to date."touch work.c make
This should recompilework.c
, then relink to produce a newserver
program. (Test that program.)touch server.c make
Should recompileserver.c
then relink to produce a newserver
.touch work.h make
This should produce two recompiles (bothserver.c
andwork.c
) then relink (producing anotherserver
executable).
Make a commit of your successful changes so far.
git add work.c work.h server.c Makefile git commit -m "Task E - move do_work() to work.c, create work.h"
Enter
git log
to find the hash for the commit you want to revert to (you can abbreviate the full 40-character hash to the first few characters, as long as those few characters are different than other hashes).Enter
git reset --hard hash
where hash is the (abbreviated) hash you just found.Use
git reset
with care!git reset --hard
will probably overwrite any changes you may have made since that commithash
. If there's some part of your code that you want to keep, make a copy of those files (e.g., in a new subdirectory~/OS/pp-threads/tmp
) before performing thatgit reset
command.Remove unneeded
#include
directives from the beginning ofserver.c
andwork.c
.For example,
work.c
uses system callsrecv()
andclose()
, which only require the header filessys/types.h
,sys/socket.h
, andunistd.h
.work.c
also requiresstdio.h
forprintf()
andperror()
. So, remove the following:#include <stdlib.h> #include <netinet/in.h>
Note: All the header file information above was found by googling
man recv
, etc.
Verify that your code still
make
s and runs correctlyOnce your changes are tested and debugged, commit and send them to stogit.
git add work.c server.c git commit -m "Task E DONE - Start server's
work.c
" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Note: Making an intermediate commit like this enables you to revert to a prior (committed) version of your code at a later time. For example, suppose you inadvertently made changes after the commit above that caused bugs in the program, and that you can't figure out how to fix those bugs. Then you could revert to the commit above as follows:
Link to index - F. Start client's
request.c
(Prereq: Task D; involvesclient.c do_request()
) -
Similarly to Task E, we move
do_request()
fromclient.c
into a separate code modulerequest.c
with header filerequest.h
, so thatclient.c
focuses on connecting to the server andrequest.c
transmits requests to that server.Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Move the definition of the function
do_request()
fromclient.c
to a new filerequest.c
. Copy the#include
directives from the beginning ofclient.c
to the beginning of the new filerequest.c
.Create a new file
request.h
with a function declaration fordo_request()
, and add a new include directive#include "request.h"
at the end of the existing#include
directives in each source fileclient.c
andrequest.c
.Use an approach similar to Task E.
Modify
Makefile
to compilerequest.c
and link the resultingrequest.o
when creating the executableclient
, and to update dependencies includingrequest.h
.Use an approach similar to Task E.
Now issue the command
make
, and debug compilation and linking as needed. Then test run the program using the tests of Task C, and debug as needed to obtain working code.Test your updated
Makefile
Use an approach similar to Task E.
Make a commit of your successful changes so far.
git add request.c request.h client.c Makefile git commit -m "Task F - move do_request() to request.c, create request.h"
Remove unneeded
#include
directives from the beginning ofserver.c
andwork.c
.Use an approach similar to Task E.
Once your changes are tested and debugged, commit and send them to stogit.
git add client.c request.c git commit -m "Task F DONE - Start client's
request.c
" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - G. Request IDs and
workdat
data structure (Prereq: Task E; involvesserver.c work.c work.h do_work()
) -
The required logging mechanism will use a unique request ID number. We will store this request ID in a new data structure
workdat
, which will eventually hold other data associated with a particular request.Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
In
server.c
, define a localint
variablerequest_id
inmain()
, initialized at 1. Add the value of this variable as a new first argument fordo_work()
(this will require changes in all three filesserver.c, work.c, work.h
).Compile and test your modified
server
with the (unmodified)client
as in HW4.Make a commit of your successful changes so far.
git add work.c work.h server.c git commit -m "Task G - Add request_id to main() and do_work()"
In
work.c
, define a data structurestruct workdat
with one integer fieldrid
(for Request ID). Then, at the beginning ofdo_work()
, define a local variablewd
of that typestruct workdat
, and assign the value of the new first argument to the fieldwd.rid
. Also add a temporaryprintf()
call to print the value ofwd.rid
, in order to verify that the request ID value 1 was correctly stored in that field.Remake and test your modified
server
with the (unmodified)client
as in HW4.You can remove the temporary
printf()
now or at a later time.Once your changes are tested and debugged, commit and send them to stogit.
git add work.c git commit -m "Task G DONE - Request IDs and
workdat
data structure" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - H. Round-trip client and server
(Prereq: Task E and Task F; involves
work.c request.c do_work() do_request()
) -
______
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Modify
do_work()
in the server'swork.c
to send a reply to the client, after receiving the client's message and before shutting down the socket. For now, send a 3-character messageACK
as the reply.Modify
do_request()
in the client'srequest.c
to receive that reply message from the server, after sending it's message and before closing its socket. Also add aprintf()
to print the content of the message that was received.Compile and test your modified
server
andclient
as in HW4, debugging as necessary.Once your changes are tested and debugged, commit and send them to stogit.
git add work.c request.c git commit -m "Task H DONE - Round-trip client and server" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - I. Parse HTTP request (initial version)
(Prereq: Task B; involves
work.c Makefile
) -
Next, we will define two more source modules for parsing an HTTP request and for interacting with files and integrate them into the server code.
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Copy your program
copyfile.c
from HW8 into your project directory. Add aMakefile
target for building the programcopyfile
fromcopyfile.o
(and recall thatcopyfile.o
is automatically generated fromcopyfile.c
. Compile the program usingmake copyfile
and run that program to verify that it correctly reads an input line and performs a file copy.Make a commit of your successful changes so far.
git add copyfile.c Makefile git commit -m "Task I - import copyfile.c"
Now create a function
parse()
incopyfile.c
that generates aname
structure from the input line.parse
1 arg: A string.
State change: A
struct name
memory location is dynamically allocated usingmalloc()
, and thatstruct name
is assigned to hold dynamically allocated copies of the tokens in arg1.Return:
struct name*
, the address of that assignedstruct name
memory location.Then modify
main()
incopyfile.c
to callparse()
to process the line it reads from standard input.Note: Do not include your
getline()
call inparse()
, which would violate the spec above.
Issue
make copyfile
to generate a new version ofcopyfile
, and test/debug. The version ofcopyfile
should behave the same as the prior version did.Make a commit of your successful changes so far.
git add copyfile.c git commit -m "Task I - define parse()"
Define a function
delete_name()
that deallocates astruct name
, satisfying the following spec.delete_name
1 arg:
struct name*
, the address of a dynamically allocated and assignedstruct name
memory location.State change: All dynamically allocated memory pointed to by arg1 becomes deallocated using
free()
, including anytok[]
elements and thestruct name
memory*
arg1 itself.Return: None.
Also add temporary
printf()
calls near the beginning and end of the definition ofdelete_name()
, so you can observe whendelete_name()
becomes called.Insert call(s) of
delete_name()
in thecopyfile.c
code as needed to insure that thestruct name
object allocated byparse()
is correctly deallocated, in order to avoid memory leaks. Test and debug this program with correct deallocation.Make a commit of your successful changes so far.
git add copyfile.c git commit -m "Task I - add delete_name(), for correct memory management"
Move the definitions of
parse()
anddelete_name
to a new fileparse.c
, and put the definition ofstruct name
plus declarations for those two files in a header fileparse.h
. Add#include "parse.h"
to bothcopyfile.c
andparse.c
in order to use the struct and the two functions in both code files.Modify
Makefile
toadd a rule for target
parse.o
with prerequisitesparse.c
andparse.h
add a rule for target
copyfile.o
with prerequisitescopyfile.c
andparse.h
add the object file
parse.o
to the prerequisites and the action for targetcopyfile
Then compile
copyfile
via the commandmake copyfile
and verify that the complation occurs correctly. Proceed to test the resulting executablecopyfile
, checking that it behaves as before.Once your changes are tested and debugged, commit and send them to stogit.
git add copyfile.c parse.c parse.h Makefile git commit -m "Task I DONE - Parse HTTP request (initial version)" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - J. Incorporate parser into server
(Prereq: Task I; involves
work.c Makefile
) -
______
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Modify
work.c
to callparse()
in order to extract the tokens from a client's input line. (This will involve modifying eitherdo_work()
orworker()
, depending on whether you have completed Task Q.) Reply to the client with the second token received from that client. ModifyMakefile
accordingly (parse.h
becomes a prerequisite forwork.o
, andparse.o
becomes a prerequisite and an added object file in the action for targetserver
).Procede to
make
and test/debug the resulting server. The behavior should be the same as before, except that the server responds with the client's second token instead ofACK
.Note that the first line of the HTTP
GET
protocol should beGET filename HTTP/1.1
If you enter that line as input for the client, then the server now parses to extract thefilename
. Include that protocol in a test run of the client and serverOnce your changes are tested and debugged, commit and send them to stogit.
git add work.c Makefile git commit -m "Task J DONE - Incorporate parser into server" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - K. Add logging (
log.c
) (Prereq: Task G; involvesserver.c work.c
) -
The log feature described in the assignment sheet calls for writing three lines to a text file to record results of receiving and responding to a client request.
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Start a new source file
log.c
that defines five functions:start_log()
, which opens a fileserver.log
for appending and writes a line indicating the beginning of a new series of log entries.START log timestamp
wheretimestamp
represents the date and time of that call tostart_log()
.The
fopen()
call will return aFILE*
value. Store that value in a static variablelog
static FILE *log;
defined in the filelog.c
before the definition ofstart_log()
. Here, the keywordstatic
indicates thatlog
is accessible only within the filelog.c
.For the timestamp string, allocate a local variable
now
of typetime_t
and a (local) arraytimestamp
of 30char
s, and fill that array using the following calls to the library functions:now = time(NULL); strftime(timestamp, 30, "%a, %d %b %Y %T %Z", gmtime(&now));
Note that these library calls could return errors. In particular,strftime()
returns 0 if the value that would be assigned totimestamp
requires more than 30 characters, including nullbyte. Otherwise,strftime()
returns the number of characters stored intimestamp
, excluding nullbyte. It is wise to check for this error, since we expect the RFC 1123 format needed for this project to include exactly 29 characters, excluding nullbyte.Look up the
#include
files needed for those library calls and add them to your filelog.c
.
Do not close the log file within
start_log()
. We will reuse the static variablelog
for the four functions below.The project assignment doesn't specify the line printed by
start_log()
(nor the line printed byend_line()
below), but these aren't prohibited, and they serve as delimiters for log output from multiple runs of your server.
start_work()
, to print the first log line for a client interaction on the open log file.start_work()
will need two arguments: the integerrequest_id
; a thread ID (which should be the pthread ID once you implement the thread Task Q, but otherwise can be an arbitrary value like 1).Use the code above involving
time()
andstrftime()
to obtain the timestamp string required for the first log line.
received_protocol()
, to print the second log line for a client interaction on the open log file.received_protocol()
should have two arguments, an integerrequest_id
and a string protocol name (GET
) extracted from parsing the client's message.
sent_protocol()
, to print the final log line for a client's interaction.sent_protocol()
should have two arguments, an integerrequest_id
and a string response code (e.g.,200 OK
) that was sent to the client in response to its request.
end_log()
with no arguments, which prints a messageEND log
and closes theFILE*
variablelog
.
Also, create a file
log.h
that contains function declarations for the five functions described above.Add
#include "log.h"
toserver.c
, and callstart_log()
near the beginning ofmain()
withinserver.c
.Also include
log.h
inwork.c
. Add a call ofreceived_protocol()
to your functiondo_work()
in an appropriate place (e.g., just after parsing).Note: Add your
received_protocol()
call to your functionworker()
instead if you have already implemented the thread Task Q.
Add a call of
sent_protocol()
todo_work()
(orworker()
) just after you send your reply to the client. For now, use"ACK"
for the second argument; later, replace by the actual protocol you send back to the client (e.g.,"200 OK"
).Modify the
Makefile
to incorporate the new source files.Add a new target
log.o
with prerequisiteslog.c
andlog.h
(and empty action, since the default compilation is sufficient).The targets
sender.o
andwork.o
should both now havelog.h
as an additional prerequisite.The target
sender
should now havelog.o
as an additional prerequisite andlog.o
should be added to the linking command in the action for that target.
The
make
command should now generate your updated server. Debug your new code as necessary, and test with the (unmodified) client.Once your changes are tested and debugged, commit and send them to stogit.
git add work.c log.c git commit -m "Task K DONE - Add logging (
log.c
)" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - L. Error checking in parser
(Prereq: Task J; involves
parse.c work.c client.c
) -
The project spec calls for the server to receive two lines of protocol from the client.
GET pagename HTTP/1.1 Host: hostname
After Task J, the server has the capability to receive a one-line message from the client containing at least two tokens, and to extract the second token from that message.In this step, we will check the first token in that first line of protocol, and take action if that token is not the string
"GET"
. We will also modify the client and server in order to communicate two lines of text, and to verify that those lines match the protocol rules above for correctGET
requests.In fact, we will modify the client to send any number of two-line messages to the server, and develop a test set of messages for verifying that all cases of protocol error checking in the server. In future tasks, we can use those test messages to check that error checking hasn't been affected by some later code modification.
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
We will first modify the client to send multiple 2-line messages. Before building any code, we will specify the desired test messages and server responses. Perform these copy operations:
cp ~rab/os/pp2/protocol_test.in . cp ~rab/os/pp2/protocol_test.out . cp protocol_test.in protocol_test.in_all cp protocol_test.out protocol_test.out_all chmod -w protocol_test.*_all
The fileprotocol_test.in
contains lines such as the following.GET mypage.html HTTP/1.1 Host: hostname # 1 GET mypage2.html HTTP/1.1 Host: rns202-1.cs.stolaf.edu # 2 GIT mypage.html HTTP/1.1 Host: hostname # 3 Get mypage.html HTTP/1.1 Host: hostname # 4 Host: hostname # 5 # 6 GET nopage.html HTTP/1.1 Host: hostname # 15 ...
Here, each section terminated by a line beginning with the#
character represents a protocol message to for the client to send to the server. (The number following#
will be ignored in client-server messages, but is included for human reference.)The file
protocol_test.out
contains lines such as the following.mypage.html # 1 mypage2.html # 2 Unimplemented protocol GIT # 3 Unimplemented protocol Get # 4 Unimplemented protocol Host: # 5 Invalid request # 6 Cannot open file # 15 ...
Here, each one-line section terminated by a line beginning with the#
character is a response message we want the server to send back to the client when that client sends the corresponding protocol message fromprotocol_test.in
to that server. The numbers following#
show the correspondence.Finally, the copies
protocol_test.in_all
andprotocol_test.out_all
will be used to record the original versions ofprotocol_test.in
andprotocol_test.out
during testing. We write-protected these with achmod
command in order to avoid inadvertently changing these originals.As a first use of the protocol test files, delete all but the first two lines of
protocol_test.in
, and all but the first line ofprotocol_test.out
.protocol_test.in
should contain onlyGET pagename HTTP/1.1 Host: hostname
protocol_test.out
should contain onlypagename
Now try your (unmodified) client and server, except invoke the client using this command:
./client hostname port < protocol_test.in | diff - protocol_test.out
Here,hostname
andport
are values for connecting to your running server. We expect the following results:Presumably your client still reads only one line from standard input. In this case, that line will be the first line of the current
protocol_test.in
, which isGET pagename HTTP/1.1
When the server receives this (one-line) request, it will extract the second token
pagename
and send that token back to the client.The client will receive that message and print it on standard output (according to Task H).
The output will be piped to the
diff
command, which compares toprotocol_test.out
.The output from
diff
should include your client's prompt for input, but no other differences (unless your client prints a label when it prints the response message received from the server).
If you observe a different behavior, make sure you understand why, and modify your code if necessary to achieve this behavior (e.g., removing a label from client's output of a response message, so the output matches the line in
protocol_test.out
).Now insert the printed form of your client's prompt for input to the beginning of your file
protocol_test.out
. This should cause thediff
command in the./client
pipeline above to print nothing, indicating an exact match of your client's output with the expected output in the modifiedprotocol_test.out
.Record your protocol test files in a commit.
git add protocol_test.* git commit -m "Task J - first version of protocol testing files"
You can optionally pull/push this commit in order to submit to stogit, or you can wait until a later commit is pull/pushed. Likewise, if you modified your client, you couldadd
the modified code files before creating this commit, or wait to commit these with other code changes in a later commit for this task.Create a shell script
client_tests
that performs the client testing pipeline above. This means to create a fileclient_tests
that consists of the following:#! /bin/bash ./client $1 $2 < protocol_test.in | diff - protocol_test.out
Perform the following shell command to makeclient_tests
an executable.chmod +x client_tests
Then verify that the scriptclient_tests
behaves the same as the original client pipeline above by entering the following shell command../client_tests hostname port
wherehostname
andport
are the hostname and port for your (already started!) server.Notes:
The first two characters
#!
of the first line of a file#! /bin/bash
mark that file as a script, assuming that file also has executable mode bit(s) (which thechmod +x
command accomplished).This is a shell script because the first line indicates that a shell
/bin/bash
should be used for interpreting the remaining lines of the file. (You could make scripts for other languages by replacing/bin/bash
by a different programming language processor.)The tokens
$1
and$2
in the shell script represent two command-line arguments for the script. Note: This shell script doesn't have any error checking, so be sure to supply a host and port number whenever you use./client_tests
.We added a newline after the pipe character
|
for readability.
Modify your client to read all lines of standard input up to end of input into a single message buffer, including newline characters within that buffer after each input line. Then, send that multiline message to the server. We expect the same response as for a one-line message, since the server currently only examines one line (assuming Task J).
Define a character array
msg
for collecting the input lines, e.g.,char msg[MAXMSG]
where the preprocessor constantMAXMSG
is large enough to hold one entire multiline protocol. Or, you can use an existing array that you already use in the client for receiving the server's response. Make sure at the array size (e.g.,MAXMSG
is large enough - 1000 characters should be plenty for the basic assignment.Initialize
msg[]
as an empty string (nullbyte inmsg[0]
). Then, for each line of input, use a library function such asstrcat()
to append each new line of input tomsg[]
.If your client uses C's
getline()
library function for input (as didsender.c
), note thatgetline()
copies each line's newline character (if there is one) automatically.Send the entire (multi-line)
msg
string, up to but excluding the terminating nullbyte, to the server in a single message, i.e., in a singlesend()
socket call.If necessary, add a temporary
printf()
towork()
in the server in order to verify that the server received the entire two-line message. However, your unmodified server code may already implement the output of the request message.
Now, start your server and use
client_tests
to test that server, verifying that the server receives the entire multi-line message then replies with the second token of the first line, as before.Make a commit of your successful changes so far.
git add client.c client_tests git commit -m "Task L - multi-line input in client, multi-line message to server; script client_tests"
Your client now has a loop for reading input lines and appending them to
msg[]
until the end of input. Modify that loop to stop repeating if either end of input is encountered or a line beginning with#
is encountered.To test that modification, add the four lines
# 1 GET mypage.html HTTP/1.1 Host: rns202-1.cs.stolaf.edu # 2
at the end ofprotocol_test.in
, and test the server usingclient_tests
. Your server should receive onlyGET pagename HTTP/1.1 Host: hostname
and no other lines, and the server's response to the client should be unchanged (so the pipeline should show no differences with the unmodifiedprotocol_test.out
). Debug as necessary to obtain this expected behavior.Now, add an outer loop in your client to send multiple requests to the server.
The body of that outer loop should start with the client's
socket()
call and end after closing the socket returned by that call (indo_request()
).Exit the outer loop only when end of file is encountered (not
#
).One way to manage the outer loop is to use the
do while
loop syntax, e.g.,do { if ((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { ... } ... } while (nchars != -1);
Here, we assume thatnchars
holds the return value fromgetline()
.Note:
nchars
is a local variable indo_request()
. Pass the value ofnchars
as the return value ofdo_request()
, and assign it (in yourdo
loop) to a variablenchars
for the guard evaluation.Also check all the return values from
do_request()
to return the value ofnchars
for the most recently call ofgetline()
.
Consider moving your loop to read multiple lines of input into
msg[]
before thesocket()
call, since there's no need to make a socket and try toconnect()
if there is no message to send. This might lead to an outer loop with this structure:do { /* loop to read input and assemble msg[] */ if (nchars != 1) { if ((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { ... } ... } } while (nchars != -1);
or an equivalent structure of your choice.Also, consider moving the prompt for input outside of the outer loop. We only want that prompt for interactive use, and we would probably only enter one protocol section interactively, which would mean there only needs to be one prompt. This would avoid multiple prompts in the client's output, which makes it easier to add tests to
protocol_test.out
.Add the following line to the end of your
protocol_test.out
mypage.html
since we now expect the client to send two requests. (Here, we are omitting the#
lines in the originalprotocol_test.out_all
.)
Now test your server using
client_tests
. We expect the following:First, the client should read the first protocol section from standard input, which will be
GET pagename HTTP/1.1 Host: hostname
client should not add the third line#1
tomsg[]
, but should proceed to send only that two-line message to the server.The server should receive exactly those two lines, and should respond to the client with a one-line message
pagename
The client should receive that one-line message and should print it.
Then, in the second iteration of its outer loop, the client should read the second protocol section from standard input, namely
GET mypage.html HTTP/1.1 Host: rns202-1.cs.stolaf.edu
Again, the client should detect the#2
line and take action to stop its input loop and forward the two-line message to the server.In a second iteration of the loop in
server.c
'smain()
, the server should receive that second two-line message, then reply to the client with the messagemypage.html
.The client should receive that reply and print it.
On the third iteration of the client's outer loop, end of input should be encountered by the client. The client should then exit the outer loop and exit, without sending anything more to the server.
Finally, the
diff
in the client's pipeline should report no differences.
Make a commit of your successful changes so far.
git add client.c protocol_test.in protocol_test.out git commit -m "Task L - multiple request capability in the client"
Now that we have upgraded the client for testing, we are ready to start modifying the server to check the validity of a request from the client. First, define a
char*
variablereply
at the beginning of the functiondo_work()
inwork.c
(or in the functionworker()
if you have already completed Task Q), and assign a dynamically allocated array of 8500 bytes toreply
.8500 bytes is large enough for up to 300 bytes of header and 8K of content in a reply message for the client.
Also, after parsing the client's request in
do_work()
(orworker()
), copy the second token toreply[]
(e.g., withstrcpy()
), thensend()
the contents ofreply[]
to the client. The (dynamically allocated) arrayreply[]
is large enough to hold any response message for the client in the basic assignment, not just the temporary reply message consisting of that second token. Don't forget to deallocatereply
usingfree()
, e.g., just after using it tosend()
the response to the client.Recompile, then test your server with
client_tests
to check that you get the same results as before.For the basic assignment, the server must not only recognize correct
GET
requests and act on those, but must also detect when the first token of protocol is notGET
, and respond appropriately in that case.We will use the library function
sscanf()
(s
tringscanf
) to examine that first token, and postpone full parsing of a request until we know that first token isGET
.sscanf()
performs input the same asscanf()
does, exceptsscanf()
reads from a string instead of standard input. For example, the codechar *str = "62 49"; int val; sscanf(str, "%d", &val);
assigns the value 62 to the integer variableval
.In the function
do_work()
(orworker()
), usesscanf()
with the format string"%s"
to read the first string (up to the first whitespace character) from the request message received from the client into an array of characterschar first[100];
Check that the return value from
sscanf()
is 1 which means that one value was assigned (namely, the value offirst[]
). If that return value is not 1, then there must not be a first token in the message from the client, so copyInvalid request
toreply[]
and sendreply
to the client. Also callreceived_protocol()
to record that anEmpty protocol
was received from the client.Otherwise, a first token exists. Compare that first string to
"GET"
, e.g., usingstrcmp()
.If the string in
first[]
matches"GET"
, proceed to your code for parsing the client's request and send the second token to the client as before. Callreceived_protocol()
to record thatGET
was received from the client.If
first[]
does not match"GET"
, send the stringUnimplemented protocol first
to the client, wherefirst
is the string value contained infirst[]
. (You could use library functions such asstrcpy()
andstrcat()
.) Callreceived_protocol()
to record thatfirst
was received from the client.
Double-check your code to insure that the dynamically allocated memory for
reply
is correctly deallocated usingfree()
in all cases. Also check to make sure thatreceived_protocol()
is called only once per client request.
Add the test case
#3
to yourprotocol_test.in
andprotocol_test.out
files.This means to add 3 lines
GIT mypage.html HTTP/1.1 Host: hostname # 3
to the end ofprotocol_test.in
(these lines can be found inprotocol_test.in_all
) , and add the two linesUnimplemented protocol GIT # 3
(found inprotocol_test.out_all
) to the end ofprotocol_test.out
.
Recompile, and test your server with
client_tests
.Note: The label
#3
inprotocol_test.out
will cause an extra section ofdiff
output from the scriptclient_tests
. To avoid this, add the following line to the scriptclient_tests
:#! /bin/bash ./client $1 $2 < protocol_test.in | grep -v '^#' | diff - protocol_test.out
The added line inserts another command (grep
) to the pipeline that removes all lines beginning with#
from the output stream from./client
.You may optionally add labels
#1
and#2
to yourprotocol_test.out
file if you wish.
Check that you see no unexpected
diff
output or other issues. Debug as needed.Then, add the test cases
#4
,#5
,#6
, and#7
to yourprotocol_test.in
andprotocol_test.out
files.This means to add 9 lines
Get mypage.html HTTP/1.1 Host: hostname # 4 Host: hostname # 5 # 6 # 7
These lines can be found in
protocol_test.in_all
.The final line
#7
causes an empty request to be sent to the server) to the end ofprotocol_test.in
Also add the 8 lines
Unimplemented protocol Get # 4 Unimplemented protocol Host: # 5 Invalid request # 6 Invalid request # 7
(found inprotocol_test.out_all
) to the end ofprotocol_test.out
.
Recompile, and test your server with the (modified)
client_tests
to check that you see no unexpecteddiff
output or other issues. Debug as needed.Note: If you encounter a segmentation fault for one of the test cases
#6
and/or#7
, be sure to check the return value fromsscanf()
before using the value offirst[]
, sincesscanf()
doesn't necessarily assign a value to its variablefirst
when the format string isn't matched.
Make a commit of your successful changes so far.
git add work.c protocol_test.in protocol_test.out client_tests git commit -m "Task L - Checking the first token in client's protocol"
The server now detects cases when a client request doesn't begin with the token
GET
, and takes action in those cases. We will now proceed to take action when the first token isGET
.Define a function
parse_GET()
inparse.c
for validating the protocol for aGET
request.parse_GET
2 args: A
char*
message received from the client, and an array ofchar
.State change: A string is copied into arg2 that indicates whether or not the client message
arg1
satisfies the protocol rules for an HTTPGET
request.Return: An
int
value 1 if arg1 is a valid HTTPGET
request, and 0 otherwise.Note: For the remainder of this task, we will use the name
request
to refer to arg1 in this spec, and the namereply
for arg2.Rewrite your
do_work()
to callparse_GET()
(or rewriteworker()
, if you have already implemented Task Q), and move your code for parsing the client's request message toparse_GET()
.In
do_work()/worker()
, pass the 8500-byte stringreply
as the first argument for the call ofparse_GET()
, and pass the request message received from the client as the second argument for that call. (Thus, we're using the namereply
for both the local variable indo_work()/worker()
and for arg1 withinparse_GET()
.)
Your initial version of
parse_GET()
should parse the stringrequest
, then copy the second token toreply[]
(i.e.,parse_GET()
's arg2) and return 1. Also, afterdo_work()
(orworker()
) callsparse_GET()
,do_work()/worker()
should send the value in (its local variable)reply
to the client as before.Within
parse_GET()
, you can copy a string intoreply[]
using a library function such asstrcpy()
.Be sure to add a declaration of
parse_GET()
toparse.h
, sowork.c
can compile using the new function.
Recompile, and test your server with
client_tests
to check that you see no unexpecteddiff
output or other issues. Debug as needed.Make a commit of your successful changes so far.
git add work.c parse.c parse.h git commit -m "Task L - add function parse_GET() in parse.c"
The remainder of this task involves modifying
parse_GET()
to check for token-count errors represented inprotocol_test.in_all
and generate the appropriate one-line responses to the client indicated inprotocol_test.out_all
.Insert an
if
statement inparse_GET()
just after your code for parsing the (first line of) the argumentrequest[]
into tokens, to check whether the number of tokens equals 3. If the count of tokens is not 3, copy the string"Invalid request"
toreply[]
and return the value 0 fromparse_GET()
. Otherwise, proceed with the rest of your code forparse_GET()
.As before, you can copy that string into
reply[]
usingstrcpy()
.Note: No
else
is required for thisif
- you can simply proceed with the remainder of theparse_GET()
code, since that code will not be reached by the CPU if you have already returned fromparse_GET()
during thatif
statement.
Add test case
#8
to the end of yourprotocol_test.in
, i.e., the three linesGET try.txt Host: hostname # 8
and add the corresponding two linesInvalid request # 8
to the end of yourprotocol_test.out
. Then recompile and test run your server againstclient_tests
, and verify that thediff
produces no output, or debug until this test succeeds.Add test cases
#9
and#10
to your filesprotocol_test.in
andprotocol_test.out
.Then recompile and test run your server with
client_tests
, and verify that thediff
produces no output, or debug until this test succeeds.Make a commit of your successful changes so far.
git add parse.c protocol_test.in protocol_test.out git commit -m "Task L - parse_GET() checks number of tokens in first protocol line"
Now add code to parse the second line of protocol in
parse_GET()
's argumentrequest[]
, using a second data structure (in order to preserve the results of parsing the first line of protocol).Add this code after the
if
for checking the number of tokens in the first protocol line, but before returning the second token.We suggest that you define a
char*
pointer variablesecondp
and assign address of the first element ofrequest[]
after the first newline tosecondp
, and parse starting atsecondp
instead of at the beginning ofrequest
. (We will usesecondp
below in order to refer to the beginning of the second protocol line withinrequest[]
.)Parse the second line into a separate data structure, so you can still access tokens of the first line.
Then, add a second
if
for checking the count of tokens in the second line of protocol. If that count is not 2, copy the string"Invalid request"
toreply[]
and return the value 0 immediately. Otherwise, proceed with the rest of your code forparse_GET()
(i.e., copy the second token of the first line toreply[]
, deallocate the memory that was dynamically allocated for tokens, and return 1).Add test case
# 11
to your filesprotocol_test.in
andprotocol_test.out
. Recompile and test run your server againstclient_tests
, and verify that thediff
produces no output, or debug until this test succeeds.Add test cases
#12
through#14
to your filesprotocol_test.in
andprotocol_test.out
. Recompile and test run your server withclient_tests
, and verify that thediff
produces no output, or debug until these tests succeed.Once your changes are tested and debugged, commit and send them to stogit.
git add parse.c protocol_test.in protocol_test.out git commit -m "Task L DONE - Error checking in parser" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - M. Send file to client
(Prereq: Task L; involves
parse.c work.c Makefile client_tests
) -
We are now ready to send a requested file to the client, using the correct HTTP protocol for the response message.
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Copy the files
mypage.html
,mypage2.html
, andprotocol_test.out_200
to your working directory.cp ~rab/os/pp2/mypage.html . cp ~rab/os/pp2/mypage2.html . cp ~rab/os/pp2/protocol_test.out_200 .
The two.html
files are examples for protocol testing, andprotocol_test.out_200
has expected replies in HTTP protocol from the server to the client for the first two test requests (only) inprotocol_test.in_all
.Note for Fall 2019: Also make fresh copies of
protocol_test.in_all
andprotocol_test.out_all
for use in this task.chmod +w protocol_test.in_all protocol_test.out_all cp ~rab/os/pp2/protocol_test.in protocol_test.in_all cp ~rab/os/pp2/protocol_test.out protocol_test.out_all chmod -w protocol_test.in_all protocol_test.out_all
Modify
parse.c
andparse.h
to add a functionhttp_header()
that satisfies the following spec.http_header
3 args: An array of characters, a
char*
string representing a protocol response code (e.g."200 OK"
) and anint
string representing the length of content to follow the header.State change: A valid HTTP header of type arg2 with
Content-Length:
value arg3 is assigned to the beginning of the array arg1, followed by a nullbyte.Return:
char *
, the (modified) array arg1.For example, the call
http_header(reply, "200 OK", 425)
should assign the following string to the beginning ofreply[]
."HTTP/1.1 200 OK\r Content-type: text/html; charset=utf-8\r Content-length: 425\r \r "
(recall that\r
represents the CR character, required for the ends of HTTP header lines). This HTTP header would be appropriate for sending a 425-byte file in response to a client'sGET
request.Note: As this example shows, you may omit the
Date:
andConnection:
fields in HTTP reply headers for this project.Add a function declaration for
http_header()
toparse.h
, and add the function definition toparse.c
.
Then, modify
do_work()
(orworker()
) to check the return value from its call ofparse_GET()
.If
parse_GET()
returns 0 (indicating that parsing failed), thensend
reply[]
to the client as before,Call sent_protocol() to record that
reply[]
was sent to the client.deallocate
reply[]
usingfree()
, andreturn from
do_work()
(orworker()
)
We can now assume that
parse_GET()
returns 1 (successful parsing of client's request message). Carry out these steps:copy
reply[]
(the file name that was the second token of theGET
request) to achar
array variablefilename
,call
http_header()
to assign a200 OK
header toreply[]
with the length offilename
as the third argument,append
filename
toreply[]
, andsend this revised
reply[]
to the client.
To test these changes, replace the first two tests (four lines) of your
protocol_test.out
by the two sections ofprotocol_test.out_200
, so that your updatedprotocol_test.out
begins withHTTP/1.1 200 OKCR Content-Type: text/html; charset=utf-8CR Content-Length: 91CR CR <html> <head><title>My page</title></head> <body><p>This is mypage.html</p></body> </html> # 1 HTTP/1.1 200 OKCR Content-Type: text/html; charset=utf-8CR Content-Length: 107CR CR <html> <head><title>My page 2</title></head> <body><p>This is <code>mypage2.html</code></p></body> </html> # 2 Unimplemented protocol GIT # 3 Unimplemented protocol Get # 4 ...
whereCR
represents a carriage return character (displays as^M
in emacs).The file
protocol_test.out_200
contains these revised sections#1
and#2
, including those carriage-return characters, so you can copy and paste from that file rather than entering manually.
Recompile, and test your server with
client_tests
. This time, we expectclient_tests
to produce differences: your client should printmypage.html
instead of<html> <head><title>My page</title></head> <body><p>This is mypage.html</p></body> </html>
and your client should printmypage2.html
instead of<html> <head><title>My page 2</title></head> <body><p>This is <code>mypage2.html</code></p></body> </html>
But there should be no other differences. Debug as needed.Make a commit of your successful changes so far.
git add parse.c parse.h work.c protocol_test.out protocol_test.in_all protocol_test.out_all git commit -m "Task M - add html_header(), revise protocol_test.out, and do preliminary test"
You may optionally also commit
protocol_test.out_200
. However, this file won't be needed for future steps or tasks. Alternatively, you can either delete that file or add that filename to a file.gitignore
in your working directory (then commit.gitignore
).
Now, modify your
do_work()
(orworker()
) to send the contents of the filefilename
instead of just the name of that file.First, try opening that file, e.g.,
FILE *fp = fopen(filename, "r");
Be sure to examine the value offp
to determine whether there was an error opening that file. If there was an error, thenassign the string
"Cannot open file"
toreply[]
(replacing whatever was already there),send that message
reply[]
to the client,call
sent_protocol()
to record that"Cannot open file"
was sentfree()
the dynamic allocation ofreply[]
, andreturn
We can now assume that the file opened successfully. We will need the file's length in order to call
http_header()
. The following code finds that length:fseek(fp, 0L, SEEK_END); int sz = ftell(fp); rewind(fp);
Here,the
fseek()
call positionsfp
to the end of the open file,the
ftell()
call returns the integer position offp
, which is the number of bytes in that file since we are positioned at the end, andthe
rewind()
call repositionsfp
to point to the beginning of the open file (which we will need in order to copy that file's contents intoreply[]
later).
Now use your existing
http_header()
call to insert an HTTP 200 header at the beginning ofreply[]
, except use the file lengthsz
determined above for the third argument of that call.Finally, append the file contents to
reply[]
. This can be done by a call of the library functionfread()
. Consulting with thefread()
manual page, we use the following arguments for this call.fread()
's first argument is the array to copy into. This can be expressed using pointer arithmetic asreply + strlen(reply)
sincestrlen(reply)
is the number of bytes in the HTTP 200 header now located at the beginning ofreply[]
.fread()
's second argument should be 1, meaning that we are reading individual bytes at a time (notint
s or some other larger unit).fread()
's third argument should be number of byte units to read, namely,sz
.fread()
's final argument is theFILE*
streamfp
.
Recompile, and test your server with
client_tests
. After these changes, we expect no output from the tests. Debug as necessary.Modify your call of
sent_protocol()
indo_work()
(orworker()
) to print"200 OK"
as the response code for a successful file transfer reply.Make a commit of your successful changes so far.
git add work.c git commit -m "Task M - test sending of files to client, and check for successful fopen call"
To test for
Cannot open file
, add test case#15
to your filesprotocol_test.in
andprotocol_test.out
.This request refers to a file
nopage.html
that is not supposed to exist. Do not create that file (or if there's some reason you need that filename, modify that test request#15
to use a name that doesn't exist as a file.
Recompile and test run your server with
client_tests
, and verify that thediff
produces no output, or debug until these tests succeed.Before leaving this task, we will refactor and automate our approach for creating new versions of
protocol_test.out
that have HTTP protocol instead of one-line informational messages. For this task, we modifiedprotocol_test.out
manually for cases#1
and#2
, but this will be too laborious and error prone for all the tests we want to make.First, we will retrieve our prior version of
protocol_test.out
, just before adding HTTP 200 messages.mv protocol_test.out protocol_test.out_new.saved git log
Thegit log
command displays a local history of commits that were made, in reverse chronological order. Identify the commit at the beginning of this task, i.e., just before the commit you made with the messageTask M - add html_header(), revise protocol_test.out, and do preliminary test
Commits have random 40-character hash names, but you only need to use the first few characters of such a name to refer to a commit in a git command - the first 5 or 6 characters would be enough (e.g.,a47xc6
).Retrieve the version of
protocol_test.out
from that commit using this command.git checkout hash -- protocol_test.out
wherehash
is the abbreviated name of the commit you found. The retrieved file should start withmypage.html # 1 mypage2.html # 2 Unimplemented protocol GIT # 3 Unimplemented protocol Get # 4 ...
Now copy the script
add_http
and associated file into your working directory.cp ~rab/os/pp2/{add_http,[245]*.{html,http}} .
This wildcard notation should copy the following files into your working directory:add_http 200.http 400.html 400.http 404.html 404.http 501.html 501.http
Make a commit of your successful changes so far.
git add add_http 200.http 400.html 400.http 404.html 404.http 501.html 501.http git commit -m "Task M - Add script add_http and related files"
Modify
Makefile
to add a new prerequisiteprotocol_test.out_new
to the default targetall:
(e.g., afterclient
andserver
) also add the following new rule (e.g., at the end ofMakefile
).protocol_test.out_new: cp protocol_test.out protocol_test.out_new
Test these changes by issuingmake
. The targetsclient
andserver
should be up to date, but we expect the fileprotocol_test.out_new
to be generated as a copy ofprotocol_test.out
. Verify that these files have the same content at this point.Now add another
Makefile
rule for updating the fileprotocol_test.out_new
.protocol_test.out_new: protocol_test.out ./add_http 200
Also, enter the following two shell commands.chmod +x add_http touch protocol_test.out
Explanation:The
chmod
command ensures that the scriptadd_http
will be executable.The
touch
command changes the "last modified" date to the current time for the fileprotocol_test.out
. This will trigger theMakefile
rule you just added for the targetprotocol_test.out_new
.
make
command, which should perform the command./add_http 200
to update the targetprotocol_test.out_new
.We now expect
protocol_test.out_new
to be the result of substituting HTTP 200 protocol sections for the first two cases#1
and#2
. In other words, the commandadd_http 200
should perform the changes you made manually above. Verify that this is the case using the commanddiff protocol_test.out_new protocol_test.out_new.saved
There should be no differences.If you do find differences, determine whether this is due to errors in the use of the script
add_http
or errors that you made when manually substituting HTTP 200 protocol, in order to determine how to proceed.You can retest this step by using
touch protocol_test.out
to regenerate a new version ofprotocol_test.out_new
.
Modify your own script
client_tests
to perform itsdiff
command usingprotocol_test.out_new
instead ofprotocol_test.out
. Now test run your server withclient_tests
, and verify thatclient_tests
produces no output, or debug until this test succeeds.After this check, we now have an automated way to transform
protocol_test.out
into a fileprotocol_test.out_new
that incorporates desired HTTP reply protocols, usingmake
and the provided scriptadd_http
, and yourclient_tests
script can check against the HTTP reply protocols you have implemented so far.After this check, we no longer need the old manually produced file
protocol_test.out_new.saved
, so you can either delete that file or add it to.gitignore
.
Note: If you made any changes (e.g., in server source code) in order to make steps above work correctly, commit those changes with an appropriate commit message.
Make a commit of your successful changes so far.
git add Makefile client_tests git commit -m "Task M - Integrate add_http into Makefile and client_tests, for automated substitution of HTTP reply protocols"
Note:
We do not commit
protocol_test.out_new
because that file is generated byMakefile
andadd_http
.
Add this filename to the file.gitignore
in your working directory (creating a new one if necessary) so the git system will know thatprotocol_test.out_new
doesn't need to be committed if it changes.
Finally, add some documentation to your new
Makefile
rulesAdd the following boldface line just before the rule for creating a default
protocol_test.out_new
.# default creation of protocol_test.out_new protocol_test.out_new: cp protocol_test.out protocol_test.out_new
Add the following documentation line just before your rule for updating
protocol_test.out_new
.# regenerate protocol_test.out_new with HTTP reply protocols impl to date
Once your changes are tested and debugged, commit and send them to stogit.
git add Makefile .gitignore git commit -m "Task M DONE - Send file to client" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - N. Implement 501 (Not Implemented)
(Prereq: Task L; involves
work.c Makefile
) -
Your function
do_work()
(orworker()
) uses a library function such assscanf()
to examine the first token of the client's request message. If that token isGET
, your code callsparse_GET()
and proceeds to attempt to send a file to the client.But if that first token is not
GET
, your code sends anUnimplemented protocol
message to the client. We will now implement the correct HTTP 501 reply to that situation.Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
For Fall 2019: Make fresh copies of
protocol_test.in_all
andprotocol_test.out_all
for use in this task.chmod +w protocol_test.in_all protocol_test.out_all cp ~rab/os/pp2/protocol_test.in protocol_test.in_all cp ~rab/os/pp2/protocol_test.out protocol_test.out_all chmod -w protocol_test.in_all protocol_test.out_all
Locate the code in
do_work()
(orworker()
) where theUnimplemented protocol...
message is constructed (inreply[]
) and sent to the client.This is the case where
first[]
does not equal"GET"
.
Instead of constructing a message
Unimplemented protocol first
(wherefirst
is the non-GET
first token your code finds), construct a two-part message consisting of a"501 Not Implemented"
HTTP header followed by the contents of the file501.html
.Delete or comment out the code for inserting
Unimplemented protocol first
at the beginning ofreply[]
.Open the file
501.html
usingfopen()
.The file
501.html
should have been added to your team's repository in Task M. Check whether this file opens correctly usingfopen()
, and if it fails to open, consult with the person who carried out that task if necessary to determine what happen and add501.html
and other files to your team's stogit repository and your own working directory.
Determine the length of
501.html
usingfseek()
andftell()
as above, and callrewind()
to reposition the open file back to the beginning.Call
http_header()
to insert an HTTP 501 header at the beginning ofreply[]
, using the length of501.html
for the third argument of that call.Append the contents of
501.html
toreply[]
usingfread()
as before.Send the resulting message
reply[]
to the clientModify your call of
sent_protocol()
for this case to indicate that"501 (Not implemented)"
was sent to the client(Retain the
free()
call for deallocating the memory that was dynamically allocated toreply[]
.)
Update the file
protocol_test.out_new
to include the proper HTTP 501 protocol replies.Add a command
./add_http 501
to the actions for the updating rule forprotocol_test.out_new
. Thus, that updating rule might now be# regenerate protocol_test.out_new with HTTP reply protocols impl to date protocol_test.out_new: protocol_test.out ./add_http 200 ./add_http 501
Enter the commands
touch protocol_test.in make
to regenerateprotocol_test.out_new
.Check the updated contents of
protocol_test.out_new
. For example, section#3
ofprotocol_test.out_new
should now consist of the following lines:HTTP/1.1 501 Not ImplementedCR Content-Type: text/html; charset=utf-8CR Content-Length: 208CR CR <html> <head><title>501 Not Implemented</title></head> <body><p style='text-align:center; font-size:150%'>501 Not Implemented</p> <p>Sorry, this server doesn't handle that type of request.</p></body> </html> # 3
Recompile, and test run your server with
client_tests
. Verify that thediff
produces no output, or debug until these tests succeed.Once your changes are tested and debugged, commit and send them to stogit.
git add work.c Makefile git commit -m "Task N DONE - Implement 501 (Not Implemented)" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - O. Implement 400 (Bad Request)
(Prereq: Task L; involves
work.c parse.c Makefile
) -
After Task L, your server can handle requests that did not begin with the token
GET
and requests that contained no tokens. ForGET
requests, your server checks whether the first two lines had the right numbers of tokens. We will now add further checks to insure that those tokens have the expected values, and modify the server to reply with the HTTP 400 protocol message when a client request has a valid request formatSwitch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
In
do_work()
(orworker()
) we check whether a client's message has no tokens. If there are no tokens, we send thereply[]
valueInvalid request
to the client.Replace that message by an HTTP 400 protocol message.
Open the file
400.html
and determine its length, as before. Callrewind()
to reposition the open file back to the beginning.Call
http_header()
to insert an HTTP 400 header at the beginning ofreply[]
, using the length of400.html
for the third argument of that call.Append the contents of
400.html
toreply[]
usingfread()
as before.Send the resulting message
reply[]
to the clientModify your call of
sent_protocol()
for this case to indicate that"400 (Bad request)"
was sent to the client(Retain the
free()
call for deallocating the memory that was dynamically allocated toreply[]
.)
Later in
do_work()
(orworker()
), your code sendsInvalid request
to the client ifparse_GET()
returns 0. Also replace that message by an HTTP 400 protocol message.Update the file
protocol_test.out_new
to include the proper HTTP 400 protocol replies.Add a command
./add_http 400
to the actions for the updating rule forprotocol_test.out_new
.Enter the commands
touch protocol_test.in make
to regenerateprotocol_test.out_new
.Check the updated contents of
protocol_test.out_new
. For example, section#6
ofprotocol_test.out_new
should now consist of the following lines:HTTP/1.1 400 Bad RequestCR Content-Type: text/html; charset=utf-8CR Content-Length: 195CR CR <html> <head><title>400 Bad Request</title></head> <body><p style='text-align:center; font-size:150%'>400 Bad Request</p> <p>The server received an incorrectly formed request.</p></body> </html> # 6
Recompile, and test run your server with
client_tests
. Verify that thediff
produces no output, or debug until these tests succeed.Make a commit of your successful changes so far.
git add work.c Makefile git commit -m "Task O - HTTP 400 protocol replies"
Now add further tests in
parse_GET()
to insure that tokens in the client's request have expected values.In
parse_GET()
, after you have verified that there are three tokens in the first line of protocol received from the client, check that the third token matches the stringHTTP/1.1
. If the strings don't match, return 0 fromparse_GET()
To test this case, add sections
#16
and#17
toprotocol_test.in
andprotocol_test.out
(notprotocol_test.out_new
).Recompile, test your server with
client_tests
, and verify thatclient_tests
produces no output, or debug until this test succeeds.Make a commit of your successful changes so far.
git add parse.c git commit -m "Task O - complete checking for first line of GET protocol"
In
parse_GET()
, after you have verified that there are two tokens in the second line of protocol received from the client, check that the first of those tokens matches the stringHost:
. If the strings don't match, return 0 fromparse_GET()
To test this case, add sections
#18
through#21
toprotocol_test.in
andprotocol_test.out
(notprotocol_test.out_new
).Recompile, test your server with
client_tests
, and verify thatclient_tests
produces no output, or debug until this test succeeds.
Once your changes are tested and debugged, commit and send them to stogit.
git add parse.c git commit -m "Task O DONE - Implement 400 (Bad Request)" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - P. Implement 404 (Not Found)
(Prereq: Task M; involves
work.c Makefile
) -
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
For Fall 2019: Make fresh copies of
protocol_test.in_all
andprotocol_test.out_all
for use in this task.chmod +w protocol_test.in_all protocol_test.out_all cp ~rab/os/pp2/protocol_test.in protocol_test.in_all cp ~rab/os/pp2/protocol_test.out protocol_test.out_all chmod -w protocol_test.in_all protocol_test.out_all
Locate the code in
do_work()
(orworker()
) where theCannot open file
message is constructed (inreply[]
) and sent to the client.This occurs when a
GET
request has successfully been received, but the indicated file does not open correctly usingfopen()
.
Instead of copying a message
Cannot open file
intoreply[]
, construct a two-part message consisting of a"404 Not Found"
HTTP header followed by the contents of the file404.html
.Delete or comment out the code for inserting
Cannot open file
at the beginning ofreply[]
.Open the file
404.html
usingfopen()
.The file
404.html
should have been added to your team's repository in Task M.
Determine the length of
404.html
usingfseek()
andftell()
as above, and callrewind()
to reposition the open file back to the beginning.Call
http_header()
to insert an HTTP 404 header at the beginning ofreply[]
, using the length of404.html
for the third argument of that call.Append the contents of
404.html
toreply[]
usingfread()
as before.Send the resulting message
reply[]
to the clientModify your call of
sent_protocol()
for this case to indicate that"404 (Not implemented)"
was sent to the client(Retain the
free()
call for deallocating the memory that was dynamically allocated toreply[]
.)
Update the file
protocol_test.out_new
to include the proper HTTP 404 protocol replies.Add a command
./add_http 404
to the actions for the updating rule forprotocol_test.out_new
.Enter the commands
touch protocol_test.in make
to regenerateprotocol_test.out_new
.Check the updated contents of
protocol_test.out_new
. The section#15
ofprotocol_test.out_new
should now consist of the following lines:HTTP/1.1 404 Not FoundCR Content-Type: text/html; charset=utf-8CR Content-Length: 189CR CR <html> <head><title>404 Not Found</title></head> <body><p style='text-align:center; font-size:150%'>404 Not Found</p> <p>Sorry, that page isn't available on this server.</p></body> </html> # 15
Recompile, and test run your server with
client_tests
. Verify that thediff
produces no output, or debug until these tests succeed.Once your changes are tested and debugged, commit and send them to stogit.
git add work.c Makefile git commit -m "Task P DONE - Implement 404 (Not Found)" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - Q. Worker thread
(Prereq: Task G; involves
work.c do_work.c do_work() workdat Makefile
) -
This step calls for executing most of the body of
do_work()
in a pthread, rather than in themain()
thread for theserver
process. For now, themain()
thread will simply block until that pthread to finish (by callingpthread_join()
), but in Task R we will add a loop inmain()
and avoid blocking themain()
thread, so that multiple worker threads might execute at once (multi-threaded server).Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Move the definition of
struct workdat
intowork.h
(so thatstruct
can be shared withserver.c
).
Add a field namedtid
(for Thread ID) of typepthread_t
tostruct workdat
, to be filled in with a new pthread's ID. (This field will be needed for logging when there are multiple threads.)
Also add#include <pthread.h>
to bothserver.c
andwork.c
.Note: It's a good practice to
#include
system header files (such aspthread.h
) before user-defined header files (such aswork.h
), because a user-defined header file might interfere with some internal naming within a system header file.
Modify the
Makefile
to use the flag-pthread
for compiling bothserver.c
andwork.c
, and the same flag-pthread
when linking to produce the executableserver
.Note: It's simplest to add
-pthread
to the default compiling rule (target%.o
) and also to the link command forserver
(i.e., the action for the targetserver
).If you use other
.c
source files for buildingserver
, the extra compilation flag-pthread
won't interfere with compiling those additional.c
files.
Now, use
make
to generate a new version ofserver
, and test it with (unchanged)client
in the usual way (as in HW4). The client and server should behave as they did before making these changes related tostruct workdat
.Make a commit of your successful changes so far.
git add work.c work.h Makefile git commit -m "Task Q - add thread ID field tid to struct workdat"
Next, replace the
struct workdat
variablewd
by a pointer variablewdp
that is dynamically allocated:struct workdat *wdp; wdp = (struct workdat *) malloc(sizeof(struct workdat));
Also replace field references such aswd.rid
by pointer expressions such aswdp->rid
, wherever these appear indo_work()
.Re
make
server
and test with the (unmodified)client
, debugging as necessary.Note: If you make mistakes you can't figure out, either ask for help with debugging or, if necessary, revert to the previous commit.
Make a commit of your successful changes so far.
git add work.c git commit -m "Task Q - change workdat variable wd to a dynamically allocated pointer wdp"
Now define a function
worker()
for a new pthread to execute, which performs all the code ofdo_work()
after the definition and allocation ofwdp
and the assignment ofdo_work()
's request ID argument towdp->rid
. Since it will become the code for a pthread, your new functionworker()
should have the following function prototype:void *worker(void *datp) {...
Notes:You may either define
worker()
beforedo_work()
, or you may use a function declaration ofworker()
before the definition ofdo_work()
, and locate the definition ofworker()
afterdo_work()
.Suggestion: Insert temporary
printf()
calls at the beginning and end ofworker()
printf("Starting worker()\n"); ... printf("Ending worker()\n");
These calls could help with debugging this step and the future step when a pthread executesworker()
.
Also, insert a call
worker(wdp);
intodo_work()
where the moved code used to be.(Note: It is not necessary to cast non-
void
pointers to thevoid
pointer type in C, e.g.,(void*)
is unnecessary inworker((void*)wdp);
)
Finally, the argument of
worker()
is a pointerdatp
of typevoid*
, but we knowdatp
actually refers to a (dynamically) allocatedstruct workdat*
pointer. The code we moved fromdo_work()
to the new functionworker()
refers towdp
, and replacing all those occurrences ofwdp
by((struct workdat*) datp)
would make the code less readable. Therefore, define a pointer variablestruct workdat *wdp = datp; wdp->tid = pthread_self();
at the beginning ofworker()
, and leave the references towdp
unchanged in the remainder ofworker()
.
The second line here assigns the new pthread's ID to the added fieldtid
of the pthread'sstruct workdat
data structure.(Note: It is not necessary to cast
void
pointers to a non-void
pointer type in C, e.g.,(struct workdat*)
is unnecessary instruct workdat *wdp = (struct workdat*)datp;
This is unlike C++, wherevoid
pointers must be cast when assigned to non-void
pointers.)
Compile and test these changes to the server, and debug as necessary. The tests should behave the same as they did before you implemented this
worker()
step.Make a commit of your successful changes so far.
git add work.c git commit -m "Task Q - implement worker() function for a pthread, and test as a helper for do_work()"
Finally, carry out
worker()
in a new pthread, created withindo_work
. This means replacing the existing call ofworker()
bypthread_t thread_id; pthread_create(&thread_id, NULL, worker, wdp);
Just after creating this thread, perform apthread_join()
operation onthread_id
to blockdo_work()
until that thread completes its work.Then, after the thread has terminated (i.e., after calling
pthread_join()
), we should deallocate the memory that was dynamically allocated for the pointerwdp
, in order to avoid a "memory leak".free(wdp);
Compile and test these changes, and debug as necessary. The
server
/client
tests should behave the same as they did before you implemented this step.Once your changes are tested and debugged, commit and send them to stogit.
git add work.c git commit -m "Task Q DONE - Worker thread" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - R. Multiple worker threads
(Prereq: Task Q; involves
server.c work.c main() do_work() Makefile
) -
After Task Q, the work of handling a client interaction takes place within a pthread that executes
worker()
instead of the server's main thread as it callsdo_work()
. We now want to modifymain()
(inserver.c
) to enclose the call ofdo_work()
in a loop, in order to handle multiple client requests, using parallel pthreads.Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
The
main()
inserver.c
currently performs oneaccept()
of a client connection, then callsdo_work()
to handle that connection. Enclose those calls ofaccept()
anddo_work()
(plus theprintf()
that precedes that call ofaccept()
) in an infinite loopwhile (1) { ... }
Test that this modification accepts multipleclient
requests and handles them correctly.Note: You may use CTRL/C in the
server
window to exit this infinite loop when you want to terminate your server. (It is an extra feature to quit the server more gracefully.)
Make a commit of your successful changes so far.
git add server.c git commit -m "Task R - multiple client requests"
The server now executes each client request in a separate pthread, but those pthreads run sequentially (without parallelism) because
do_work()
(which runs in the server's main thread) blocks by callingpthread_join()
on the pthread it creates. To achieve parallel execution of the pthreads, we will move thepthread_join()
calls fromdo_work()
tomain()
and make those join calls non-blocking. This way, the loop inmain()
can go on toaccept()
new client connections while prior client requests may still be in progress.But this means we need a data structure to hold IDs for pthreads that haven't been joined yet. We also need to
free()
thestruct workdata
data structures that were dynamically allocated indo_work()
(by the server's main thread), in order to avoid memory leaks (see the pthread creation step above)We will define this data structure as an array of pointers to
struct workdat
, since those structs need to be deallocated, and since they also hold the thread id's that must be joined in theirtid
field.To start on this agenda, we will first build the array of pointers to
struct workdat
and partially test it withinwork.c
, as follows.Create a new source file
wdp_array.c
that definesa preprocessor constant for an array size
#define MAX_ARRAY 100
a
static
array of pointers tostruct workdat
static struct workdat *wdp_array[MAX_ARRAY];
Here, the keywordstatic
indicates thatwdp_array[]
is accessible only within the filewdp_array.c
. Since the arraywdp_array[]
is allocated globally (within the filewdp_array.c
), the C language initializes all its elements as 0.A function
add_wdp()
that satisfies the following spec.add_wdp
1 arg: type
struct workdat*
, representing the address of the data structure for a created pthread.State change: For the lowest index
i
such thatwdp_array[i]
is 0, assign arg1 to that elementwdp_array[i]
.
If allMAX_ARRAY
elements ofwdp_array
are non-zero, then print an error message and call_exit(1)
to quit the server immediately.Return: none
A function
check_threads()
that satisfies the following spec.check_threads
0 args.
State change: For all non-zero elements of the array
wdp_array[]
, callpthread_tryjoin_np()
on that element'stid
field. If that call returnsEBUSY
, take no action; otherwise, deallocate that element'sstruct workdat
usingfree()
, then assign 0 to that element.Return: none
Add
#include
directives at the beginning ofwdp_array.c
for the system include files needed for functions used in your code, plus#include "work.h"
in order to define the data typestruct workdat
.Notes:
A straightforward implementation is sufficient for the basic assignment. For example,
add_wdp()
could use a simple loop to iterate until an element ofwdp_array[]
containing 0 is discovered (or allMAX_ARRAY
elements have been checked).However, implement thread-safe versions of your functions
add_wdp()
andcheck_threads()
. In the end,main()
will callcheck_threads()
, and each pthread will need to calladd_wdp()
for its ownstruct workdat
(in order to get the timing correct for that call).You can use a
pthread_mutex_t
lock variable as inpthreads.c
in order to achieve mutual exclusion among calls of the functionsadd_wdp()
andcheck_threads()
. Be sure to use a single lock variable in both functions -- we want to use the same lock variable, since both functions are interacting with the same shared data structurewdp_array[]
.
Also create a header file
wdp_array.h
that consists only of function declarations foradd_wdp()
andcheck_threads()
.Add a
Makefile
target forwdp_array.o
in order to specify the prerequisiteswdp_array.c wdp_array.h work.h
. Try compiling your new file withmake wdp_array.c
and debug whatever syntax issues arise.Now temporarily modify
check_threads()
to comment out the call ofpthreads_tryjoin_np()
and replace it by a call ofpthreads_join()
.Add an
#include
directive forwdp_array.h
towork.c
.
Add the calladd_wdp(wdp);
just after assigningpthread_self()
towdp->tid
inworker()
.
Replace the calls ofpthread_join()
andfree()
withindo_work()
bycheck_threads();
Proceed to
make
the server, then test the server and (unmodified) client, debugging as necessary. The system should behave as before (in part becausepthread_join()
is being used instead ofpthread_tryjoin_np()
).
Make a commit of your successful changes so far.
git add wdp_array.c wdp_array.h work.c Makefile git commit -m "Task R - create wdp_array[], with preliminary test within work.c"
Now, move thread checking to
main()
as follows.Move the call of
check_threads()
fromdo_work()
(inwork.c)
tomain()
(inserver.c
), at the end of the infinite loop inmain()
. This will allow a call ofdo_work()
to return without waiting for its pthread to terminate.Also modify
check_threads()
(inwdp_array.c
) to remove the temporary call ofpthread_join()
and restore the original call ofpthread_tryjoin_np()
.Add
#include "wdp_array.h"
toserver.c
, and add that dependency to theMakefile
target forserver.o
.
Also addwdp_array.o
as both a dependency for the executable targetserver
and in the link action for that target.Proceed to
make
the server and test with the client. This test should behave as before.
Once your changes are tested and debugged, commit and send them to stogit.
git add server.c work.c wdp_array.c Makefile git commit -m "Task R DONE - Multiple worker threads" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index - S. Browser test (Prereq: Task M)
-
In this task, use an ordinary web brower (such as Chrome) to request and receive a web page from your
server
.This is the final task for the complete basic assignment.
Link to index - T. Parallel threads demo (Extra feature)
(Prereq: Task R and Task H; involves
work.c wdp_array.c worker() add_wdp() check_threads()
) -
This task is an extra feature. In this task, demonstrate that your worker threads can actually execute in parallel.
Switch to your development branch
git checkout mybranch
If there may have been updates tomaster
since you last usedmybranch
(e.g., updates by a team member), then updatemybranch
as follows.git merge origin/master git push mybranch
Add an artificial delay in
worker()
, e.g.,sleep(30);
Be sure to google thesleep()
library function and make sure thatwork.c
has the required#include
directives in order to make this call.Locate this artificial
sleep()
call at about the point in the code whereworker()
carries out a client's request.If your team has completed Task K (
log.c
), insert thesleep()
call between the second and third log messages.Otherwise, add temporary
printf()
calls just before and just after your code inworker()
that sends a response message to the client, and add thesleep()
call between those temporaryprintf()
calls.These temporary
printf()
messages should include the request IDwdp->rid
, the thread IDwdp->tid
(cast as anint
), and whether thatprintf()
is before or after sending the message to the client.
These temporary
printf()
calls can be removed when your team completes Task K.Also add temporary
printf()
calls in the functionsadd_wdp()
andcheck_threads()
inwdp_array.c
.______
______
______
______
______
______
______
______
______
______
____________
Use
make
to generate a new version ofserver
with the artificial delay. Test thisserver
with a singleclient
over the network - it should satisfy the request as usual, except for the long delay.______
______
______
______
______
______
______
______
______
______
______
______
______
Once your changes are tested and debugged, commit and send them to stogit.
git add work.c git commit -m "Task T DONE - Parallel threads demo (Extra feature)" git pull origin mybranch git push origin mybranch git checkout master git merge mybranch git pull origin master git push origin master
Link to index