首页 > > 详细

CIS 455/555讲解、Web Systems辅导、讲解Java/Python编程语言 解析C/C++编程|讲解R语言程序

CIS 455/555: Internet and Web Systems
Spring 2019
Homework 1: Web server and Microservice Framework
Milestone 1 due February 11, 2019, at 10:00pm EST
Milestone 2 due February 22, 2019, at 10:00pm EST
1. Background
A Web server represents a nice, limited-scale introduction to building a server system. It requires careful attention to
concurrency issues, and it requires well-designed programming abstractions to support extensibility. However, the
actual HTTP protocol is not incredibly complex. You’ll start by dealing with the issues of handling concurrency,
before building in a set of abstractions for future extensions.
In the early days of dynamic, Web server-based applications, interfaces such as Java servlets were commonplace.
However, servlets are heavyweight -- requiring fairly cumbersome frameworks such as Apache Tomcat or Eclipse
Jetty, having complex APIs, etc. Over time, other approaches were developed, particularly in server platforms for
other languages (e.g., Node.js for JavaScript, Django for Python). A key idea here was the notion of
programmatically mapping routes (paths and patterns) to callback or event handlers (functions called by the Web
server).
In recent years, the Spark Framework ported these same abstractions to Java, and it has gained significant traction
within the Web community. We’ll be implementing a framework that emulates the Spark APIs -- and gives you
insight into what is going on inside Spark (and also Jetty, which Spark runs at its core). Once you build out your
Homework 1 server and framework, you will ultimately use it to (1) serve HTML pages, e.g., for your search engine;
(2) supports actions in response to HTML forms, e.g., to trigger the actual search; (3) support REST Web service
calls, e.g., to build distributed crawling.
This assignment focuses on developing an application server, i.e., a Web (HTTP) server that runs Java-based
services, in two stages. In the first stage, you will implement a simple HTTP server for static content (i.e., files like
images, style sheets, and HTML pages). In the second stage, you will expand this work to handle Web service calls.
Web service calls are the underpinnings of many aspects of the cloud, and are used to invoke operations remotely.
One method for creating services in Java is through servlets, with which you might be familiar if you’ve taken CIS
450/550. Increasingly, however, there has been a movement towards lighter-weight RESTful services that expose
APIs over HTTP’s GET, POST, DELETE, PUT, and other operations. Such services are typically written by
attaching handlers to various routes or URL paths. In Java, perhaps the most popular framework for such operations
is the Spark Framework (not to be confused with Apache Spark). We will implement a microservices framework
that emulates the Spark API.
1.1. A Sample Program in the Spark Framework
This homework assignment consists of two milestones. Technically, you can build a one-off solution for Milestone 1,
then throw much of it away in order to do Milestone 2. That’s not what we recommend -- instead we want you to
understand where you are going with Milestone 2 before embarking on Milestone 1. If you implement Milestone 1understand where you are going with Milestone 2 before embarking on Milestone 1. If you implement Milestone 1
using the same raw framework as Milestone 2 (not necessarily implementing every bell and whistle initially), you’ll
be much better positioned for the second milestone.
Thus, we begin by explaining the programming abstraction we are providing (cloning from open source) for this
project. Let’s look at a sample Spark Framework program, from their “Getting Started” documentation.
import static spark.Spark.*;
public class HelloWorld {
public static void main(String[] args) {
get("/hello", (req, res) -> "Hello World");
}
}
Upon a browser request (to localhost:8080/hello), it returns “Hello World.” Super simple, right?
Before diving into how it works, it’s worth noting two aspects of Java 8 that you may not have seen.
1. import static spark.Spark.* finds the spark.Spark class (in a JAR file) and imports all static functions in the
Spark class to the global scope. Thus, public static void Spark.get(...) is callable as get().
2. (req, res) -> “Hello World” is a lambda function that may be familiar to you if you have used functional
languages. It takes a pair of input parameters, req and res, and returns a string (“Hello World”). The types of
req, res, and the function return value were defined by the get() function itself: namely, they are of type
Route which takes a Request and a Response, returns a String, and optionally throws a HaltException if
the request results in an error.
Recall that HTTP defines a mechanism for the Web server (or a client) to make a request, and that the server must do
some computation and send back a response.
The call to get is registering a route handler for HTTP GET requests (to /hello). (Other kinds of HTTP requests,
such as POST, DELETE, HEAD, etc. are also mapped to route handlers using the same basic style.) This simple
route does not have any patterns or variables, but more complex routes can make define parameters in the path or as
an HTTP query string.
The Route handler is the trivial “Hello World” lambda function here. In Spark Framework, the handler (1) writes
results to a String return result, (2) has the ability to modify other aspects of the HTTP response by modifying the
Response object. If something goes wrong, the handler can throw a HaltException that returns an HTTP error code.
A more interesting sample program can look up details from the request, and set parts of the response:
// matches "GET /hello/foo" and "GET /hello/bar"
// request.params(":name") is 'foo' or 'bar'
get("/hello/:name", (request, response) -> {
String cookie = request.cookies("cookie");
response.type("text/plain");
return "Hello: " + request.params(":name") + cookie;
});
Later in the semester, we’ll see how REST calls are mapped to these same sorts of routes.
1.2. The Basic CIS 455/555 HTTP Server Framework
Our code framework is very heavily based on the Spark Framework, in order to ensure it is realistic. As such, yourOur code framework is very heavily based on the Spark Framework, in order to ensure it is realistic. As such, your
code may be tested against a variety of JUnit tests, and some aspects of it need to conform to our API.
As the saying goes, a picture is worth a thousand words. So we’ll start with an illustration of the standard “flow”
among the classes we have given you, which we expect to occur as Web requests are made.
The WebServer takes command line arguments. With the help of the WebServiceController (which is create and
configure a WebService containing an HttpServer listening on a port) and a ServiceFactory (which creates objects
conforming to standard interfaces), the WebServer instantiates an HttpServer with an internal thread-safe
HttpTaskQueue and a series of HttpWorkers. Each worker, as it receives an HttpTask from the queue, uses the
HttpIoHandler to parse the data on the socket, create a Request, call an HttpRequestHandler to handle the
request, and creates a Response that is then sent via HttpIoHandler to the client.
For the first milestone, the HttpRequestHandler will simply take a limited subset of information from the Request,
such as the URL path, and fetch and return a file. Subsequently, in Milestone 2 you’ll generalize the handler to
invoke custom code.
2. Developing and running your code
Now you have some understanding of how things are supposed to work. Before you start writing an implementation,
we strongly recommend that you do the following:
1. Carefully read the entire assignment (both milestones) from front to back and make a list of the features
you need to implement.
2. Read the debugging and testing tips below, for hints on how to approach testing and debugging.
3. Check out the project ProducerConsumerDemo from Bitbucket (refer to HW0 instructions for Importing
from Git into Eclipse Che; use the Java Stack and the Git URL https://bitbucket.org/upenn-cis555/555-
producer-consumer.git) and see if you can understand how it (1) uses synchronization and (2) uses logging. 4. Think about how the key features will work. For instance, before you start with MS2, go through the
steps the server will need to perform to handle a request. If you still have questions, have a look at some of
the extra material on the assignments page, or ask one of us during office hours.
5. Spend at least some time thinking about the design of your solution. What classes, beyond the ones we
have provided, will you need? How many threads will there be? What will their interfaces look like? Whichhave provided, will you need? How many threads will there be? What will their interfaces look like? Which
data structures need synchronization? And so on.
6. Regularly check your changes into your subversion repository. This will give you many useful features,
including a recent backup and the ability to roll back any changes that have mysteriously broken your code.
You may not use any third-party code other than the standard Java libraries (exceptions noted in the
assignment) and any code we provide. Yes, there are higher-level libraries that do some of the core
functionality we want -- but the goal is to learn how these work!
2.1 Getting the Homework Source Code
We recommend that you continue using Eclipse Che and Bitbucket as with HW0. As with HW0, you should first
fork the framework code for HW1 (from https://bitbucket.org/upenn-cis555/555-hw1) to your own private repository;
then clone it from your repository to Eclipse Che by creating a new Che Workspace (Java stack, dev-machine, Add
or Import Git repository, URL to your private repository). The process is essentially the same as for HW0 (if you
gave the workspace a name other than the default, there should now be a new "homework-1" or “555-hw1” workspace listed in the sidebar). Recall that there is a bit of work to set the project and the Maven run options. For
instance, once the project loads, you will probably need to select the “555-hw1” folder in the project pane,
Project|Update Project Configuration… and choose Java.
When you set the Run option for Maven, use
mvn exec:java -f ${current.project.path} -Dexec.args="8080 ${current.project.path}/www"
When you set the Preview URL for Eclipse Che, just use ${server.tomcat8}/.
When you set the Debug option for Maven, use:
cd ${current.project.path} && mvn install -DskipTests && mvnDebug exec:java -Dexec.args="8080 ./www"
To ensure proper grading, your submission must meet the requirements specified in Sections 3.3 and 4 below - in
particular, it must build and run correctly in Eclipse Che and have a mvn script that correctly builds and runs the
code. During submission, the build server will attempt to “vet” your code and let you know if there are any issues,
but we cannot promise that this pre-screening will be 100% effective.

We strongly recommend that you regularly check the discussions on Piazza for clarifications and solutions to
common problems.
2.1. Logging, testing, and mock objects
In this homework, we make use of three standard techniques and tools that are extremely valuable when trying to
develop high-quality code (and save both sleep and sanity). Note that tests should help you flesh out, develop, and
validate your code and you should be writing tests even as you write your code. (There are some who believe you
write the test cases first and then write the code!) You should make use of:
1. Logging infrastructure. You’ve probably used System.out.println at various points to see what’s going
on in your program. Logging tools, like Apache Log4J, are a generalization of this idea and allow much
finer-grained control of what messages you see.
2. Modular interfaces and unit tests. You’ve probably used JUnit tests before, to validate functionality in
your code. For something like a Web server, unit-testing sub-functions (e.g., the HTTP request parser, sleep
and wake-up on the request queue, setting the response variables correctly, setting cookies) is extremely
helpful in validating the overall functionality. Maven helps automate this process.
3. Mock objects. Sometimes you’ll need to simulate different parts of the code with stubs or fake (“mock”)
objects. For instance, to test HTTP parsing from a socket, you might want to create a “fake” socket that lets
you closely manage what is sent and received. Libraries like Guava allow you to do this.you closely manage what is sent and received. Libraries like Guava allow you to do this.
2.1.1. Logging in Log4J
Let’s first talk about the logging system. Apache Log4J (and other implementations of logging infrastructure) allow
your program to log progress, in several ways that are more powerful than System.out.println:
1. The log can go to a file instead of, or in addition to, the console. In real server applications, you would
always write your data to a log file.
2. The logging infrastructure records what code wrote the log message, in addition to the message itself. That
can be incredibly helpful in debugging!
3. The logging infrastructure lets you specify different levels of messages: typically low-level debug
messages (DEBUG), informational messages (INFO), warnings (WARN), and full errors (ERROR). You can
set the level of messages you want to see, e.g., to disable debug messages when you think the program is
mostly running OK.
4. The logging infrastructure lets you turn on and off the sources of messages: you may only want to look at
logging messages from a certain class or package.
We are using Apache Log4J v2 in this course. This version of Log4J will optionally look for a log4j.yaml describing
where you want to send the log messages, what you want to capture, etc. You can find details on this via Google.
For simplicity we are actually just setting the Log4J configuration in the main program -- you can see in
edu.upenn.cis.cis455.WebServer or in edu.upenn.cis.cis455.m1.server.TestSendException a call to
Configurator.setLevel that records DEBUG (and above) messages for everything in the
edu.upenn.cis.cis455 package. By default the output will go to the console.
For each class that uses the logger, you need to (1) create a static logger object for the class, (2) write to the logger. If
you look at edu.upenn.cis.cis455.HttpParsing you’ll see at the top how we create the logger, ultimately
parameterizing it with the class. Be sure to pass in the correct class to the getLogger() function, as this is used to
help you track and filter where log messages are originating. Finally, you can call logger.info(), logger.debug(),
logger.error(), etc. in your code to write to the log.
2.1.2. Unit tests and mock objects
As mentioned previously, you should think carefully about the components of your server and how to expose the
“right” interfaces. Hopefully the set of default classes and the figure early in this document will help a bit. If you do
your design well (or iterate until you get it right), you’ll likely have some logical ways of testing the functionality in a
self-contained way. It’s time to develop a set of unit tests in JUnit. If you put them anywhere under the
src/test/java directory in your project, Maven will automatically run the tests when you build the program -- thus
making sure you didn’t inadvertently introduce bugs.
We’ve given you one sample JUnit test, edu.upenn.cis.cis455.m1.server.TestSendException, which is designed to
test that the HttpIoHandler.sendException function correctly sends a 404 HTTP error when called. You might want
to do something similar to test that you can parse requests (and send errors when the parsing fails), to test that a
correct response is sent, etc.
If you look at our sample code in the provided TestHelper, you’ll see that we create a “mock object” for the request
Socket, using a library called Guava, and making use of ByteArray streams to emulate the input and output to the
socket. You can see the code here:
Socket s = mock(Socket.class);
byte[] arr = sampleGetRequest.getBytes();
final ByteArrayInputStream bis = new ByteArrayInputStream(arr); final ByteArrayInputStream bis = new ByteArrayInputStream(arr);
final ByteArrayOutputStream byteArrayOutputStream = new
ByteArrayOutputStream();
when(s.getInputStream()).thenReturn(bis);
when(s.getOutputStream()).thenReturn(byteArrayOutputStream);
when(s.getLocalAddress()).thenReturn(InetAddress.getLocalHost());
when(s.getRemoteSocketAddress()).thenReturn(
InetSocketAddress.createUnresolved("host", 80));
The s object partly emulates a Socket, but is in fact not a real socket. We create a “fake” input stream that is returned
on a call to s.getInputStream() -- instead this returns the ByteArrayInputStream bis. Similarly, s.getOutputStream() returns the
ByteOutputStream byteArrayOutputStream. Similarly, we create a default InetSocketAddress for a call to
s.getRemoteSocketAddress(). When we pass this object s to our code-to-be-tested, it can’t tell this isn’t a “real” socket so
it will read/write as appropriate. When we test sendException() we expect it to not actually read from the input
stream, but to write to the output stream. Now we can read the output stream from the byte array “underlying” the
ByteArrayOutputStream, convert it to a string, and test what is in the string.
You should be able to generalize your own unit tests from this sample. Note that we’ve shown how to pass in an
input to the mock socket here -- which isn’t useful for testing the sendException() function but might be useful in
your own code.
2.2. “Integration testing” your server
If you did a good job with your unit tests, a lot of the basic functionality will work in the server. However, you
ultimately need to “integration testing” as a whole server. Please take a look at the debugging/testing tips we have
mentioned below, just as a reminder of how to chase down the inevitable bugs.
Note that your server will logically have two different IP ports. From your browser -- Chrome or an
alternative -- the server will sit at localhost:34xxx or similar, as described by the “preview” link when you run
the server. This is because Docker maps port 8080 inside the container to some external address. From within
your container -- i.e., from within your Java program or anything running in the Che Terminal -- the server
will be on port 8080.
The expected “main program” for your Web server should be WebServer. If you configure Maven and Che to run
WebServer and create a “preview”, similar to per HW0 Section 2.4, you will get a link to the Web server that you can
click on from the browser. To test your server, you have a variety of options:
● You can use the Developer Tools in Chrome to inspect the HTTP headers. Open the main Chrome menu,
choose "More Tools", and click on "Developer Tools". This should pop up a new window. Click on the
Network tab, which will list all the HTTP requests processed by Chrome (click on a request for extra
details).
● If you want to check whether you are using the correct headers, you may find the site websniffer.net
useful.
● From the Terminal, you can use the telnet command to directly interact with the server. Run:
sudo apt-get update
sudo apt-get install telnet
telnet localhost 8080
Then type in a GET request, and hit Enter twice; you should see the server's response.
More advanced tools, as you have a “mature” server and want to see how well it does, can be run from the Che
Terminal:
● You may also want to consider using the curl command-line utility to do some automated testing of your
server. curl makes it easy to test HTTP/1.1 compliance by sending HTTP requests that are purposefully
invalid - e.g., sending an HTTP/1.1 request without a Host header. 'man curl' lists a great many flags.
● To stress-test your server, you can use Apachebench:
o Run sudo apt-get update and then sudo apt-get install apache2-utils.
o Apachebench (ab) can be configured to make many requests concurrently, which will help you findo Apachebench (ab) can be configured to make many requests concurrently, which will help you find
concurrency problems, deadlocks, etc.
We suggest that you use multiple options for testing; if you only use Firefox, for instance, there is a risk that
you hard-code assumptions about Firefox, so your solution won't work with curl or ab. You may also want to
compare your server's behavior (NOT performance!) with that of a known-good server, e.g., the CIS web
server. Please do test your solution carefully! Also, please do NOT run Apachebench across major Web sites, as you
will likely make them angry for doing what looks like a DoS attach on them!
3. Milestone 1: Multithreaded HTTP Server
For the first milestone, your task is relatively simple. You will develop a Web server that can be
invoked from the command line, taking the following parameters, in this order:
1. Port to listen for connections on. Port 80 is the default HTTP port, but it is often blocked by firewalls, so
your server should be able to run on any other port (e.g., 8080).
2. Root directory of the static web pages. For example, if this is set to the directory /var/www, a request for
/mydir/index.html will return the file /var/www/mydir/index.html.
(do not hard-code any part of the path in your code - your server needs to work on a different machine, which
may have completely different directories!) By default we have included a subdirectory called www, but you
shouldn’t assume this is the only possible Web “home” directory.
So, for instance, if you go into your /projects/555-hw1 directory and run java –cp target/homework-1-1.0-SNAPSHOT.jar
edu.upenn.cis.cis455.WebServer 8080 /home/cis455/myweb this should run your server on port 8080. By default, if no
parameters are given, you should assume that your server uses port 8080, and that the root directory for static
Web pages is “./www”.
In its initial version, your program will accept incoming GET and HEAD requests from a Web browser, and it will
make use of a thread pool (as discussed in class) to invoke a worker thread to process each request. When an HTTP
request is received by the worker, simplified Request and Response objects should be created (see
...m1.server.interfaces for the foundations), and a simple HttpRequestHandler should determine which file was
requested (relative to the root directory specified above) and return the file. If a directory was requested, the
request should return a 404 error. Your server should return the correct MIME types for some basic file formats,
based on the extension (.jpg, .gif, .png, .txt, .html). Keep in mind that image files must be sent in binary/byte stream
form -- not with println or equivalent -- otherwise the browser will not be able to read them. If a GET or
HEAD request is made that is not a valid UNIX path specification, if no file is found, or if the file is not accessible,
you should return the appropriate HTTP error. See the HTTP Made Really Easy paper for more details.
MAJOR SECURITY CONCERN: You should make sure that users are not allowed to request absolute paths or
paths outside the static file root directory. We will validate, e.g., that we can't get hold of /etc/passwd!
3.1. Expected level of HTTP protocol support
Your application server must be HTTP 1.1 compliant, and it must support all the features described in HTTP Made
Really Easy. However:
● Persistent connections are suggested but not required for HTTP 1.1 servers. If you do not wish to support
persistent connections, be sure to include “Connection: close” in the header of the response.
● Chunked encoding (sometimes called chunking) is also not required. Support for persistent connections
and chunking is extra credit, described near the end of this assignment.and chunking is extra credit, described near the end of this assignment.
HTTP Made Really Easy is not a complete specification, so you will occasionally need to look at RFC 2616 (the
'real' HTTP specification; http://www.ietf.org/rfc/rfc2616.txt) for protocol details. If you have a
protocol-related question, please make an effort to find the answer in the spec before you post the question to
Piazza!
3.2. Special URLs
Your application server should implement two special URLs. If someone issues a GET /shutdown, your server
should shut down immediately after any threads handling requests have finished. If someone issues a GET
/control, your server should return a 'control panel' web page, which must contain at least a) a list of all the
threads in the thread pool, b) the status of each thread ('waiting' or the URL it is currently handling), and c) a button
that shuts down the server, i.e., is linked to the special /shutdown URL. It must be possible to open the special
URLs in a normal web browser.
3.3 Core Requirements
For efficiency, your application server must be implemented using a thread pool that you implement, as discussed in
class. Specifically, there should be one thread that listens for incoming TCP requests and enqueues them, and some
number of threads that process the requests from the queue and return the responses. We will examine your code to
make sure it is free of race conditions and the potential for deadlock, so code carefully!
3.3.1. Standard API
You must also implement the standard interface, which we’ll be using to test. Refer to the Figure in Section 1.2 for
the overall “flow.”
● The controller: WebServiceController. Just as developers statically import spark.Spark.* in the Spark
Framework and use it to configure routes and server behavior, developers using your Web server framework
will statically import edu.upenn.cis.cis455.WebServiceController. This class is used by the developer’s
main program to control the Web services. It establishes the port, routes, etc., via functions such as get(),
post(), etc.
● The WebService, which encapsulates an HttpServer (see below) listening on a specific port. It is the actual
code that registers the route-handling and configuration calls such as get(), post(), etc., and it sets up the
HttpServer and other code as appropriate.
● The objects to capture the HTTP Request and Response. We have a first version of these in m1.server,
which have a subset of the whole API appropriate for Milestone 1. A second, more complete version appears
in m2.server, e.g., handling cookies, form parameters, etc. You’ll need to develop your own
implementations. For Milestone 1 you can extend the m1.server implementations; later you’ll change your
code to extend m2.server.
● The ServiceFactory is used to create the appropriate objects to match the above interfaces. From here, you
should be able to instantiate instances of the HTTP server, a request object, and a response object. You
should assume that, among other things, our test drivers will use the ServiceFactory to instantiate test
objects!objects!
Next, we have two “helper” interfaces you should use but may expand (superset) while preserving existing function
signatures:
● ThreadManager -- this is intended to let workers coordinate with the central Web service control. They
can update status, check whether they should terminate, etc. You would need to implement a subclass of this
to handle your thread management.
● HttpRequestHandler -- this is a Java 8 functional interface for taking a (Request, Response) pair and
returning a result. It is most useful in Milestone 2, but for now it has a subclass called
MockRequestHandler that takes a request and sends a mock result.
3.3.2. Provided Skeleton Code Whose API You Don’t Need to Maintain
We’ve also given you a bunch of starter code to help establish the main components of the project. These are all for
your own internal use, as opposed to public APIs, so you may change the function interfaces. Note that some of our
sample code does call functions here -- so you would need to change accordingly.
● HttpServer: The actual HTTP server with thread pool and thread management. This class should create
and manage the thread poll, listen on sockets, and invoke handlers.
HttpTask -- this is a “unit of work” for a Web service request handler, encompassing the data for a request.
● HttpWorker -- this is the actual Web service “worker” that takes an HTTP request, parses it (using
HttpIoHandler), and sends responses (or exceptions).
● HttpTaskQueue -- this is intended to be the concurrency-protected queue between the server and the
workers.
● HttpIoHandler -- this processes incoming requests on the socket, and sends outgoing responses. For now
there is a (completely stubbed!) sendException() method.
We also provide some helper methods for parsing HTTP requests, in the util package.
4. Milestone 2: Microservices Framework
The second milestone will build upon the Web server from Milestone 1, with support for building HTTP services,
using the full routing framework model based on the Spark Framework interfaces. Basically, your first Milestone
built a static HTML server, and your second Milestone builds out the full microservices framework.
● A developer should be able to register Route handlers, Filters, and the like from the
WebServiceController. ○ Recall that one registers a route handler by calling post(path, handler) for a POST request, get(path,
handler) for a GET request, etc. The path may actually include a pattern with wildcards (see the
description in the Spark Framework documentation). The handler takes an
HttpRequestHandler (which in turn has a handle method that takes a Request and a Response).
○ A Filter is called before (or after) the route handler, and is typically there to prevent certain
requests from being handled (by throwing a HaltException, which is caught before the call to the
route handler) or to add parameters to the HTTP request (giving more detail for the route handler).
Filters are commonly used to validate that a request is authorized (e.g., it belongs to an active
session) before the route handler is called. See, for an example, the before() filter in this Spark
Framework example. ● You are expected to fully implement objects that provide the functionality hinted at with the
...m2.server.interfaces “stub” classes (modeled after Spark Framework). Users should be able to create and
query for Cookies, create Sessions that expire if not renewed, and process HTTP form and GET parameters.
● If a route handler throws a HaltException your server should catch the exception, and return an● If a route handler throws a HaltException your server should catch the exception, and return an
appropriate HTTP response with the error code and message.
● If a route handler calls redirect in the response, your server should send an HTTP redirect message (a 301
or 302 code, as specified in the HTTP spec) to forward the browser to a new location.
● When creating a session, your session token must not be guessable: the client can ‘fake’ the session ID in a
cookie.
● For routes with wildcards, you may assume that a wildcard only matches a single ‘step’ between slashes.
See extra credit for a more general solution.
Additionally:
● You should build test cases for the new functionality, and develop tests to validate parsing, responses,
handlers, etc.
● Logging messages from Log4j should be saved to a log file. You should be sure to use the INFO log level
to track user requests for URLs.
To help you get started (and to form a reference specification), we have given you some additional, more fleshed-out
classes, e.g., for requests, responses, and servers in the ...m2.server “pa
联系我们
  • QQ:99515681
  • 邮箱:99515681@qq.com
  • 工作时间:8:00-21:00
  • 微信:codinghelp
热点标签

联系我们 - QQ: 99515681 微信:codinghelp
程序辅导网!