本篇文章主要介绍 Java Web 服务器是怎么运行的,以及如何手写一个简易版的 tomcat 服务器。
Web 服务器也称为超文本传输协议(HTTP)服务器,因为它使用 HTTP 与其客户端进行通信。常用的 Web 服务器有:Apache 服务器、Nginx 服务器、Tomcat 服务器等等。
对于笔者来说,最常使用的就是 tomcat 服务器了。
平常我们使用 tomcat 服务器都是新建一个 Web 项目,写好代码,在 IDEA 中配置好 tomcat 的路径然后点击 IDEA 的绿色启动按钮 Run,就启动了 tomcat 服务器,我们也就能通过浏览器去访问我们的项目了。不过却很少去深究里面的原理,今天我们就来盘一盘 Web 服务器的基本原理。
Http 请求流程
说到服务器,肯定要说到我们客户端(浏览器)是怎么和服务器交互的,通过HTTP 请求。Http 请求的处理流程如下:
先来说一下 HTTP 请求,我们如果要手写一个服务器,那肯定要先清楚服务器这边接受到的请求是什么样的,才好进行解析。
HTTP 请求
- 一个 Http 请求包含以下三个部分:
- 请求办法 + uri + 协议/版本
- 请求头
- 实体 示例如下:
GET http://localhost:8080/hello-servlet HTTP/1.1
Host: localhost:8080
Connection: keep-alive
sec-ch-ua: "Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
myname=pengnan&age=18
上面第一行就是请求办法 + uri + 使用的协议,中间的就是请求头,空行后面是实体。注意请求头和实体时间一定是有空行(CRLF)分隔的。
- 与 HTTP 请求类似,HTTP 响应也包括三个部分:
- 协议 + 状态码 + 描述
- 响应头
- 响应实体段 示例如下:
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=1174D54DBE49E3E6E157B2351ABB1201; Path=/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 53
Date: Sat, 16 Apr 2022 15:24:21 GMT
<html><body>
<h1>Hello World!</h1>
</body></html>
上述响应头和响应实体正文之间也有一个空行(CRLF)分隔。
Socket 类
俗话说的好,网络编程一切皆是 Socket。那 Socket 的重要性不言而喻。实现从浏览器到服务器的通信实际上就是 Socket 通信。Socket 也叫套接字,是网络连接的端点。那么相应的,我们只要在网络的这一边有个端点,在网络的另一边有个端点,就能实现网络的连接。
套接字由 java.net.Socket 标志,Socket 类可以分为服务端和客户端,服务端使用的类主要是 ServerSocket 类,客户端使用的类主要是 Socket 类。
为了更深刻的理解 HTTP 请求,也就是我们浏览器是怎么发送请求到服务器的(比如百度、微博之类)。我们可以先手写一个客户端,我们自己通过这个客户端来实现请求服务器(百度或微博),而不使用浏览器去请求。
既然是客户端,那我们就要使用 Socket 类,Socket 类有众多构造办法,常用的是这个
publict Socekt (java.lang.String host, int port)
其中参数 host 是远程主机的名称或 IP 地址,port 的相应的端口号。比如我们想连接百度,我们的 host 可以写 baidu.com, 端口可以写 443.
那我们是怎么发送出一个 HTTP 请求的呢(假设本次请求的是本地的 8080 端口),看下面代码:
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream outputStream = socket.getOutputStream();
PrintWriter out = new PrintWriter(outputStream, true);
// 给指定 ip 地址发送一个 http 请求
out.println("GET /index.jsp HTTP/1.1"); // 请求办法 uri 协议
out.println("Host: localhost:8080"); // 请求头
out.println("Connection: keep-alive");
out.println(); // 空行
那上面的代码就代表我们发送了一个 HTTP 请求了,如果要接受服务器返回来的响应,可以接着写:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
通过这个 in 来读取里面的内容进行解析。
这里说明一下流的使用,很简单的,outputStream 流就是可以往里面写东西的,因为要输出去嘛,output 出去,所以 outputStream 都是和 write 绑定使用的。而 inputSream 是输入流,输入进来的,我们要从里面读取东西出来,所以 inputStream 是和 read 绑定使用的。只要记住这两个关系,使用的时候就不会混。
简易版 Tomcat
ok,铺垫了这么多,终于可以进入正题,这次的简易版的服务器只有三个类:
- HttpServer
- Request
- Response
先说一下整体的思路:
- 既然是要搭建服务器,我们就理所当然的要使用 ServerSocket 类来创建一个套接字。
- HttpServer 的作用就是创建一个 ServerSocket,然后等待连接。
- 有连接进来后我们就要解析进来的流,解析成 Request 对象,方便我们使用。
- 本次简易版为了快速展示整个流程,我们只处理静态资源(后面会有改进)。
- 我们根据请求的路径去获取静态资源文件,读取文件内容然后使用 Response 类输出到浏览器回去。
主体代码如下,我们贴一点解释一点,慢慢来
- 首先是 HttpServer 类,他的作用就几个:
- 创建 ServerSocket
- 使用 Request 解析
- 使用 Response 响应
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() {
xxx
}
}
这里说一下 System.getProperty("user.dir")
的作用,就是获取当前项目所在的根路径,WEB_ROOT
就是我们存放静态资源的目录。上述的 SHUTDOWN_COMMAND 表示如果请求路径是 /SHUTDOWN 就会停止服务器。
这里我们简单看下整个目录结构:
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 request = new Request(input);
request.parse();
// 创建一个 Response 对象,返回静态资源
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
// Close the socket
socket.close();
// 检查是否是停止命令
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
这个 await() 办法别看代码少,其实这就是一个 web 服务器的本质了,创建 ServerSocket,等待连接,处理连接,返回响应。
接着看 Request 类和 Response 类:
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
public void parse() {
// 从 socket 流中读取数据 这个 parse 办法就是读取流,流里面其实就是 http 请求(见上面的 http请求示例)
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++) { // i 是读取的长度,将流中的内容写到 request 字符串中
request.append((char) buffer[j]);
}
System.out.print(request.toString());
uri = parseUri(request.toString());
}
// 根据 http 请求解析出请求路径
// 其实就是 http 请求第一行,两个空格之间:GET index.html HTTP/1.1
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;
}
}
Request 类这里的作用主要的读取 HTTP 请求中的路径,这时候整个 request 实例会被传给 response。
public class Response {
private static final int BUFFER_SIZE = 1024;
Request request;
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 {
// 自己写一个成功的响应头
String successHeader = "HTTP/1.1 200 OK\r\n" +
"Content-Type: text/html\r\n" +
"\r\n";
output.write(successHeader.getBytes()); // 注意 Response 类中的 output 是 HttpServer 类中传进来的
// 再读取 webroot 目录下的静态文件的内容,输出到 output
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch != -1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
} else {
// file not found
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();
}
}
}
Response 类主要是根据路径去找文件,如果文件不存在,则返回错误消息到浏览器,存在则返回文件内容到浏览器。
写完之后我们看下 index.html 的内容:
<html>
<head>
<title>Welcome to BrainySoftware</title>
</head>
<body>
<img src="./images/logo.gif">
<br>
Welcome to My first Server.
</body>
</html>
启动一下 HttpServer 的 main 办法,访问一下 http://localhost:8080/index.html 如下:
至此,一个简易版的 Tomcat 服务器就算手写完毕。之后还会对这个手写的服务器进行进一步的完善,不出意外的话这应该会写成一个系列,这是第一篇。
代码我放在 gitee 上面,需要自取:gitee.com/the_meaning…
本篇文章思路来自《深入剖析 Tomcat》这本书,这是第一章的内容,看完之后觉得有点意思。我看的是实体书,如果也想要看同学可以评论区 call 我一下,我去找找电子书资源放上来。
写完太晚啦,晚安吧。