完成404的响应
上一个版本中我们已经实现了根据浏览器中用户在地址栏上输入的URL中的抽象路径去static目录下寻找对应资源进行响应的工作。
但是会存在路径输入有误,导致定位不对(要么定位的是一个目录,要么该文件不存在),此时再发送响应的响应正文时使用文件输入流读取就会出现异常提示该资源不存在。
这是一个典型的404情况,因此我们在ClientHandler处理请求的环节,在实例化File对象根据抽象路径定位webapps下的资源后,要添加一个分支,若该资源存在则将其响应回去,如果不存在则要响应404状态代码和404页面提示用户。
实现:
1:在static下新建一个子目录root
该目录用于保存当前服务端所有网络应用共用的资源,比如404页面,因为无论请求哪个网络应用中的资源都可能发生不存在的情况。
2:在root目录下新建页面:404.html
该页面居中显示一行字即可:404,资源不存在!
页面内容为:
效果如下:
3:在ClientHandler处理请求的环节,当实例化File对象后添加一个分支,如果该File对象存在且表示的是一个文件则将其响应给浏览器
否则发送的响应做如下变化:
1:状态行中的状态代码改为404,状态描述改为NotFound
2:响应头Content-Length发送的是404页面的长度
3:响应正文为404页面内容
完成后,在浏览器地址栏输入一个不存在的资源地址,检查服务端是否正确响应404页面(在浏览器中输入http://localhost:8088/myweb)
package com.webserver.core;
import com.webserver.http.HttpServerRequest;
import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 该线程任务负责与指定的客户端完成HTTP交互
* 每次HTTP交互都采取一问一答的规则,因此交互由三步来完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable{ //客户端处理器
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
HttpServletRequest request = new HttpServletRequest(socket);
//2处理请求
//例如:浏览器地址栏输入的路径为:http://localhost:8088/myweb/index.html
//那么解析请求后得到的抽象路径部分uri:/myweb/index.html
String path = request.getUri();
System.out.println("抽象路径:" + path);
//3发送响应
/*
File(File parent,String sub)
阅读文档,理解该构造方法
提示:static目录是确定存在的目录
我们是要找这个目录下的内容是否存在
*/
File staticDir = new File(
ClientHandler.class.getClassLoader().getResource(
"./static"
).toURI()
);
//去static目录下根据用户请求的抽象路径定位下面的文件
File file = new File(staticDir,path);
String line;//状态行
if(file.isFile()){//实际存在的文件
line = "HTTP/1.1 200 OK";
}else{//1:文件不存在 2:是一个目录
line = "HTTP/1.1 404 NotFound";
file = new File(staticDir,"/root/404.html");
}
//3.1发送状态行
println(line);
//3.2发送响应头
line = "Content-Type: text/html";
println(line);
line = "Content-Length: " + file.length();
println(line);
//单独发送回车+换行表示响应头部分发送完毕
println("");
//3.3发送响应正文
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024 * 10];
int len;
FileInputStream fis = new FileInputStream(file);
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
System.out.println("响应发送完毕!");
} catch(IOException | URISyntaxException e){
e.printStackTrace();
}finally{
//一次HTTP交互后断开链接(HTTP协议要求)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);//发送回车符
out.write(10);//发送换行符
}
}
测试:
输入http://localhost:8088/myweb/index123.html
控制台信息可以正常显示:
输入http://localhost:8088
进行重构(功能拆分):
重构代码
将ClientHandler中发送响应的工作拆分出去
实现:
1:在com.webserver.http包中新建类:HttpServletResponse 响应对象
2:在响应对象中定义对应的属性来保存响应内容并定义response方法来发送响应.
3:修改ClientHandler,使用响应对象完整响应的发送
HttServletResponse中:
package com.webserver.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* 响应对象
* 该类的每一个实例用于表示一个HTTP协议规定的响应内容。
* 每个响应由三部分构成:
* 状态行,响应头,响应正文
*/
public class HttpServletResponse {
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应正文的相关信息
private File contentFile;
private Socket socket;
public HttpServletResponse(Socket socket){
this.socket = socket;
}
/**
* 发送响应
* 将当前响应对象内容按照标准的响应格式发送给客户端
*/
public void response() throws IOException {
//3.1发送状态行
sendStatusLine();
//3.2发送响应头
sendHeaders();
//3.3发送响应正文
sendContent();
}
/**
* 发送状态行
*/
private void sendStatusLine() throws IOException {
String line = "HTTP/1.1"+" "+statusCode+" "+statusReason;
println(line);
System.out.println("发送状态行:"+line);
}
/**
* 发送响应头
*/
private void sendHeaders() throws IOException {
String line = "Content-Type: text/html";
println(line);
System.out.println("发送响应头:"+line);
line = "Content-Length: "+contentFile.length();
println(line);
System.out.println("发送响应头:"+line);
//单独发送回车+换行表示响应头部分发送完毕
println("");
}
/**
* 发送响应正文
*/
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024*10];
int len;
try( //自动关闭流
FileInputStream fis = new FileInputStream(contentFile);
) {
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
System.out.println("响应正文发送完毕!");
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);//发送回车符
out.write(10);//发送换行符
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
public File getContentFile() {
return contentFile;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
}
}
ClientHandler中:
package com.webserver.core;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 该线程任务负责与指定的客户端完成HTTP交互
* 每次HTTP交互都采取一问一答的规则,因此交互由三步来完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable{ //客户端处理器
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
HttpServletRequest request = new HttpServletRequest(socket);
HttServletResponse response = new HttServletResponse(socket);
//2处理请求
String path = request.getUri();
System.out.println("抽象路径:" + path);
File staticDir = new File(
ClientHandler.class.getClassLoader().getResource(
"./static"
).toURI()
);
//去static目录下根据用户请求的抽象路径定位下面的文件
File file = new File(staticDir,path);
if(file.isFile()){//实际存在的文件
response.setContentFile(file);
}else{//1:文件不存在 2:是一个目录
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"/root/404.html");
response.setContentFile(file);
}
//3发送响应
response.response();
} catch(IOException | URISyntaxException e){
e.printStackTrace();
}finally{
//一次HTTP交互后断开链接(HTTP协议要求)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
测试:
正常页面测试:(http://localhost:8088/myweb/index.html)
404页面测试:(http://localhost:8088/)
重构代码,将ClientHandler中处理请求的操作拆分出去
实现:
1:在com.webserver.core包下新建类:DispatcherServlet,并定义service方法,用来处理请求
2:将ClientHandler处理请求的操作移动到service方法中去
3:ClientHandler通过调用DispatcherServlet的service完成处理请求环节.
DispatcherServlet中:
package com.webserver.core;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
import java.io.File;
import java.net.URISyntaxException;
/**
* 处理请求的环节
*/
public class DispatcherServlet {
//表示resources下的static目录,实际运行编译后是target/classes下的static目录
private static File staticDir;
static { //仅需要一份,所以放置在静态块中加载
try { //静态块中不能抛出异常,只能自己解决(try/catch)
staticDir = new File(
ClientHandler.class.getClassLoader().getResource(
"./static"
).toURI()
);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
public void service(HttpServletRequest request, HttServletResponse response){
String path = request.getUri();
System.out.println("抽象路径:" + path);
//去static目录下根据用户请求的抽象路径定位下面的文件
File file = new File(staticDir,path);
if(file.isFile()){//实际存在的文件
response.setContentFile(file);
}else{//1:文件不存在 2:是一个目录
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"/root/404.html");
response.setContentFile(file);
}
}
}
ClientHandler中:
package com.webserver.core;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 该线程任务负责与指定的客户端完成HTTP交互
* 每次HTTP交互都采取一问一答的规则,因此交互由三步来完成:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable{ //客户端处理器
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//1解析请求
HttpServletRequest request = new HttpServletRequest(socket);
HttServletResponse response = new HttServletResponse(socket);
//2处理请求
DispatcherServlet servlet = new DispatcherServlet();
servlet.service(request,response);
//3发送响应
response.response();
} catch(IOException e){
e.printStackTrace();
}finally{
//一次HTTP交互后断开链接(HTTP协议要求)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在index页面上显示一张图片
实现:
1:准备一张图片,取名为logo
2:在index.html页面上使用标签将其显示出来
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>我的首页</title>
</head>
<body>
<!--
注释
<h>标签分为<h1>-<h6>表示各级标题。标题独占一行。
<center>标签:HTML5之后不再建议使用的标签。作用是将它包含的所有元素在浏览器上居中显示
<input>标签:输入组件,用于获取用户在页面上的输入。输入组件可以有多种样子
具体可以通过type属性执行。例如:
type="text":文本框
type="password":密码框
type="radio":单选框
type="checkbox":复选框
type="button":无事件按钮
type="submit":提交按钮
<a>标签:超链接,标签中间指定超链接的文本信息,href属性用于指定点击后跳转
的位置
<br>标签:换行
<table>标签:表格。属性border用于指定边框。
<table>标签中包含<tr>标签用于表示行
<tr>标签中包含<td>标签用于表示列
<td>标签中常见属性:
align:对其方式。left左对齐,right右对齐,center剧中对其
colspan:跨列合并列,合并是从左向右合并列
rowspan:跨行合并列,合并是从上向下合并列
-->
<center>
<!--
<h1>L12131415L</h1>
-->
<img src="./logo.jpg" width="400">
<br>
<input type="text" size="25">
<input type="button" value="点我!" onclick="alert('点你妹啊')">
<br>
<a href="https://blog.csdn.net/L12131415L">别点这链接</a>
<br>
<table border="1">
<tr>
<td rowspan="3">CGB2112</td>
<td>绍伟</td>
<td>翔麟</td>
<td>洪江</td>
<td>老王</td>
<td>芳芳</td>
<td>飞哥</td>
<td colspan="2" align="center">文灿</td>
</tr>
<tr>
<td colspan="2" align="center">涵博</td>
<td>丽娟</td>
<td>敏子</td>
<td>小渔</td>
<td>凡哥</td>
<td>泽玄</td>
<td>永其</td>
</tr>
<tr>
<td colspan="3" align="center">兴源</td>
<td colspan="2" align="center">曦露</td>
<td colspan="3" align="center">文铧</td>
</tr>
</table>
</center>
</body>
</html>
直接查看效果:
3:启动服务端,使用浏览器请求该页面查看图片的显示情况.
测试发现图片无法加载.
原因:
当页面上需要加载其他资源(图片,样式文件,脚本文件等)时,浏览器会自动发起请求去下载
这些资源.因此当我们的页面上有其他资源时,呈现一个页面一问一答就不够了,需要多次的
请求响应来完成.
解决:
将WebServerApplication的start方法中的代码加上死循环,重复接收多次客户端的连接即可.
WebServerApplication中:
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* WebServer主类
* WebServer是一个web容器,模拟Tomcat的基础功能。
* Web容器的两个主要任务:
* 1:管理部署在容器中的所有网络应用(WebApp),每个网络应用就是我们俗称的一个"网站。"
* 它通常包含页面,处理业务的代码,其他资源等等
* 2:负责与客户端(通常是浏览器)完成TCP链接,并基于HTTP协议进行交互,使得客户端可以
* 通过网络远程调用容器中的某个网络应用
*/
public class WebServerApplication {
private ServerSocket serverSocket;
public WebServerApplication() {
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while (true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程负责与该客户端交互
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WebServerApplication server = new WebServerApplication();
server.start();
}
}
此时再次测试可以正常显示图片!
HTML中图片路径指定:
404.html中代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body>
<center>
<img src="./404.png">
<h1>404资源不存在!</h1>
</center>
</body>
</html>
本地打开:
浏览器下打开:
原因:
在html页面上我们可以指定一个资源的路径以便加载或使用。例如超链接,图片等标签上常常会指定一个路径。
路径有两种:
1:相对路径
- 页面上使用"./“表示当前目录时,由于html是被浏览器解释的,因此该路径不能与在java源代码中使用”./"混淆(实际表示的路径不相同!)
- 浏览器在解释页面上的"./“时,是根据请求当前页面时URL中该页面所在的目录:
例如:
请求当前首页。我们在浏览器输入的地址为:
http://localhost:8088/myweb/index.html
那么在index.html页面中若使用了”./"路径,则浏览器理解的实际位置:
http://localhost:8088/myweb/
因此若页面上<img src="./logo.png">
浏览器实际理解的该图片路径为:
http://localhost:8088/myweb/logo.png
相对路径在进行内部转发操作时可能导致定位失效。
当输入的是
http://localhost:8088/myweb/123.html
404页面上的代码为:
<img src="./404.png">
浏览器实际理解的该图片路径为:
http://localhost:8088/myweb/404.png
但实际404.png在root目录下的。
2:绝对路径
"/":根。指的位置是URL当中抽象路径开始的"/"
举例: v
http://localhost:8088/myweb/index.html
|----抽象路径-----|
在index.html页面上:
<img src="/myweb/logo.png">
^就是绝对路径中"/",对应URL中抽象路径部分起始的"/":
http://localhost:8088/myweb/index.html
^
因此,浏览器理解该图片的实际位置:
http://localhost:8088/myweb/logo.png
解决办法:
http://localhost/myweb/logo.jpg
http://localhost/root/404.png
这样在浏览器中就可以正常显示了。
导入学子商城
导入学子商城webapp资源后访问其首页,发现页面无法正常显示.
在resources/static中导入文件:
测试http://localhost:8088/TeduStore/index.html,效果如下:
在浏览器中按F12进行查看,效果如下:
浏览器F12跟踪请求和响应的交互发现两个问题:
1:我们仅发送了两个响应头(Content-Length和Content-Type).
虽然目前仅需要这两个头,但是服务端实际可以根据处理情况设置需要发送其他响应头
2:Content-Type的值是固定的"text/html",这导致浏览器请求到该资源后无法正确
理解该资源因此没有发挥出实际作用.
解决办法:
1:在HttpServletResponse中添加一个Map类型的属性用于保存所有要发送的响应头
Map<String,String> headers
2:修改发送响应头的方法sendHeaders中的逻辑,将固定发送两个头的操作改为遍历
headers这个Map,将所有要发送的响应头逐个发送
此时重新重启服务端,测试http://localhost:8088/TeduStore/index.html
3:只需要在发送前根据处理情况向headers中put要发送的响应头即可.这个工作需要
3.1:在响应对象中添加一个方法:addHeader,将要发送的响应头存入headers中
3.2:在DispatcherServlet处理请求环节调用addHeader存放要发送的响应头即可
HttpServletResponse中:
package com.webserver.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HttServletResponse {
//状态行相关信息
private int statusCode = 200; //状态代码
private String statusReason = "OK"; //状态描述
//响应头相关信息
//key:响应头的名字 value:响应头对应的值
private Map<String,String> headers = new HashMap<>();
//响应正文的相关信息
private File contentFile;
private Socket socket;
public HttServletResponse(Socket socket) {
this.socket = socket;
}
/**
* 发送响应
* 将当前响应对象内容按照标准的响应格式发送给客户端
*/
public void response() throws IOException {
//3.1发送状态行
sendStatusLine();
//3.2发送响应头
sendHeaders();
//3.3发送响应正文
sendContent();
}
/**
* 发送状态行
*/
private void sendStatusLine() throws IOException {
String line = "HTTP/1.1"+" "+statusCode+" "+statusReason;
println(line);
System.out.println("发送状态行:"+line);
}
/**
* 发送响应头
*/
private void sendHeaders() throws IOException {
//遍历headers将所有响应头发送给浏览器
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for (Map.Entry<String,String> e: entrySet) {
String name = e.getKey(); //获取响应头的名字
String value = e.getValue(); //获取响应头对应的值
//Content-Type: text/html
String line = name + ": " + value;
println(line);
System.out.println("发送响应头:"+line);
}
//单独发送回车+换行表示响应头部分发送完毕
println("");
}
/**
* 发送响应正文
*/
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024 * 10];
int len;
try( //自动关闭流
FileInputStream fis = new FileInputStream(contentFile);
) {
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
System.out.println("响应正文发送完毕!");
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);//发送回车符
out.write(10);//发送换行符
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
public File getContentFile() {
return contentFile;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
}
/**
* 添加一个要发送的响应头
* @param name
* @param value
*/
public void addHeader(String name,String value) {
this.headers.put(name,value);
}
}
在DispatcherServlet中实现根据用户请求的资源的后缀来设置响应头 Content-Type的值.
目前先支持6个类型实现功能:
实现:
1:在DispatcherServlet中定义一个Map,保存资源后缀名与对应的类型头信息
2:截取用户请求的资源的后缀名并作为key提取对应的头信息
3:设置响应头Content-Type的值
DispatcherServlet中:
package com.webserver.core;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
import java.io.File;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/**
* 处理请求的环节
*/
public class DispatcherServlet {
//表示resources下的static目录,实际运行编译后是target/classes下的static目录
private static File staticDir;
private static Map<String,String> mimeMappping = new HashMap<>();
static { //仅需要一份,所以放置在静态块中加载
try {
staticDir = new File(
ClientHandler.class.getClassLoader().getResource(
"./static"
).toURI()
);
} catch (URISyntaxException e) {
e.printStackTrace();
}
mimeMappping.put("html","text/html");
mimeMappping.put("css","text/css");
mimeMappping.put("js","application/javascript");
mimeMappping.put("png","image/png");
mimeMappping.put("gif","image/gif");
mimeMappping.put("jpg","image/jpeg");
}
public void service(HttpServerRequest request, HttServletResponse response){
String path = request.getUri();
System.out.println("抽象路径:" + path);
//去static目录下根据用户请求的抽象路径定位下面的文件
File file = new File(staticDir,path);
if(file.isFile()){//实际存在的文件
response.setContentFile(file);
response.addHeader("Content-Type",content_Type(file));
response.addHeader("Content-Length",file.length()+"");
}else{//1:文件不存在 2:是一个目录
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"/root/404.html");
response.setContentFile(file);
response.addHeader("Content-Type","text/html");
response.addHeader("Content-Length",file.length()+"");
}
}
/**
* 获取文件的后缀名
* @param file
* @return
*/
public String content_Type(File file){
String name = file.getName();
String lastname = name.substring(name.lastIndexOf(".")+1);
//根据后缀名提取对应的mime类型
return mimeMappping.get(lastname);
}
}
在DispatcherServlet的service方法中每次处理请求时为了设置资源对应的响应头Content-Type,我们先实例化一个Map保存类型与头信息的对应关系.但实际上这个Map没有必要每次都创建一遍,全局一份即可,每次仅需要根据资源后缀提取对应的响应头的值即可,因此将其设置为静态资源。在开发中,经常将静态资源提取出来,构成数据字典。
实现:
1:在com.webserver.http包下新建类:HttpContext
这个类用于维护所有HTTP协议规定的内容以便复用.
2:在HttpContext中定义一个静态属性:Map mimeMapping
3:在静态块中对该属性进行初始化
4:提供一个静态方法可根据资源后缀从这个Map中提取Content-Type对应的值
该方法名为:getMimeType()
5:设置响应头Content-Type的值时就可以根据资源后缀调用步骤4提供的方法
6:将原DispatcherServlet中设置响应头Content-Type和Content-Length的
工作移动到HttpServletResponse的设置响应正文方法setEntity中.
原因:一个响应中只要包含正文就应当包含说明正文信息的两个头Content-Type和
Content-Length.因此我们完全可以在设置正文的时候自动设置这两个头.
这样做的好处是将来设置完正文(调用setEntity)后无需再单独设置这两个头了.
HttpContext中:
package com.webserver.http;
import java.util.HashMap;
import java.util.Map;
/**
* 当前类用于定义所有HTTP协议规定的内容,可被复用
*/
public class HttpContext {
/**
* 资源后缀与MIME类型的对应关系
* key:资源的后缀名 (例如:png)
* value:MIME类型 (例如:image/png)
*/
private static Map<String,String> mimeMappping = new HashMap<>();
static {
initMimeMapping();
}
/**
* 初始化mimeMappping集合
*/
private static void initMimeMapping(){
mimeMappping.put("html","text/html");
mimeMappping.put("css","text/css");
mimeMappping.put("js","application/javascript");
mimeMappping.put("png","image/png");
mimeMappping.put("gif","image/gif");
mimeMappping.put("jpg","image/jpeg");
}
/**
* 提供一个公开方法给外界使用mimeMappping集合
* 根据资源后缀名获取对应的MIME类型
* @param ext
* @return 后缀名
*/
public static String getMimeType(String ext){
return mimeMappping.get(ext);
}
}
HttServletResponse中:
DispatcherServlet中:
package com.webserver.core;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpContext;
import com.webserver.http.HttpServerRequest;
import java.io.File;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
/**
* 处理请求的环节
*/
public class DispatcherServlet {
//表示resources下的static目录,实际运行编译后是target/classes下的static目录
private static File staticDir;
static { //仅需要一份,所以放置在静态块中加载
try {
staticDir = new File(
ClientHandler.class.getClassLoader().getResource(
"./static"
).toURI()
);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
public void service(HttpServerRequest request, HttServletResponse response){
String path = request.getUri();
System.out.println("抽象路径:" + path);
//去static目录下根据用户请求的抽象路径定位下面的文件
File file = new File(staticDir,path);
if(file.isFile()){//实际存在的文件
response.setContentFile(file);
}else{//1:文件不存在 2:是一个目录
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"/root/404.html");
response.setContentFile(file);
}
}
}
进一步精简代码:
HttpContext中:
HttpServerRequest中:
HttServletResponse中:
导入类型包:
编译后:(没有就手动复制进去)
HttpContext中:
/**
* 初始化mimeMappping集合
*/
private static void initMimeMapping(){
/*
java.util.Properties
该类专门用于解析.properties文件的
Properties本身是一个Map
Properties继承自Hashtable
Hashtable实现了Map接口,它是一个并发安全的Map
而HashMap不是并发安全的(注意:此处不需要保证并发安全,所以借用数据即可)
*/
Properties properties = new Properties();
//读取和当前类HttpContext在同一目录下(编译后会合并在同一个目录)的web.properties
/*
两个实际开发中常用的相对路径区别:
类名.class.getClassLoader().getResource(".")
这里的"."当前目录,指的是该类所在的包中顶级包的上一级,即:"根"
例如
HttpContext类,指定的包package com.webserver.http
那么
HttpContext.class.getClassLoader().getResource(".")
对应的目录就是com的上一级。因为IDEA中编译后,代码都在target/classes
下,所以这里就相当于是classes这个目录。
类名.class.getResource(".")
这里的"."当前目录,指的是当前类所在的目录
例如
HttpContext类,指定的包package com.webserver.http
那么
HttpContext.class.getResource(".")
对应的目录就是http目录。因为IDEA中编译后,当前类在:
target/classes/com/webserver/http下所以这里就相当于是
target/classes/com/webserver/http这个目录。
*/
try {
properties.load(
HttpContext.class.getResourceAsStream(
"./web.properties"
)
);
properties.forEach(
(k,v)->mimeMappping.put(k.toString(),v.toString())
);
} catch (IOException e) {
e.printStackTrace();
}
}
测试:
可以在HttpContext中暂时添加一个main()方法进行测试。
解决空请求问题
HTTP协议注明:为了保证服务端的健壮性,应当忽略客户端空的请求。
浏览器有时会发送空请求,即:与服务端链接后没有发送标准的HTTP请求内容,直接与服务端断开链接。此时服务端按照一问一答的处理流程在解析请求时请求行由于没有内容,在拆分后获取信息会出现数组下标越界(会报ArrayIndexOutOfBoundsException异常)。
原因在于这里:
解决:
当解析请求行时发现没有内容就对外抛出空请求异常(自定义的一个异常),并最终抛出给
ClientHandler,使其忽略后续的处理请求与发送响应的工作,直接与客户端断开来忽略本次操作。
实现:
1:在com.webserver.http包下新建自定义异常:EmptyRequestException,空请求异常(添加上所有的构造器)
2:在HttpServletRequest的解析请求行方法parseRequestLine中,当读取请求行后发现是一个
空字符串则对外抛出空请求异常并通过构造方法继续对外抛出给ClientHandler
3:ClientHandler添加一个新的catch专门捕获空请求异常,捕获后无需做任何处理,目的仅仅是
忽略处理请求和响应客户端的工作
EmptyRequestException中:
package com.webserver.http;
/**
* 空请求异常
* 当HttpServletRequest在解析请求时发现本次为空请求时会抛出异常
*/
public class EmptyRequestException extends Exception{
public EmptyRequestException() {
}
public EmptyRequestException(String message) {
super(message);
}
public EmptyRequestException(String message, Throwable cause) {
super(message, cause);
}
public EmptyRequestException(Throwable cause) {
super(cause);
}
public EmptyRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
HttpServletRequest中:
(注意这里的HttpServerRequest一直写错了,应该为HttpServletRequest)
/**
* 实例化请求对象的过程也是解析的过程
*/
public HttpServerRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1.1解析请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
/**
* 解析请求行
*/
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
//如果请求行是一个空字符串则说明本次是空请求
if(line.isEmpty()){
//对外抛出空请求异常
throw new EmptyRequestException();
}
System.out.println("请求行:"+line); //GET /index.html HTTP/1.1
//将请求行内容拆分出来并分别赋值给三个变量
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:"+method); //method:GET
System.out.println("uri:"+uri); //uri:/index.html
System.out.println("protocol:"+protocol); //protocol:HTTP/1.1
}
ClientHandler中:
为了忽略后期的处理,这里捕获异常后不做操作。
完成用户注册业务
用户注册业务的大致流程:
1:用户访问注册页面,并在页面上输入注册信息后点击注册按钮
2:数据提交发到服务端,服务端解析页面提交上来的数据
3:根据解析出来的数据进行响应的注册处理
4:给用户回复一个注册处理结果的页面(注册成功或失败)
这里涉及的知识点:
1:页面如何将用户输入的信息提交给服务端(表单form的使用)
2:服务端如何通过解析请求得到表单数据
3:DispatcherServlet如何区分请求是处理注册还是请求一个静态资源(页面,图片等)
本版本完成表单的提交以及请求的解析工作。这个工作是通用操作,无论将来处理何种业务,解析表单数据的方式都是相同的。
实现:
1:在myweb下新建用户注册页面reg.html( 在这个页面上我们学习form表单的使用)
reg.html中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<center>
<h1>用户注册</h1>
<table border="2">
<tr>
<td>用户名</td>
<td><input type="text" ></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" ></td>
</tr>
<tr>
<td>昵称</td>
<td><input type="text" ></td>
</tr>
<tr>
<td>年龄</td>
<td><input type="text" ></td>
</tr>
<tr>
<td colspan="6" align="center"><input type="button" value="注册"></td>
</tr>
</table>
</center>
</body>
</html>
效果:
在index.html中:
测试:
运行服务端后,输入http://localhost:8088/myweb/index.html
效果如下:
注意:此时点击“注册”按钮是没有任何反应的。
继续完善reg.html:
<form>
标签:表单,用于将用户在页面上输入的内容提交给服务端使用
其有两个重要的属性:
action:用于指定表单提交的位置
method:指定表单提交的方式,有两个可选值
- get:GET请求方式提交表单数据,此时表单中的数据会被拼接到URL的 抽象路径部分进行提交
- post:POST请求方式提交表单数据,表单数据会被包含在请求的消息正文中进行提交。
原则上:当表单中含有用户隐私信息(如密码),或表单中含有附件,这时要使用POST请求提交
method属性可以不显示的指定出来,不指定则默认为GET请求提交
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<center>
<h1>用户注册</h1>
<form action="/myweb/reg" method="GET">
<table border="2">
<tr>
<td>用户名</td>
<td><input type="text" ></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" ></td>
</tr>
<tr>
<td>昵称</td>
<td><input type="text" ></td>
</tr>
<tr>
<td>年龄</td>
<td><input type="text" ></td>
</tr>
<tr>
<td colspan="6" align="center">
<!-- submit是专门用于提交的按钮 -->
<input type="submit" value="注册"></td>
</tr>
</table>
</form>
</center>
</body>
</html>
此时重新启动服务端,进到注册页面中,点击“注册”。显示404,效果如下:
注意:
只有被包含在form标签中的输入框中用户输入的信息才会被提交。
并且每个输入框都必须使用name属性指定名字,否则提交是该输入框会被忽略。
继续完善reg.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<center>
<h1>用户注册</h1>
<form action="/myweb/reg" method="GET">
<table border="2">
<tr>
<td>用户名</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>昵称</td>
<td><input type="text" name="nickname"></td>
</tr>
<tr>
<td>年龄</td>
<td><input type="text" name="age"></td>
</tr>
<tr>
<td colspan="6" align="center">
<!-- submit是专门用于提交的按钮 -->
<input type="submit" value="注册"></td>
</tr>
</table>
</form>
</center>
</body>
</html>
测试:
此时重新启动服务端,进到注册页面中,输入信息(暂时先别输中文),点击“注册”。
表单提交后URL地址为:
http://localhost:8088/myweb/reg?username=fangfang&password=123456&nickname=ff&age=18
其中的抽象路径部分:
/myweb/reg?username=fanchuanqi&password=123456&nickname=chuanqi&age=22
GET形式提交表单时,URL的抽象路径就会变成上述的样子。
格式如下:
请求部分?参数部分
请求部分是由form表单中action指定的值决定的。
参数部分是由form表单中所有的输入框决定的。
- 格式为:输入框的名字=输入框上输入的信息,并用"&"分割每一个输入框内容
username=fanchuanqi
password=123456
nickname=chuanqi
age=22
2:重构HttpServletRequest的解析工作,添加对表单数据的解析。
在HttpServletRequest中:
定义属性:
在解析请求行方法下新定义一个方法:
HttpServerRequest中parseUri()方法代码:
/**
* 进一步解析uri
*/
private void parseUri(){
/*
uri是有两种情况的,1:不含有参数的 2:含有参数的
例如:
不含有参数的:/myweb/reg.html
含有参数的:
/myweb/reg?username=fanchuanqi&password=123456&nickname=chuanqi&age=22
处理方式:
1:若不含有参数,则直接将uri的值赋值给requestURI
2:若含有参数
2.1:先将uri按照"?"拆分为请求部分和参数部分
将请求部分赋值给requestURI
将参数部分赋值给queryString
2.2:再将参数部分按照"&"拆分出每一组参数
每组参数再按照"="拆分为参数名和参数值
并将参数名作为key,参数值作为value保存到parameters这个Map中
允许页面输入框空着,这种情况该参数的值为null,存入parameters即可
*/
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){//存在http://localhost:8088/myweb/reg.html?的情况
queryString = data[1];
data = queryString.split("\\&");
for (String para : data ) {
String[] paras = para.split("\\=");
parameters.put(paras[0],paras.length>1?paras[1]:null);
//存在某一个输入框没输的情况
}
}
System.out.println("requestURI:" + requestURI);
System.out.println("queryString:" + queryString);
System.out.println("parameters:" + parameters);
}
测试:
此时重新启动服务端,进到注册页面中,输入信息(暂时先别输中文),点击“注册”。
控制台结果:
注意一:
代码这样写的原因在于地址可能会出现三种情况:
1.当地址为http://localhost:8088/myweb/reg.html时
2.当地址为http://localhost:8088/myweb/reg.html?时
3.当地址为http://localhost:8088/myweb/reg?username=fangfang&password=123&nickname=ff&age=18时
注意二:
代码这样写的原因在于:可能某一个输入框没输值
在HttpServletRequest中给私有属性添加上get()方法:
拦截请求并处理,完成用户注册业务操作
之前我们已经将注册页面中表单提交的数据解析完毕并存入到HttpServletRequest对应属性中了
现在我们完成DispatcherServlet处理请求的环节,增加对业务的处理。
实现:
1:新建一个包:com.webserver.controller,这个包中保存所有将来用于处理业务的类
2:在controller包中新建处理与用户数据相关的业务类:UserController
3:在UserController中添加reg()方法,用于处理用户注册逻辑
4:在DispatcherServlet处理请求的环节中,首先我们将原来判断路径使用的请求对象中的uri换成
requestURI.
原因:uri中可能表示的路径中含有参数,而不是纯请求部分了。
5:如果请求路径是/myweb/reg,则说明这次请求是reg.html页面form表单提交的请求(action决定)
那么这个请求就是要处理注册业务,因此我们实例化UserController并调用reg方法进行处理即可。
UserController中:
package com.webserver.controller;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
/**
* MVC模型
* M:model
* V:view
* C:controller
*
* 处理与用户相关的业务操作
*/
public class UserController {
/**
* 处理用户注册
* @param request
* @param response
*/
public void reg(HttpServerRequest request, HttServletResponse response){
System.out.println("开始处理用户注册");
System.out.println("处理用户注册完毕!");
}
}
DispatcherServlet中:
如果判断是注册请求,则走请求流程:
注意:此时运行服务端到注册页面,点击“注册”后,页面白板,控制台报错。
原因在于,此时注册功能没有设置代码,响应中的响应正文还是原来的空值,不能进行读写。又在实际开发过程中,响应正文是可以为空的。
解决办法:
此时再次测试,页面还是白板,但是控制台可以输出打桩的两句话了。
UserController中:
package com.webserver.controller;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
/**
* MVC模型
* M:model
* V:view
* C:controller
*
* 处理与用户相关的业务操作
*/
public class UserController {
/**
* 处理用户注册
* @param request
* @param response
*/
public void reg(HttpServerRequest request, HttServletResponse response){
System.out.println("开始处理用户注册");
//1获取用户表单提交上来的数据
String username = request.getParameter("username");
String password = request.getParameter("password");
String nickname = request.getParameter("nickname");
String ageStr = request.getParameter("age");
//parameters是一个Map集合,里面存储了HTML页面中提取出来的数据。
//getParameter通过键得到Map中对应的值
System.out.println(username+","+password+","+nickname+","+ageStr);
System.out.println("处理用户注册完毕!");
}
}
测试注册,输入信息,点击“注册”:
设计一个类,专门用来保存用户信息:
设置序列号,私有属性,构造器,提供get/set方法(注意:序列号不用提供get/set方法),toString()
package com.webserver.entity;
import java.io.Serializable;
/**
* 使用当前类的每一个实例表示一个用户信息
*/
public class User implements Serializable {
static final long serialVersionUID = 1L;
private String username;
private String password;
private String nickname;
private int age;
/**
* 构造器
* @param username
* @param password
* @param nickname
* @param age
*/
public User(String username, String password, String nickname, int age) {
this.username = username;
this.password = password;
this.nickname = nickname;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", nickname='" + nickname + '\'' +
", age=" + age +
'}';
}
}
在UserController中:
package com.webserver.controller;
import com.webserver.entity.User;
import com.webserver.http.HttServletResponse;
import com.webserver.http.HttpServerRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/**
* MVC模型
* M:model
* V:view
* C:controller
*
* 处理与用户相关的业务操作
*/
public class UserController {
/**
* 该目录用于保存所有用户信息
*/
private static File USER_DIR = new File("./users");
static{
if(!USER_DIR.exists()){
USER_DIR.mkdirs();
}
}
/**
* 处理用户注册
* @param request
* @param response
*/
public void reg(HttpServerRequest request, HttServletResponse response){
System.out.println("开始处理用户注册");
//1获取用户表单提交上来的数据
String username = request.getParameter("username");
String password = request.getParameter("password");
String nickname = request.getParameter("nickname");
String ageStr = request.getParameter("age");
int age = Integer.parseInt(ageStr);
//parameters是一个Map集合,里面存储了HTML页面中提取出来的数据。
//getParameter通过键得到Map中对应的值
System.out.println(username+","+password+","+nickname+","+ageStr);
//2将用户信息以一个User实例形式表示,并序列化到文件中
User user = new User(username,password,nickname,age);
//将该用户信息以用户名.obj的形式保存到users目录中
File userFile = new File(USER_DIR,username+".obj");
try(
FileOutputStream fos = new FileOutputStream(userFile);
ObjectOutputStream oos = new ObjectOutputStream(fos);
){
oos.writeObject(user);
//注册成功了
}catch (IOException e){
}
System.out.println("处理用户注册完毕!");
}
}
新建一个html页面
reg_success.html中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册成功</title>
</head>
<body>
<center>
<h1>恭喜您,注册成功了!</h1>
</center>
</body>
</html>
在UserController中:(注意这里添加的代码是从DispatcherServlet中复制的)
再添加上设置响应正文的代码:
启动服务端进行测试:
在idea中可以看到users目录中已经出现以用户名命名的文件了。
注意:后缀名记得打点(.obj)