《How Tomcat Works》学习(一)——简易web服务器的实现
前言
《How Tomcat Works》是一本老书。现在Java的同学,大概率对servlet都是随便略过即可,因为现在springmvc、springboot之类的,要上手根本不需要搞那么复杂。我之所以淘来这本书学习,一是因为我读大学的时候,javaweb还是要玩servlet那一套的,有情节在那里,二是因为想深入了解springmvc的源码,所以对servlet这块也要有一定理解才行。这个系列准备按照书中的章节,一边敲代码一边总结,一步一步为自己实现的web服务器添砖加瓦。
构建简单的web服务器
一个简单的web服务器,应该是三部分组成,主程序、接收请求的处理、返回结果的处理这三个模块。下面逐个实现
主程序
package ex01;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpServer {
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
private boolean shutdown = false;
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
try {
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
Request request = new Request(input);
request.parse();
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
socket.close();
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
25-32行是创建socket服务端的标准代码。43行的Request类作为处理请求的类,44行解析请求。46行的Response为处理返回类,48行进行返回信息的处理。主程序的逻辑还是挺简单的。
Request类
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
public void parse() {
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[2048];
try {
i = input.read(buffer);
}
catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j=0; j<i; j++) {
request.append((char) buffer[j]);
}
System.out.print(request.toString());
uri = parseUri(request.toString());
}
private String parseUri(String requestString) {
int index1, index2;
index1 = requestString.indexOf(' ');
if (index1 != -1) {
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
return requestString.substring(index1 + 1, index2);
}
return null;
}
public String getUri() {
return uri;
}
}
第10行parse函数为处理请求类的主处理函数,14-23行主要是把请求的信息读到缓冲区buffer中,然后添加request里面。25行调用parseUri函数,这个函数的作用是提取请求信息中,请求的资源是什么,我们看看24行的控制台输出,请求的信息是什么:
GET /hello.html HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: username-localhost-8888="2|1:0|10:1606012683|23:username-localhost-8888|44:NGIwMmM2YzA5NGMzNDEwMWJjNjUyZDU3NTUwZmRiZGI=|61284d724e007155013c2d8a67d0d4810a8d558d1c5677f9cf0c9938b3802053"; _xsrf=2|e05db3f6|554bdc3b2832c9c829726f118623686e|1606013132
看第一行,parseUri函数提取的就是/hello.html这个。根据这个结果理解parseUri函数的代码就很容易理解了。
Response类
public class Response {
private static final int BUFFER_SIZE = 1024;
private Request request;
private OutputStream output;
public Response(OutputStream output) {
this.output = output;
}
public void setRequest(Request request) {
this.request = request;
}
public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, BUFFER_SIZE);
output.write(new String("HTTP/1.1 200 OK\r\n\r\n").getBytes());
while (ch != -1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
} else {
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n"
+ "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
}
} catch (Exception e) {
System.out.println(e.toString());
} finally {
if (fis != null)
fis.close();
}
}
}
19行根据WEB_ROOT的目录和之前request解析的uri信息,组合起来,就是需要获取的文件路径。21-27行读取文件并写到输出流给客户端,在写文件的开头,还要加上"HTTP/1.1 200 OK\r\n\r\n",作为response的返回头,这样浏览器才能识别服务器返回请求成功的信息。
页面资源
我们要为服务器准备一个页面用来测试,这里我们在项目的根目录下创建webroot目录,在webroot目录下创建一个html文件hello.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
接下来我们进行测试,首先启动web服务器,然后在浏览器输入localhost:8080/hello.html,然后得到输出:
我们成功访问到对应的html页面。
小结
通过简单的代码,我们已经实现了web服务器。但是这个服务器只能根据我们输入的路径获取路径下对应的文件并返回其内容。而使用tomcat服务器时我们通常是调用servlet,服务器会调用对应的处理类,不同的处理类会根据情况返回不同的内容,不是单纯返回一个页面或者一个资源这么简单。在后面的章节中,会实现一个简单的servlet。
本文地址:https://blog.csdn.net/sadoshi/article/details/110680119