06-简易版Web服务器

nobility 发布于 2021-08-11 3044 次阅读


简易版Web服务器

项目构建

依赖引入

构建Maven项目,引入Junitjavax.servlet-api包,因为要进行单元测试和servlet的开发

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
  </dependency>
</dependencies>

目录说明

Web服务器入口

创建Connector对象进行,并进行服务器的启动

import connector.Connector;

public class Main {
  public static void main(String[] args) {
    Connector connector = new Connector();  //创建连接器
    connector.start();  //启动连接器
  }
}
服务器源文件目录
│  Main.java
├─connector
│  │  Connector.java
│  │  ConnectorUtils.java
│  │  HttpStatus.java
│  │  Request.java
│  │  Response.java
└─processor
        ServletProcessor.java
        StaticProcessor.java
HttpStatus

枚举类,用于规定Http的响应状态

package connector;

public enum HttpStatus {
  STATUS_CODE_OK(200, "OK"),
  STATUS_CODE_FOUND(404, "Not Found");
  private int statusCode;
  private String reason;

  HttpStatus(int statusCode, String reason) {
    this.statusCode = statusCode;
    this.reason = reason;
  }

  public int getStatusCode() {
    return statusCode;
  }

  public String getReason() {
    return reason;
  }
}
ConnectorUtils

利用枚举类,拼接Http响应头和定义web资源目录

package connector;

import java.io.File;

public class ConnectorUtils {
  public static final String WEB_ROOT = System.getProperty("user.dir")  //启动程序用户所在目录
      + File.separator + "target"
      + File.separator + "classes"
      + File.separator + "web";
  public static final String PROTOCOL = "HTTP/1.1";
  public static final String CARRIAGE = "\r";
  public static final String NEWLINE = "\n";
  public static final String SPACE = " ";

  public static String renderStatus(HttpStatus status) {  //根据HttpStatus构建HTTP响应头
    StringBuilder stringBuilder = new StringBuilder(PROTOCOL)
        .append(SPACE)
        .append(status.getStatusCode())
        .append(SPACE)
        .append(status.getReason())
        .append(CARRIAGE).append(NEWLINE)
        .append(CARRIAGE).append(NEWLINE);
    return stringBuilder.toString();
  }
}
资源文件目录

由于是在资源目录中写了TimeServlet.java文件,在进行测试时可能会加载不到,经过测试需要手动运行用到该类的测试用例,之后再使用Maven的test批量测试时就没问题了

│
└─web
    │  404.html
    │  index.html
    ├─images
    │      img.png
    └─servlet
            TimeServlet.java
index.html

其中引用了images下的一张图片

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>index.html</h1>
<img src="/images/img.png" alt="img">
</body>
</html>
404.html

当找不到资源后返回该页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <h1>404 Not Found</h1>
</body>
</html>
TimeServlet

向客户端返回当前时间的Servlet

package servlet;

import connector.ConnectorUtils;
import connector.HttpStatus;

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

//接口中其他空方法的实现省略
public class TimeServlet implements Servlet {
  @Override
  public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    PrintWriter out = servletResponse.getWriter();  //获取响应对象的打印流
    out.println(ConnectorUtils.renderStatus(HttpStatus.STATUS_CODE_OK));  //返回响应头
    out.println(new Date());  //返回当前时间
  }
}

测试文件目录
│  TestClient.java
├─connector
│      RequestTest.java
│      ResponseTest.java
├─processor
│      ServletProcessorTest.java
└─utils
        TestUtils.java
TestUtils

用于辅助测试类进行测试

package utils;

import connector.Request;
import connector.Response;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class TestUtils {
  public static Request createRequest(String requestString) {
    InputStream inputStream = new ByteArrayInputStream(requestString.getBytes());  //使用字节数组输入流来模拟
    Request request = new Request(inputStream);  //创建请求对象
    request.parse();  //解析uri
    return request;
  }

  public static String getResponseString(Request request) throws IOException {
    OutputStream outputStream = new ByteArrayOutputStream();  //使用字节数组输出流来模拟
    Response response = new Response(outputStream);  //创建响应对象
    response.setRequest(request);  //设置响应对象对应的请求对象
    response.sendStaticResource();  //发送静态资源
    return outputStream.toString(); //返回相应的静态资源
  }

  public static String readFileToString(String fileName) throws IOException {
    Path path = Paths.get(fileName);  //获取文件路径
    byte[] bytes = Files.readAllBytes(path);  //读取文件全部字节
    return new String(bytes);  //转换为字符串返回
  }
}

Request对象

实现代码

package connector;

import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;


public class Request extends AbstractRequest {  //AbstractRequest抽象类实现了ServletRequest,并进行了空实现
  private static final int BUFFER_SIZE = 1024;
  private InputStream inputStream;
  private String uri;

  public Request(InputStream inputStream) {  //使用输入流创建请求对象,接收请求信息
    this.inputStream = inputStream;
  }

