前言
最近在看一些前端的知识,但是只是看了一些标签的用法,真正到了使用的时候,却又写不出来,学习的效率很低,但是我也发现了一个有意思的点,当我在HBuilder中启动项目时,我就可以在浏览器中访问它了——这说明HBuilder本身启动了一个静态web服务器。所以,本着对这个的兴趣,我准备来自己模仿实现一个简单的HBuilder——这里只是实现可以在浏览器中访问这个特性。
注:我这里只处理GET方式的无参请求。
浏览器实现效果
demo的目录结构
说明:
src目录下面是模拟的简单静态服务器的代码,然后提供了一个404.html页面,下面的resource目录是存放静态web文件的地方,所有与web相关的文件都在此文件夹中——html、css、js文件。
demo介绍
先来理解一下HBuilder的工作模式:
HBuilder编写好代码后,直接选择允许,然后自动弹出浏览器窗口,页面已经可以访问了。对于页面中使用的静态资源,都是使用的相对路径。这些相对路径都是相对于当前文件夹所在的路径,即以 ./ 或者 / 开头的资源。例如:resource 目录下的图片nicai.jpg,在html中使用时,它的路径为:./nicai.jpg或者 /nicai.jpg。但我们在浏览器中使用此资源时,它的路径即为:http://ip:port/nicai.jpg。这即是统一定位符(URL)或者统一资源标识符(URI)的威力,但是我们也要明白它的定位,也是基于主机本身的定位的。通过URL可以对应一个确定的资源,这里就是 / 映射到了resource目录下面,所以我们可以通过网络访问到resource下面的文件。
所以,这里的思路就很清晰了。只要获取到浏览器发送的请求报文中的响应行,并且获取里面的请求路径,如:/ 或者 /index.html即表示请求resource目录下面的index.html文件,其它的情况类似。
StaticWebServer 类
启动一个ServerSocket对象,然后获取用户连接后发送的请求并返回响应数据。
package org.dragon;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StaticWebServer {
private final static int PORT = 8000; // 端口
private final static int THREAD_NUM = 10; // 线程池的容量
public void start() {
System.out.println("服务启动中。。。");
ExecutorService pool = Executors.newFixedThreadPool(THREAD_NUM);
try (ServerSocket server = new ServerSocket(PORT)) {
while (true) {
System.out.println("等待连接。。。");
Socket con = server.accept();
pool.submit(new Connection(con)); // 使用线程处理每一个连接,这里的连接均为短连接,即模拟 HTTP/1.0 协议的部分功能。
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new StaticWebServer().start();
}
}
Connection 类
获取到请求路径,然后处理它。注意,这里如果使用GET方式的含参请求,程序就会报错。不过,这不是主要的,这里只是一个简单的验证性的demo。这个类的主要功能是:获取请求报文中的第一行,即请求行,然后其余的直接读取输出在控制台。
请求行包括:请求方法 请求路径 协议版本\r\n。
这里解析出来请求行即可,其它的不做处理。然后针对请求路径进行处理,请求路径直接映射到 resource目录下,使用路径进行拼接,最终是通过file协议来定位到资源的位置。这个也可以算是file协议和http协议之间的交互吧。如果不存在该路径的话,即没有找到该URL对应的资源,这就是我们的老朋友——404 Not Found了,返回一个固定的404页面即可。
package org.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Connection implements Runnable {
private Socket connection;
private InputStream in;
private OutputStream out;
public Connection(Socket connection) {
this.connection = connection;
}
@Override
public void run() {
try {
in = new BufferedInputStream(connection.getInputStream());
out = new BufferedOutputStream(connection.getOutputStream());
// 读取请求行,剩下的不处理。
StringBuilder sb = new StringBuilder();
while (true) {
int c = in.read();
if (c == '\r' || c == '\n' || c == -1 ) {
break;
}
sb.append((char)c);
}
// 这种情况有问题,不处理它。
if (sb.length() == 0) {
return ;
}
String requestLine = sb.toString();
String[] requests = requestLine.split(" ");
System.out.println("请求报文:\n" + requestLine);
// 对于剩下的请求报文,只是简单的打印处理,1024字节已经足够了。
byte[] b = new byte[1024];
int len = in.read(b);
System.out.println(new String(b, 0, len, StandardCharsets.UTF_8));
// 只处理GET方式的无参请求,也即只对请求路径进行响应,忽略其它参数。
String requestPath = requests[1];
requestPath = "/".equals(requestPath) ? "/index.html" : requestPath;
// 开始生成响应报文
int statusCode = 0; // 状态码
String phrase = null; // 短语
String contentType = null; // MIME类型
long length; // 文件的长度
Path path = Paths.get("./", "resource", requestPath);
// 请求路径存在且为一个文件
if (Files.exists(path) && !Files.isDirectory(path)) {
// 200 OK
statusCode = 200;
phrase = "OK";
contentType = Files.probeContentType(path);
length = Files.size(path);
} else {
// 404 Not Found!
statusCode = 404;
phrase = "Not Found";
path = Paths.get("./src", "org", "dragon", "404.html");
contentType = Files.probeContentType(path);
// 如果找不到格式,就默认它为通用MIME类型的 application/octet-stream
contentType = contentType == null ? "application/octet-stream" : contentType;
length = Files.size(path);
}
byte[] header = this.getHeader(statusCode, phrase, contentType, length);
byte[] body = this.getBody(path);
// 响应
out.write(header);
out.write(body);
out.flush(); // 手动刷新输出流,因为我用的是缓冲流
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 这里不使用StringBuilder拼接了,所以要注意空格,不要遗漏。
* (实际上,它应该还是会被优化成StringBuilder的。)
* */
public byte[] getHeader(int statusCode, String phrase, String contentType, long length) {
String header = "HTTP/1.0 " + statusCode + " " + phrase +"\r\n"
+ "Content-Type: " + contentType + "\r\n"
+ "Content-Length: " + length + "\r\n"
+ "\r\n";
return header.getBytes(StandardCharsets.UTF_8);
}
public byte[] getBody(Path path) throws IOException {
// 读取小文件,大文件不合适!
return Files.readAllBytes(path);
}
}
运行时的输出(部分)
浏览器的网络调试窗口
eclipse的输出
服务启动中。。。
等待连接。。。
等待连接。。。
等待连接。。。
请求报文:
GET / HTTP/1.1
Host: localhost:8000
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36 Edg/85.0.564.51
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,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-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
请求报文:
GET /my.css HTTP/1.1
等待连接。。。
Host: localhost:8000
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36 Edg/85.0.564.51
Accept: text/css,*/*;q=0.1
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: style
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
请求报文:
GET /my.js HTTP/1.1
Host: localhost:8000
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36 Edg/85.0.564.51
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: script
Referer: http://localhost:8000/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
404页面
进一步可改进的点
如果觉得太乱的话,可以把不同类的资源放入不同的文件夹中,然后修改相对路径的位置即可。但是要注意,index.html和favico.ico必须是处于根目录下面,这个一般是约定的处理,不要破坏它。
注意: 我这里使用到的静态资源就不提供了,因为我是学习web前端的,但是写的不好,如果你想要验证的话,可以将自己的web前端静态资源(html、css、js)直接复制到resource目录下,启动StaticWebServer类即可。
说明
这个小demo挺有趣的,虽然前端没有学习到什么东西,但是也算是复习了网络的知识吧。感兴趣的朋友们,可以自己尝试一下,对于网络的理解和学习都是有一点帮助的。