CIS 455/555: Internet and Web SystemsSpring 2019Homework 1: Web server and Microservice FrameworkMilestone 1 due February 11, 2019, at 10:00pm ESTMilestone 2 due February 22, 2019, at 10:00pm EST1. BackgroundA Web server represents a nice, limited-scale introduction to building a server system. It requires careful attention toconcurrency issues, and it requires well-designed programming abstractions to support extensibility. However, theactual 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 EclipseJetty, having complex APIs, etc. Over time, other approaches were developed, particularly in server platforms forother languages (e.g., Node.js for JavaScript, Django for Python). A key idea here was the notion ofprogrammatically mapping routes (paths and patterns) to callback or event handlers (functions called by the Webserver).In recent years, the Spark Framework ported these same abstractions to Java, and it has gained significant tractionwithin the Web community. We’ll be implementing a framework that emulates the Spark APIs -- and gives youinsight into what is going on inside Spark (and also Jetty, which Spark runs at its core). Once you build out yourHomework 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 servicecalls, e.g., to build distributed crawling.This assignment focuses on developing an application server, i.e., a Web (HTTP) server that runs Java-basedservices, in two stages. In the first stage, you will implement a simple HTTP server for static content (i.e., files likeimages, 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 CIS450/550. Increasingly, however, there has been a movement towards lighter-weight RESTful services that exposeAPIs over HTTP’s GET, POST, DELETE, PUT, and other operations. Such services are typically written byattaching handlers to various routes or URL paths. In Java, perhaps the most popular framework for such operationsis the Spark Framework (not to be confused with Apache Spark). We will implement a microservices frameworkthat emulates the Spark API.1.1. A Sample Program in the Spark FrameworkThis 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 tounderstand 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 1using the same raw framework as Milestone 2 (not necessarily implementing every bell and whistle initially), you’llbe much better positioned for the second milestone.Thus, we begin by explaining the programming abstraction we are providing (cloning from open source) for thisproject. 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 theSpark 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 functionallanguages. It takes a pair of input parameters, req and res, and returns a string (“Hello World”). The types ofreq, res, and the function return value were defined by the get() function itself: namely, they are of typeRoute which takes a Request and a Response, returns a String, and optionally throws a HaltException ifthe 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 dosome 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 simpleroute does not have any patterns or variables, but more complex routes can make define parameters in the path or asan HTTP query string.The Route handler is the trivial “Hello World” lambda function here. In Spark Framework, the handler (1) writesresults to a String return result, (2) has the ability to modify other aspects of the HTTP response by modifying theResponse 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 barget(/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 FrameworkOur 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, yourcode 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 andconfigure a WebService containing an HttpServer listening on a port) and a ServiceFactory (which creates objectsconforming to standard interfaces), the WebServer instantiates an HttpServer with an internal thread-safeHttpTaskQueue and a series of HttpWorkers. Each worker, as it receives an HttpTask from the queue, uses theHttpIoHandler to parse the data on the socket, create a Request, call an HttpRequestHandler to handle therequest, 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 toinvoke custom code.2. Developing and running your codeNow 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 featuresyou 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 Importingfrom 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 thesteps the server will need to perform to handle a request. If you still have questions, have a look at some ofthe 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 wehave 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? Whichdata 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 theassignment) and any code we provide. Yes, there are higher-level libraries that do some of the corefunctionality we want -- but the goal is to learn how these work!2.1 Getting the Homework Source CodeWe recommend that you continue using Eclipse Che and Bitbucket as with HW0. As with HW0, you should firstfork 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, Addor Import Git repository, URL to your private repository). The process is essentially the same as for HW0 (if yougave 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. Forinstance, 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, usemvn exec:java -f ${current.project.path} -Dexec.args=8080 ${current.project.path}/wwwWhen 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 ./wwwTo ensure proper grading, your submission must meet the requirements specified in Sections 3.3 and 4 below - inparticular, it must build and run correctly in Eclipse Che and have a mvn script that correctly builds and runs thecode. 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 tocommon problems.2.1. Logging, testing, and mock objectsIn this homework, we make use of three standard techniques and tools that are extremely valuable when trying todevelop high-quality code (and save both sleep and sanity). Note that tests should help you flesh out, develop, andvalidate your code and you should be writing tests even as you write your code. (There are some who believe youwrite 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 goingon in your program. Logging tools, like Apache Log4J, are a generalization of this idea and allow muchfiner-grained control of what messages you see.2. Modular interfaces and unit tests. You’ve probably used JUnit tests before, to validate functionality inyour code. For something like a Web server, unit-testing sub-functions (e.g., the HTTP request parser, sleepand wake-up on the request queue, setting the response variables correctly, setting cookies) is extremelyhelpful 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 letsyou 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 Log4JLet’s first talk about the logging system. Apache Log4J (and other implementations of logging infrastructure) allowyour 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 wouldalways write your data to a log file.2. The logging infrastructure records what code wrote the log message, in addition to the message itself. Thatcan be incredibly helpful in debugging!3. The logging infrastructure lets you specify different levels of messages: typically low-level debugmessages (DEBUG), informational messages (INFO), warnings (WARN), and full errors (ERROR). You canset the level of messages you want to see, e.g., to disable debug messages when you think the program ismostly running OK.4. The logging infrastructure lets you turn on and off the sources of messages: you may only want to look atlogging 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 describingwhere 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 inedu.upenn.cis.cis455.WebServer or in edu.upenn.cis.cis455.m1.server.TestSendException a call toConfigurator.setLevel that records DEBUG (and above) messages for everything in theedu.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. Ifyou look at edu.upenn.cis.cis455.HttpParsing you’ll see at the top how we create the logger, ultimatelyparameterizing it with the class. Be sure to pass in the correct class to the getLogger() function, as this is used tohelp 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 objectsAs 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 doyour design well (or iterate until you get it right), you’ll likely have some logical ways of testing the functionality in aself-contained way. It’s time to develop a set of unit tests in JUnit. If you put them anywhere under thesrc/test/java directory in your project, Maven will automatically run the tests when you build the program -- thusmaking 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 totest that the HttpIoHandler.sendException function correctly sends a 404 HTTP error when called. You might wantto do something similar to test that you can parse requests (and send errors when the parsing fails), to test that acorrect response CIS 455/555作业代做、Web Systems作业代写、代做Java/Python编程语言作业 帮做C/C++编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 requestSocket, using a library called Guava, and making use of ByteArray streams to emulate the input and output to thesocket. 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 returnedon a call to s.getInputStream() -- instead this returns the ByteArrayInputStream bis. Similarly, s.getOutputStream() returns theByteOutputStream byteArrayOutputStream. Similarly, we create a default InetSocketAddress for a call tos.getRemoteSocketAddress(). When we pass this object s to our code-to-be-tested, it can’t tell this isn’t a “real” socket soit will read/write as appropriate. When we test sendException() we expect it to not actually read from the inputstream, but to write to the output stream. Now we can read the output stream from the byte array “underlying” theByteArrayOutputStream, 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 aninput to the mock socket here -- which isn’t useful for testing the sendException() function but might be useful inyour own code.2.2. “Integration testing” your serverIf you did a good job with your unit tests, a lot of the basic functionality will work in the server. However, youultimately need to “integration testing” as a whole server. Please take a look at the debugging/testing tips we havementioned 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 analternative -- the server will sit at localhost:34xxx or similar, as described by the “preview” link when you runthe server. This is because Docker maps port 8080 inside the container to some external address. From withinyour container -- i.e., from within your Java program or anything running in the Che Terminal -- the serverwill be on port 8080.The expected “main program” for your Web server should be WebServer. If you configure Maven and Che to runWebServer and create a “preview”, similar to per HW0 Section 2.4, you will get a link to the Web server that you canclick 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 theNetwork tab, which will list all the HTTP requests processed by Chrome (click on a request for extradetails).● If you want to check whether you are using the correct headers, you may find the site websniffer.netuseful.● From the Terminal, you can use the telnet command to directly interact with the server. Run:sudo apt-get updatesudo apt-get install telnettelnet localhost 8080Then type in a GET request, and hit Enter twice; you should see the servers response.More advanced tools, as you have a “mature” server and want to see how well it does, can be run from the CheTerminal:● You may also want to consider using the curl command-line utility to do some automated testing of yourserver. curl makes it easy to test HTTP/1.1 compliance by sending HTTP requests that are purposefullyinvalid - 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 findconcurrency problems, deadlocks, etc.We suggest that you use multiple options for testing; if you only use Firefox, for instance, there is a risk thatyou hard-code assumptions about Firefox, so your solution wont work with curl or ab. You may also want tocompare your servers behavior (NOT performance!) with that of a known-good server, e.g., the CIS webserver. Please do test your solution carefully! Also, please do NOT run Apachebench across major Web sites, as youwill likely make them angry for doing what looks like a DoS attach on them!3. Milestone 1: Multithreaded HTTP ServerFor the first milestone, your task is relatively simple. You will develop a Web server that can beinvoked 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, soyour 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, whichmay have completely different directories!) By default we have included a subdirectory called www, but youshouldn’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.jaredu.upenn.cis.cis455.WebServer 8080 /home/cis455/myweb this should run your server on port 8080. By default, if noparameters are given, you should assume that your server uses port 8080, and that the root directory for staticWeb pages is “./www”.In its initial version, your program will accept incoming GET and HEAD requests from a Web browser, and it willmake use of a thread pool (as discussed in class) to invoke a worker thread to process each request. When an HTTPrequest 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 wasrequested (relative to the root directory specified above) and return the file. If a directory was requested, therequest 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 streamform -- not with println or equivalent -- otherwise the browser will not be able to read them. If a GET orHEAD 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 orpaths outside the static file root directory. We will validate, e.g., that we cant get hold of /etc/passwd!3.1. Expected level of HTTP protocol supportYour application server must be HTTP 1.1 compliant, and it must support all the features described in HTTP MadeReally Easy. However:● Persistent connections are suggested but not required for HTTP 1.1 servers. If you do not wish to supportpersistent 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 connectionsand 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 (thereal HTTP specification; http://www.ietf.org/rfc/rfc2616.txt) for protocol details. If you have aprotocol-related question, please make an effort to find the answer in the spec before you post the question toPiazza!3.2. Special URLsYour application server should implement two special URLs. If someone issues a GET /shutdown, your servershould 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 thethreads in the thread pool, b) the status of each thread (waiting or the URL it is currently handling), and c) a buttonthat shuts down the server, i.e., is linked to the special /shutdown URL. It must be possible to open the specialURLs in a normal web browser.3.3 Core RequirementsFor efficiency, your application server must be implemented using a thread pool that you implement, as discussed inclass. Specifically, there should be one thread that listens for incoming TCP requests and enqueues them, and somenumber of threads that process the requests from the queue and return the responses. We will examine your code tomake sure it is free of race conditions and the potential for deadlock, so code carefully!3.3.1. Standard APIYou must also implement the standard interface, which we’ll be using to test. Refer to the Figure in Section 1.2 forthe overall “flow.”● The controller: WebServiceController. Just as developers statically import spark.Spark.* in the SparkFramework and use it to configure routes and server behavior, developers using your Web server frameworkwill statically import edu.upenn.cis.cis455.WebServiceController. This class is used by the developer’smain 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 actualcode that registers the route-handling and configuration calls such as get(), post(), etc., and it sets up theHttpServer 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 appearsin m2.server, e.g., handling cookies, form parameters, etc. You’ll need to develop your ownimplementations. For Milestone 1 you can extend the m1.server implementations; later you’ll change yourcode to extend m2.server.● The ServiceFactory is used to create the appropriate objects to match the above interfaces. From here, youshould be able to instantiate instances of the HTTP server, a request object, and a response object. Youshould assume that, among other things, our test drivers will use the ServiceFactory to instantiate testobjects!objects!Next, we have two “helper” interfaces you should use but may expand (superset) while preserving existing functionsignatures:● ThreadManager -- this is intended to let workers coordinate with the central Web service control. Theycan update status, check whether they should terminate, etc. You would need to implement a subclass of thisto handle your thread management.● HttpRequestHandler -- this is a Java 8 functional interface for taking a (Request, Response) pair andreturning a result. It is most useful in Milestone 2, but for now it has a subclass calledMockRequestHandler that takes a request and sends a mock result.3.3.2. Provided Skeleton Code Whose API You Don’t Need to MaintainWe’ve also given you a bunch of starter code to help establish the main components of the project. These are all foryour own internal use, as opposed to public APIs, so you may change the function interfaces. Note that some of oursample 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 createand 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 (usingHttpIoHandler), and sends responses (or exceptions).● HttpTaskQueue -- this is intended to be the concurrency-protected queue between the server and theworkers.● HttpIoHandler -- this processes incoming requests on the socket, and sends outgoing responses. For nowthere is a (completely stubbed!) sendException() method.We also provide some helper methods for parsing HTTP requests, in the util package.4. Milestone 2: Microservices FrameworkThe 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 Milestonebuilt 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 theWebServiceController. ○ 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 thedescription in the Spark Framework documentation). The handler takes anHttpRequestHandler (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 certainrequests from being handled (by throwing a HaltException, which is caught before the call to theroute 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 activesession) before the route handler is called. See, for an example, the before() filter in this SparkFramework 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 andquery 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 anappropriate 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 301or 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 acookie.● 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 levelto track user requests for URLs.To help you get started (and to form a reference specification), we have given you some additional, more fleshed-outclasses, e.g., for requests, responses, and servers in the ...m2.server “pa转自:http://ass.3daixie.com/2019020429660311.html