上期文章,我们做了一个简单的 web 服务器,实现了对一个请求的简单响应。这篇文章我们会在之前的基础上,实现对服务器中静态资源的访问,包括 HTML、以及HTML中包含的 css、js 和图片。
HTML 的全称为超文本标记语言,我们可以理解为按照某种规则对多媒体资源进行排版显示的文本格式,浏览器访问的各种页面也是基于 HTML 。那么我们如何返回一个 HTML 格式的响应呢?
我们还是从 Hello World 开始,这次我们返回给浏览器一个蓝色的 Hello World(因为我们地球就是蓝色星球,哈哈)。
在这之前我们需要先准备一个 HTML 文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
<style>
body {
text-align: center;
}
h1 {
font-size: 72px;
color: blue;
}
</style>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
在浏览器中打开之后的效果如下:
浏览器和我们的 Web 服务器依赖 http 协议进行数据传输。如果我们要给浏览器返回上面的 html 文件,还需要将这个文件弄成 HTTP 格式的信息,再返回给我们的浏览器。那么 HTTP 协议是什么格式的呢?
HTTP 的报文分两种,一种是客户端向服务端发送的请请求报文,另一种则是服务端向客户端返回的响应报文。由于我们的“蓝色Hello World”是作为响应信息返回给浏览器(即客户端),所以这里我们先分析响应报文。以下是一个响应报文的示例:
HTTP/1.1 200 OK
Date: Sat, 16 Jan 2024 14:36:19 GMT
Server: Apache/2.4.6 (CentOS)
Content-Length: 44
Content-Type: text/html; charset=utf-8
<!DOCTYPE html>
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
下面我们对报文进行简单的解析:
我们将“蓝色 Hello World”处理成一个 HTTP 响应报文:
HTTP/1.1 200 OK
Content-Length: 408
Content-Type: text/html; charset=utf-8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
<style>
body {
text-align: center;
}
h1 {
font-size: 72px;
color: blue;
}
</style>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
现在我们结合前面的文章中的代码,将响应信息直接返回给请求该服务器的浏览器。
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
Socket accept = serverSocket.accept();
InputStream inputStream = accept.getInputStream();
byte[] bytes = new byte[1024];
int i;
//输出请求信息
while((i=inputStream.read(bytes,0,bytes.length))!=-1){
System.out.println(new String(bytes,0,i,StandardCharsets.UTF_8));
if (i<1024){
break;
}
}
//响应内容
String responseContext = "HTTP/1.1 200 OK\n" +
"Content-Length: 408\n" +
"Content-Type: text/html; charset=utf-8\n" +
"\n" +
"<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
" <title>Hello World</title>\n" +
" <style>\n" +
" body {\n" +
" text-align: center;\n" +
" }\n" +
" h1 {\n" +
" font-size: 72px;\n" +
" color: blue;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <h1>Hello, World!</h1>\n" +
"</body>\n" +
"</html>";
OutputStream outputStream = accept.getOutputStream();
outputStream.write(responseContext.getBytes(StandardCharsets.UTF_8));
//清空缓存区,刷新到目的空间
outputStream.flush();
outputStream.close();
inputStream.close();
}
使用浏览器访问 http://localhost:8080/ 就能得到如下的效果
到这里,我们已经能够返回一个 HTML 格式的响应报文,并且浏览器也成功获取到报文,在页面显示了我们想要的效果。但是这就够了吗?不!我们还要根据请求路径去展示不同的页面,并且页面中还包含额外的资源,比如图片、js、css等等。
在我们使用浏览器访问一个网站的时候,网页中往往包含各种按钮或者是链接,当我们点击的时候,就会进入到各种各样的页面。如果这个时候我们去观察浏览器的网址栏,就会发现,我们点击按钮或者链接进入的各种网页本质上是在切换不同的 URL,去请求对应的网页资源。这里我们模仿这个操作,去处理 HTTP 请求,以实现静态资源的切换。
HTTP 请求和响应格式类似,但是又有一些不同。以下是一个请求报文的示例:
POST /submit-form HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
username=example&password=123
下面我们对报文进行简单的解析:
报文的第一行是请求行,其中有三个部分,用空格区分:
请求行后面的就是类似于响应头的请求头,同样是 k-v 结构,描述请求相关的内容
请求头之后是请求空行,用于区分请求头部和请求主体
请求空行之后是请求主体,携带请求主体信息。GET 请求通常不包含请求体,而 POST 请求则可能包含请求体,用来传递需要提交到服务器的数据。
根据对请求报文的分析,我们知道,请求报文的第一行的第二部分是请求 URL。因此我们只需要处理请求报文的第一行,并且获取第二部分的请求信息,然后根据请求信息去获取到对应的静态资源。所以我们需要对程序进行一定的修改。
inputStream = accept.getInputStream();
BufferedReader bfr = new BufferedReader(new InputStreamReader(inputStream));
String firstLine = bfr.readLine();
if (firstLine==null||"".equals(firstLine)){
return;
}
String httpPath = firstLine.split(" ")[1];
解释一下上面的代码:
首先我们先通过 accept 对象获取到 InputStream 对象
然后将获取到的 InputStream 包装成一个 BufferedReader 对象目的是为了方便按行读取数据
接着我们读取请求报文的第一行
判断请求报文是否有内容,没有内容的话就直接结束
然后按照空格去分隔请求行,拿到请求 URL(ps:这里写的有点粗糙,可能为空,也可能分割有问题,由于不是重点,我们就简单处理一下)
到这里,我们就拿到了请求 URL。
通过上面的步骤,我们顺利地拿到了请求 URL,但是我们怎么根据 URL 去拿到对应的资源呢?一种非常暴力的思路就是 if-else,通过预先设定好的几种 URL 去响应指定的字符串对象。优点是实现简单,但是缺点也很明显,代码可读性和可扩展性都极差。
使用 tomcat 部署过项目的同学应该知道,只要把静态文件放到指定的目录下,然后我们通过与文件对应的 URL 就可以访问到指定的文件。因此我在想是不是也可以预先指定一个目录,然后根据请求的 URL 去访问指定目录下的文件。
private static final String STATIC_RESOURCE_PATH = "src/main/resources/static/";
//读取文件
String path = STATIC_RESOURCE_PATH+httpPath;
//创建文件对象
File file = new File(path);
private static final String PAGE_NOT_FOUND_404_PATH = "404.txt";
404.txt 文件内容如下:
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
Sorry, the page you are looking for could not be found.
``` //判断文件是否存在,不存在直接返回 404
if (!file.exists()){
fileInputStream = new FileInputStream(STATIC_RESOURCE_PATH+PAGE_NOT_FOUND_404_PATH);
int b = 0;
byte[] bytes = new byte[1024];
while((b=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,b);
}
}else{
fileInputStream = new FileInputStream(file);
//写文件
//响应行
outputStream.write("HTTP/1.1 200 OK\r\n".getBytes(StandardCharsets.UTF_8));
//响应头
outputStream.write(("Content-Length:"+file.length()+"\r\n").getBytes(StandardCharsets.UTF_8));
//响应空行
outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8));
//响应内容
int b = 0;
byte[] bytes = new byte[1024];
while((b=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,b);
}
}
outputStream.flush();
//释放资源
if (outputStream!=null){
outputStream.close();
}
if (bfr!=null){
bfr.close();
}
if (fileInputStream!=null){
fileInputStream.close();
}
if (inputStream!=null){
inputStream.close();
}
最终代码
public class HttpServer {
private static final String STATIC_RESOURCE_PATH = "src/main/resources/static/";
private static final String PAGE_NOT_FOUND_404_PATH = "404.txt";
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
while(true){
InputStream inputStream = null;
FileInputStream fileInputStream = null;
OutputStream outputStream = null;
BufferedReader bfr = null;
try{
//获取输入流
Socket accept = server.accept();
//获取输出流
outputStream = accept.getOutputStream();
inputStream = accept.getInputStream();
//对流进行包装
bfr = new BufferedReader(new InputStreamReader(inputStream));
String firstLine = bfr.readLine();
if (firstLine==null||"".equals(firstLine)){
return;
}
String httpPath = firstLine.split(" ")[1];
//读取文件
String path = STATIC_RESOURCE_PATH+httpPath;
//创建文件对象
File file = new File(path);
//判断文件是否存在,不存在直接返回 404
if (!file.exists()){
fileInputStream = new FileInputStream(STATIC_RESOURCE_PATH+PAGE_NOT_FOUND_404_PATH);
int b = 0;
byte[] bytes = new byte[1024];
while((b=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,b);
}
}else{
fileInputStream = new FileInputStream(file);
//写文件
//响应行
outputStream.write("HTTP/1.1 200 OK\r\n".getBytes(StandardCharsets.UTF_8));
//响应头
outputStream.write(("Content-Length:"+file.length()+"\r\n").getBytes(StandardCharsets.UTF_8));
//响应空行
outputStream.write("\r\n".getBytes(StandardCharsets.UTF_8));
//响应内容
int b = 0;
byte[] bytes = new byte[1024];
while((b=fileInputStream.read(bytes))!=-1){
outputStream.write(bytes,0,b);
}
}
outputStream.flush();
}catch (Exception e){
e.printStackTrace();
}finally{
//释放资源
if (outputStream!=null){
outputStream.close();
}
if (bfr!=null){
bfr.close();
}
if (fileInputStream!=null){
fileInputStream.close();
}
if (inputStream!=null){
inputStream.close();
}
}
}
}
}
http://localhost:8080/hello.html
http://localhost:8080/hello2.html
http://localhost:8080/hello3.html
http://localhost:8080/hello4.html
在 HTML 中可能会链接外部的 js 文件、css 文件、图片等等。做的过程中我就在思考,这一部分该怎么去实现。那时候我在坐地铁,正好没啥事,所以针对这个问题我就开始在脑子里面去模拟了,首先我想到的是服务端的解决方案。
想要知道一个 HTML 是否包含其他的资源,我们就需要去解析 HTML 文件,然后针对其中的某些标签,比如 <img src="">
,通过解析标签中的 src 属性,定位到相应的资源文件。这个时候就有一个问题摆在我面前,多个文件如何返回到客户端?
首先我们必须要遵循 HTTP 响应报文的格式,并且响应主体中是 HTML ,然后还要携带 HTML 中包含的其他资源文件,在我对于 HTTP 协议的认知里面,好像没有办法在一次传输 HTML 本体的同时还要携带它所包含的多媒体资源。所以我就想,是不是应该分多次请求?
想到多次,我突然意识到,HTTP 通信是按照一个请求对应一个响应进行的,如果分多次传输,那么客户端一定要发起多次请求。发送既然是客户端去做的,那么客户端就一定明确的知道它需要请求哪些资源,因此我猜想:
如果是这样的话,实现起来肯定会比在服务器端去处理资源要更为简单,而且客户端本来也需要解析 HTML 文件去渲染效果,让客户端去请求多个资源也更合理。同样,服务器端的职责也更加清晰——返回符合请求的响应即可,不需要做额外的事情。
接下来我们就来验证一下是不是如我们猜想的一样,是客户端去解析和请求多个资源。
首先我们需要一个包含众多资源的 HTML 文件,包括 js、css、图片。HTML内容如下
<!DOCTYPE html>
<html>
<head>
<title>望岳</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<div id="background">
<h1 id="poem">望岳</h1>
<p id="poem">作者:杜甫</p>
<p id="poem">朝代:唐</p>
<p id="poem">全文如下:</p>
<p id="poem">岱宗夫如何?齐鲁青末了。造化钟神秀,阴阳割昏晓。荡胸生曾云,决眦入归鸟。会当凌绝顶,一览众山小。</p>
</div>
</body>
</html>
其中包含了 css 和 js ,然后在又在包含的 css 文件中引用了一张背景图片,项目目录如下:
我们通过浏览器去访问 http://localhost:8080/wangyue.html,看看效果
虽然乱码了了,但是我们可以看到确实是浏览器去请求HTML包含的所有多媒体文件,而服务器只负责根据请求的url 返回相应的文件。
到这里我们这篇文章也要跟大家说再见了,这篇文章我们完成了这样几件事情:
最后希望大家多多点赞评论支持,你的支持是我持续更新的最大动力,那,让我们下一期再见吧!