名称:Minicat

Minicat要做的事情:作为⼀个服务器软件提供服务的,也即我们可以通过浏览器客户端发送http请求,
Minicat可以接收到请求进⾏处理,处理之后的结果可以返回浏览器客户端。
1)提供服务,接收请求(Socket通信)
2)请求信息封装成Request对象(Response对象)
3)客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
4)资源返回给客户端浏览器
我们递进式完成以上需求,提出V1.0、V2.0、V3.0版本的需求
V1.0需求:浏览器请求http://localhost:8080,返回⼀个固定的字符串到⻚⾯"Hello Minicat!”
V2.0需求:封装Request和Response对象,返回html静态资源⽂件
V3.0需求:可以请求动态资源(Servlet)
完成上述三个版本后,我们的代码如下:

V1.0实现:

初步实现

  1. 开启socket
  2. 获取输出流,输出指定字符串。
  3. 测试http://localhost:8080 ```java package server;

import java.io.IOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket;

/**

  • MiniCat 主类 */ public class Bootstrap { //定义socket定义的端口号 private int port=8080;

    public int getPort() {

    1. return port;

    }

    public void setPort(int port) {

    1. this.port = port;

    }

    /**

    • MiniCat 启动需要初始化 展开的操作
    • V1.0需求:浏览器请求http://localhost:8080,返回⼀个固定的字符串到⻚⾯"Hello Minicat!”
    • V2.0需求:封装Request和Response对象,返回html静态资源⽂件
    • V3.0需求:可以请求动态资源(Servlet) */ public void start() throws IOException { //miniCat 1.0 版本 浏览器访问(http://localhost:8080)返回固定字符串到页面 ServerSocket serverSocket =new ServerSocket(port); System.out.println(“MiniCat start on port:”+port); while (true){
      1. final Socket socket = serverSocket.accept();
      2. final OutputStream outputStream = socket.getOutputStream();
      3. outputStream.write("hello MiniCat!".getBytes());
      4. socket.close();
      } } public static void main(String[] args) { Bootstrap bootstrap = new Bootstrap(); try {
      1. bootstrap.start();
      } catch (IOException e) {
      1. e.printStackTrace();
      } } }
  1. 测试结果<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1699668/1595232724637-7b74b5ae-37fe-4a8b-90c6-af5e2c3e22de.png#align=left&display=inline&height=467&margin=%5Bobject%20Object%5D&name=image.png&originHeight=467&originWidth=689&size=19550&status=done&style=none&width=689)<br />**发现没有请求头和响应头,像这样**<br />**![image.png](https://cdn.nlark.com/yuque/0/2020/png/1699668/1595232861376-484bcb6a-828b-4481-93a4-966ae864781c.png#align=left&display=inline&height=389&margin=%5Bobject%20Object%5D&name=image.png&originHeight=391&originWidth=749&size=28669&status=done&style=none&width=746)**
  2. <a name="nqmpO"></a>
  3. ### 增加响应头
  4. **必要字段有:**
  5. - **HTTP/1.1 200 OK**
  6. - **Content-Type: text/html**
  7. - **Content-Length**
  8. **代码如下:**
  9. ```java
  10. /**
  11. * http协议工具类,主要提供响应头信息,这里我们只提供200和404
  12. */
  13. public class HttpProtocolUtil {
  14. /**
  15. * 为响应码200提供请求头信息
  16. * @return
  17. */
  18. public static String getHttpHeader200(long contentLength) {
  19. return "HTTP/1.1 200 OK \n" +
  20. "Content-Type: text/html \n" +
  21. "Content-Length: " + contentLength + " \n" +
  22. "\r\n";
  23. }
  24. /**
  25. * 为响应码404提供请求头信息(此处也包含了数据内容)
  26. * @return
  27. */
  28. public static String getHttpHeader404() {
  29. String str404 = "<h1>404 not found</h1>";
  30. return "HTTP/1.1 404 NOT Found \n" +
  31. "Content-Type: text/html \n" +
  32. "Content-Length: " + str404.getBytes().length + " \n" +
  33. "\r\n" + str404;
  34. }
  35. }

在启动类中增加响应头信息。变更start方法如下:
增加获取http响应头信息,并将length传入,加上data用输出流进行输出。

  1. /**
  2. * MiniCat 启动需要初始化 展开的操作
  3. * V1.0需求:浏览器请求http://localhost:8080,返回⼀个固定的字符串到⻚⾯"Hello Minicat!"
  4. * V2.0需求:封装Request和Response对象,返回html静态资源⽂件
  5. * V3.0需求:可以请求动态资源(Servlet)
  6. */
  7. public void start() throws IOException {
  8. //miniCat 1.0 版本 浏览器访问(http://localhost:8080)返回固定字符串到页面
  9. ServerSocket serverSocket =new ServerSocket(port);
  10. System.out.println("MiniCat start on port:"+port);
  11. while (true){
  12. final Socket socket = serverSocket.accept();
  13. final OutputStream outputStream = socket.getOutputStream();
  14. String data="hello MiniCat!";
  15. String responseText= HttpProtocolUtil.getHttpHeader200(data.getBytes().length)+data;
  16. outputStream.write(responseText.getBytes());
  17. socket.close();
  18. }
  19. }

V2.0实现:

获取请求信息

  1. while (true){
  2. final Socket socket = serverSocket.accept();
  3. //从输入流中获取请求信息
  4. final InputStream inputStream = socket.getInputStream();
  5. //针对网络情况等于0的时候
  6. int count=0;
  7. while (count==0){
  8. //同一批数据有可能会过不来 获取输入流中的长度
  9. count = inputStream.available();
  10. }
  11. byte[] bytes=new byte[count];
  12. inputStream.read(bytes);
  13. System.out.println("====>请求信息"+new String(bytes));
  14. }

获取如下:
image.png

封装request:

request封装如下:

  1. /**
  2. * 把请求信息封装为Request对象(根据InputSteam输入流封装)
  3. */
  4. public class Request {
  5. private String method; // 请求方式,比如GET/POST
  6. private String url; // 例如 /,/index.html
  7. private InputStream inputStream; // 输入流,其他属性从输入流中解析出来
  8. public String getMethod() {
  9. return method;
  10. }
  11. public void setMethod(String method) {
  12. this.method = method;
  13. }
  14. public String getUrl() {
  15. return url;
  16. }
  17. public void setUrl(String url) {
  18. this.url = url;
  19. }
  20. public InputStream getInputStream() {
  21. return inputStream;
  22. }
  23. public void setInputStream(InputStream inputStream) {
  24. this.inputStream = inputStream;
  25. }
  26. public Request() {
  27. }
  28. // 构造器,输入流传入
  29. public Request(InputStream inputStream) throws IOException {
  30. this.inputStream = inputStream;
  31. // 从输入流中获取请求信息
  32. int count = 0;
  33. while (count == 0) {
  34. count = inputStream.available();
  35. }
  36. byte[] bytes = new byte[count];
  37. inputStream.read(bytes);
  38. String inputStr = new String(bytes);
  39. // 获取第一行请求头信息
  40. String firstLineStr = inputStr.split("\\n")[0]; // GET / HTTP/1.1
  41. String[] strings = firstLineStr.split(" ");
  42. this.method = strings[0];
  43. this.url = strings[1];
  44. System.out.println("=====>>method:" + method);
  45. System.out.println("=====>>url:" + url);
  46. }
  47. }

封装response:

  1. package server;
  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.IOException;
  5. import java.io.OutputStream;
  6. /**
  7. * 封装Response对象,需要依赖于OutputStream
  8. *
  9. * 该对象需要提供核心方法,输出html
  10. */
  11. public class Response {
  12. private OutputStream outputStream;
  13. public Response() {
  14. }
  15. public Response(OutputStream outputStream) {
  16. this.outputStream = outputStream;
  17. }
  18. // 使用输出流输出指定字符串
  19. public void output(String content) throws IOException {
  20. outputStream.write(content.getBytes());
  21. }
  22. /**
  23. *
  24. * @param path url,随后要根据url来获取到静态资源的绝对路径,进一步根据绝对路径读取该静态资源文件,最终通过
  25. * 输出流输出
  26. * /-----> classes
  27. */
  28. public void outputHtml(String path) throws IOException {
  29. // 获取静态资源文件的绝对路径
  30. String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path);
  31. // 输入静态资源文件
  32. File file = new File(absoluteResourcePath);
  33. if(file.exists() && file.isFile()) {
  34. // 读取静态资源文件,输出静态资源
  35. StaticResourceUtil.outputStaticResource(new FileInputStream(file),outputStream);
  36. }else{
  37. // 输出404
  38. output(HttpProtocolUtil.getHttpHeader404());
  39. }
  40. }
  41. }

编写静态资源工具类:

  1. package server;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.OutputStream;
  5. public class StaticResourceUtil {
  6. /**
  7. * 获取静态资源文件的绝对路径
  8. * @param path
  9. * @return
  10. */
  11. public static String getAbsolutePath(String path) {
  12. String absolutePath = StaticResourceUtil.class.getResource("/").getPath();
  13. return absolutePath.replaceAll("\\\\","/") + path;
  14. }
  15. /**
  16. * 读取静态资源文件输入流,通过输出流输出
  17. */
  18. public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException {
  19. int count = 0;
  20. while(count == 0) {
  21. count = inputStream.available();
  22. }
  23. int resourceSize = count;
  24. // 输出http请求头,然后再输出具体内容
  25. outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes());
  26. // 读取内容输出
  27. long written = 0 ;// 已经读取的内容长度
  28. int byteSize = 1024; // 计划每次缓冲的长度
  29. byte[] bytes = new byte[byteSize];
  30. while(written < resourceSize) {
  31. if(written + byteSize > resourceSize) { // 说明剩余未读取大小不足一个1024长度,那就按真实长度处理
  32. byteSize = (int) (resourceSize - written); // 剩余的文件内容长度
  33. bytes = new byte[byteSize];
  34. }
  35. inputStream.read(bytes);
  36. outputStream.write(bytes);
  37. outputStream.flush();
  38. written+=byteSize;
  39. }
  40. }
  41. }
  1. 获取socket输入流,作为获取请求头信息。
  2. 获取socket输出流,作为响应信息。
  3. 分别进行封装。
  4. StaticResourceUtil 根据url进行读取文件,缓冲式输出,一次输出1024.
  5. 最终调用请求封装成request,响应封装成response。传入socket输入及输出流。具体代码如下:

    1. while(true) {
    2. Socket socket = serverSocket.accept();
    3. InputStream inputStream = socket.getInputStream();
    4. // 封装Request对象和Response对象
    5. Request request = new Request(inputStream);
    6. Response response = new Response(socket.getOutputStream());
    7. response.outputHtml(request.getUrl());
    8. socket.close();
    9. }

V3.0实现:

  1. 启动先加载配置文件到servlet
  2. 初始化加载servlet,解析web.xml
  3. 找到对应封装好的 request,response。

定义servlet

  • 编写servlet接口: ```java public interface Servlet {

    void init() throws Exception;

    void destory() throws Exception;

    void service(Request request, Response response) throws Exception; }

  1. - 编写HttpServlet
  2. ```java
  3. public abstract class HttpServlet implements Servlet{
  4. public abstract void doGet(Request request, Response response);
  5. public abstract void doPost(Request request,Response response);
  6. @Override
  7. public void service(Request request, Response response) throws Exception {
  8. if("GET".equalsIgnoreCase(request.getMethod())) {
  9. doGet(request,response);
  10. }else{
  11. doPost(request,response);
  12. }
  13. }
  14. }
  • 自定义servlet

    1. public class LagouServlet extends HttpServlet {
    2. @Override
    3. public void doGet(Request request, Response response) {
    4. String content = "<h1>LagouServlet get</h1>";
    5. try {
    6. response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
    7. } catch (IOException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. @Override
    12. public void doPost(Request request, Response response) {
    13. String content = "<h1>LagouServlet post</h1>";
    14. try {
    15. response.output((HttpProtocolUtil.getHttpHeader200(content.getBytes().length) + content));
    16. } catch (IOException e) {
    17. e.printStackTrace();
    18. }
    19. }
    20. @Override
    21. public void init() throws Exception {
    22. }
    23. @Override
    24. public void destory() throws Exception {
    25. }
    26. }

    自定义web.xml

    ```xml <?xml version=”1.0” encoding=”UTF-8” ?>

    lagou servlet.LagouServlet
  1. <servlet-mapping>
  2. <servlet-name>lagou</servlet-name>
  3. <url-pattern>/lagou</url-pattern>
  4. </servlet-mapping>

  1. 目的 输入http://localhost:8080/lagou就可以到LagouServlet
  2. <a name="nCVQi"></a>
  3. #### 更改bootstrap
  4. ```java
  5. loadServlet();
  6. while(true) {
  7. Socket socket = serverSocket.accept();
  8. InputStream inputStream = socket.getInputStream();
  9. // 封装Request对象和Response对象
  10. Request request = new Request(inputStream);
  11. Response response = new Response(socket.getOutputStream());
  12. // 静态资源处理
  13. if(servletMap.get(request.getUrl()) == null) {
  14. response.outputHtml(request.getUrl());
  15. }else{
  16. // 动态资源servlet请求
  17. HttpServlet httpServlet = servletMap.get(request.getUrl());
  18. httpServlet.service(request,response);
  19. }
  20. socket.close();
  21. }

引入需要的pom

dom4j,jaxen

  1. <dependency>
  2. <groupId>dom4j</groupId>
  3. <artifactId>dom4j</artifactId>
  4. <version>1.6.1</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>jaxen</groupId>
  8. <artifactId>jaxen</artifactId>
  9. <version>1.1.6</version>
  10. </dependency>

解析web.xml

  1. 读取文件成document对象
  2. 取到servlet找到servlet-name,对应的class
  3. 找到servlet-name对应的servlet-mapping
  4. 缓存url和servlet ```java private Map servletMap = new HashMap(); /**

    • 加载解析web.xml,初始化Servlet */ private void loadServlet() { InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(“web.xml”); SAXReader saxReader = new SAXReader();

      try {

      1. Document document = saxReader.read(resourceAsStream);
      2. Element rootElement = document.getRootElement();
      3. List<Element> selectNodes = rootElement.selectNodes("//servlet");
      4. for (int i = 0; i < selectNodes.size(); i++) {
      5. Element element = selectNodes.get(i);
      6. // <servlet-name>lagou</servlet-name>
      7. Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
      8. String servletName = servletnameElement.getStringValue();
      9. // <servlet-class>server.LagouServlet</servlet-class>
      10. Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
      11. String servletClass = servletclassElement.getStringValue();
  1. // 根据servlet-name的值找到url-pattern
  2. Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
  3. // /lagou
  4. String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
  5. servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
  6. }
  7. } catch (DocumentException e) {
  8. e.printStackTrace();
  9. } catch (IllegalAccessException e) {
  10. e.printStackTrace();
  11. } catch (InstantiationException e) {
  12. e.printStackTrace();
  13. } catch (ClassNotFoundException e) {
  14. e.printStackTrace();
  15. }
  16. }
  1. <a name="QsgE3"></a>
  2. ## 多线程改造:
  3. 当前问题,现在属于单线程,只能单线程的去处理。<br />所以要改造成多线程方式。
  4. <a name="v3SZ9"></a>
  5. ### 定义requestProcess
  6. ```java
  7. public class RequestProcessor extends Thread {
  8. private Socket socket;
  9. private Map<String, HttpServlet> servletMap;
  10. public RequestProcessor(Socket socket, Map<String, HttpServlet> servletMap) {
  11. this.socket = socket;
  12. this.servletMap = servletMap;
  13. }
  14. @Override
  15. public void run() {
  16. try{
  17. InputStream inputStream = socket.getInputStream();
  18. // 封装Request对象和Response对象
  19. Request request = new Request(inputStream);
  20. Response response = new Response(socket.getOutputStream());
  21. // 静态资源处理
  22. if(servletMap.get(request.getUrl()) == null) {
  23. response.outputHtml(request.getUrl());
  24. }else{
  25. // 动态资源servlet请求
  26. HttpServlet httpServlet = servletMap.get(request.getUrl());
  27. httpServlet.service(request,response);
  28. }
  29. socket.close();
  30. }catch (Exception e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }

不使用线程池bootstarp

  1. while(true) {
  2. Socket socket = serverSocket.accept();
  3. RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap);
  4. requestProcessor.start();
  5. }

使用线程池

定义线程池

  1. // 定义一个线程池
  2. int corePoolSize = 10;//基本大小核心线程数
  3. int maximumPoolSize =50;//最大线程数
  4. long keepAliveTime = 100L;//保持时间
  5. TimeUnit unit = TimeUnit.SECONDS;//单位
  6. BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);//线程队列
  7. ThreadFactory threadFactory = Executors.defaultThreadFactory();//线程工厂
  8. RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();//拒绝策略 如何拒绝任务
  9. ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
  10. corePoolSize,
  11. maximumPoolSize,
  12. keepAliveTime,
  13. unit,
  14. workQueue,
  15. threadFactory,
  16. handler
  17. );

使用线程池改造bootstarp

  1. while(true) {
  2. Socket socket = serverSocket.accept();
  3. RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap);
  4. //requestProcessor.start();
  5. threadPoolExecutor.execute(requestProcessor);
  6. }

为什么并发达到一定数量,使用线程池效率更高一些呢?

  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 提高线程的可管理性:对线程进行统一分配和监控,避免无限制创建线程导致内存溢出或者耗尽CPU