  public String getUri() {  //获取解析后的uri
    return uri;
  }

  public void parse() {  //接收请求并进行解析
    int length;
    byte[] bytes = new byte[BUFFER_SIZE];  //用于接收请求的字节数组
    try {
      length = inputStream.read(bytes);  //接收的长度
      StringBuilder requestString = new StringBuilder();
      for (int i = 0; i < length; i++) {  //一个字节一个字节的加入到StringBuilder
        requestString.append((char) bytes[i]);
      }
      this.uri = parseUri(requestString.toString());  //将解析后的uri赋值给成员变量uri
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private String parseUri(String requestString) {
    //HTTP请求: GET /index.html HTTP/1.1
    int start, end;
    start = requestString.indexOf(' ');  //找到第一个空格位置
    if (start != -1) {
      end = requestString.indexOf(' ', start + 1);  //找到第二个空格位置
      if (end > start) {
        return requestString.substring(start + 1, end);  //截取两个空格之间的请求地址
      }
    }
    throw new IllegalArgumentException("parse uri failure");
  }
}

class AbstractRequest implements ServletRequest {
  //实现ServletRequest接口的空实现代码省略
}

测试代码

package connector;

import org.junit.Assert;
import org.junit.Test;
import utils.TestUtils;

public class RequestTest {
  public static final String validRequest = "GET /index.html HTTP/1.1";
  public static final String expectedParse = "/index.html";

  @Test
  public void parseTest() {
    Request request = TestUtils.createRequest(validRequest);  //创建request对象
    Assert.assertEquals(expectedParse, request.getUri());  //对比uri
  }
}

Response对象

实现代码

package connector;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import java.io.*;
import java.util.Locale;

public class Response extends AbstractResponse {  //AbstractRequest抽象类实现了ServletResponse,并进行了空实现
  private static final int BUFFER_SIZE = 1024;
  Request request;
  OutputStream outputStream;

  public Response(OutputStream outputStream) {  //使用输出流创建响应对象,输出响应信息
    this.outputStream = outputStream;
  }

  public void setRequest(Request request) {  //设置请求对象才能进行对应的响应
    this.request = request;
  }

  public void sendStaticResource() throws IOException {
    File file = new File(ConnectorUtils.WEB_ROOT, request.getUri());
    try {
      write(HttpStatus.STATUS_CODE_OK, file);
    } catch (IOException e) {  //若读取静态资源文件出错,说明该资源不存在
      write(HttpStatus.STATUS_CODE_FOUND, new File(ConnectorUtils.WEB_ROOT, "404.html"));
    }
  }

  private void write(HttpStatus status, File resource) throws IOException {  //根据资源文件和响应状态进行信息响应
    try (FileInputStream fileInputStream = new FileInputStream(resource)) {
      outputStream.write(ConnectorUtils.renderStatus(status).getBytes());  //设置响应头
      byte[] bytes = new byte[BUFFER_SIZE];
      int length = 0;
      while ((length = fileInputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {  //向byte数组中读取
        this.outputStream.write(bytes, 0, length);  //将静态资源文件输出到输出流中
      }
    }
  }

  @Override
  public PrintWriter getWriter() {
    return new PrintWriter(outputStream, true);  //将输出流包装成打印流
    //第二个参数表示,println()方法时间自动进行flush()操作
  }
}

class AbstractResponse implements ServletResponse {
  //实现ServletResponse接口的空实现代码省略
}

测试代码

package connector;

import org.junit.Assert;
import org.junit.Test;
import utils.TestUtils;
import java.io.IOException;


public class ResponseTest {
  public static final String validRequest = "GET /index.html HTTP/1.1";  //200请求头
  public static final String invalidRequest = "GET /notfound.html HTTP1.1";  //404请求头

  public static final String status200 = "HTTP/1.1 200 OK\r\n\r\n";  //200响应头
  public static final String status404 = "HTTP/1.1 404 Not Found\r\n\r\n";  //404响应头

  @Test
  public void bingoSendStaticResourceTest() throws IOException {
    Request request = TestUtils.createRequest(validRequest);  //创建请求对象
    String fileString = TestUtils.readFileToString(ConnectorUtils.WEB_ROOT + request.getUri());  //读取本地文件字符串内容
    String responseString = TestUtils.getResponseString(request);  //获取响应对象响应内容
    Assert.assertEquals(status200 + fileString, responseString);  //对比返回内容
  }

  @Test
  public void errorSendStaticResourceTest() throws IOException {
    Request request = TestUtils.createRequest(invalidRequest);  //创建请求对象
    String fileString = TestUtils.readFileToString(ConnectorUtils.WEB_ROOT + "/404.html");  //读取本地文件字符串内容
    String responseString = TestUtils.getResponseString(request);  //获取响应对象响应内容
    Assert.assertEquals(status404 + fileString, responseString);  //对比返回内容
  }
}

Processor

StaticProcessor

package processor;

import connector.Request;
import connector.Response;

import java.io.IOException;

public class StaticProcessor {
  public void process(Request request, Response response) {
    try {
      response.sendStaticResource();  //发送静态资源文件
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

ServletProcessor

实现代码
package processor;

import connector.ConnectorUtils;
import connector.Request;
import connector.Response;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class ServletProcessor {
  private URLClassLoader urlClassLoader;  //用于加载类的类加载器

  public ServletProcessor() throws MalformedURLException {
    File file = new File(ConnectorUtils.WEB_ROOT);  //获取webroot文件夹
    URL url = file.toURI().toURL();  //将文件路径转换为url
    this.urlClassLoader = new URLClassLoader(new URL[]{url});  //创建URLClassLoader对象
  }

  protected Servlet getServlet(Request request) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    /*要求servlet请求路径是以 *.servlet 的形式*/
    String uri = request.getUri();  //获取请求uri
    String servletName = uri.substring(1, uri.indexOf('.'))  //去掉 .servlet 后缀
        .replaceAll("/", ".");  //将路径请求的 / 转化为 .
    Class<?> servletClass = urlClassLoader.loadClass(servletName);  //反射加载该servlet
    return (Servlet) servletClass.newInstance();  //创建该servlet实例
  }

  public void process(Request request, Response response) {
    try {
      Servlet servlet = getServlet(request);
      servlet.service(request, response);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    } catch (InstantiationException e) {
      e.printStackTrace();
    } catch (ServletException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}
测试代码
package processor;

import connector.Request;
import org.junit.Assert;
import org.junit.Test;
import utils.TestUtils;

import javax.servlet.Servlet;
import java.net.MalformedURLException;

public class ServletProcessorTest {
  public static final String servletRequest = "GET /servlet/TimeServlet.servlet HTTP/1.1";  //动态资源请求头

  @Test
  public void processTest() throws MalformedURLException, IllegalAccessException, InstantiationException, ClassNotFoundException {
    Request request = TestUtils.createRequest(servletRequest);  //创建请求对象
    ServletProcessor servletProcessor = new ServletProcessor();  //创建ServletProcessor实例
    Servlet servlet = servletProcessor.getServlet(request);  //根据请求对象获取Servlet实例
    Assert.assertEquals("servlet.TimeServlet", servlet.getClass().getName());  //对比类路径
  }
}

Connector

实现代码

以下可使用BIO、NIO、AIO进行改写,这里仅仅使用了简单的单线程排队的服务器

package connector;

import processor.ServletProcessor;
import processor.StaticProcessor;

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

public class Connector implements Runnable {
  public static final int PORT = 8080;
  StaticProcessor staticProcessor;
  ServletProcessor servletProcessor;

  {
    try {
      this.staticProcessor = new StaticProcessor();
      this.servletProcessor = new ServletProcessor();
    } catch (MalformedURLException e) {
      e.printStackTrace();
    }
  }

  public void start() {
    new Thread(this).start();  //在新线程中启动服务
  }

  @Override
  public void run() {
    try (ServerSocket serverSocket = new ServerSocket(PORT);) {
      while (true) {
        Socket socket = serverSocket.accept();
        InputStream inputStream = socket.getInputStream();
        OutputStream outputStream = socket.getOutputStream();
        Request request = new Request(inputStream);
        request.parse();  //解析uri
        Response response = new Response(outputStream);
        response.setRequest(request);  //设置响应对象对应的请求对象
        if (request.getUri().endsWith(".servlet")) {  //若是请求servlet
          servletProcessor.process(request, response);  //交由servletProcessor处理请求和响应对象
        } else {  //若是请求静态资源
          staticProcessor.process(request, response);  //交由staticProcessor处理请求和响应对象
        }
        socket.close();  //关闭socket
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

测试客户端

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

public class TestClient {
  public static final String Host = "localhost";
  public static final int Port = 8080;
  private static final int BUFFER_SIZE = 1024;

  public static final String HttpHeader = "GET /index.html HTTP/1.1";  //请求静态资源的请求头
  //public static final String HttpHeader = "GET /servlet/TimeServlet.servlet HTTP/1.1";  //请求动态资源的请求头

  public static void main(String[] args) throws IOException {
    Socket socket = new Socket(Host, Port);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write(HttpHeader.getBytes());  //发送请求
    socket.shutdownOutput();  //发完请求关闭输出流
    InputStream inputStream = socket.getInputStream();
    byte[] bytes = new byte[BUFFER_SIZE];
    int length;
    StringBuilder stringBuilder = new StringBuilder();
    while ((length = inputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {  //向byte数组中读取
      for (int i = 0; i < length; i++) {  //一个字节一个字节的加入到StringBuilder
        stringBuilder.append((char) bytes[i]);
      }
    }
    socket.shutdownInput();  //接收完响应就关闭输入流
    System.out.println(stringBuilder.toString());  //打印接收到的响应
  }
}
此作者没有提供个人介绍
最后更新于 2021-08-11