简易版Web服务器
项目构建
依赖引入
构建Maven项目,引入Junit
和javax.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()); //打印接收到的响应
}
}
Comments NOTHING