一、重构
1、什么是重构
重构就是通过调整代码结构和设计来改善代码质量和可维护性的过程,将复杂的内容用方法分隔开。
重构的目的是解决代码:
可读性差、代码冗余、缺乏可扩展性、低效率的问题
2、重构原生Web服务框架
服务端 ServerBootApplication:
/**
* @document: 接收客户端的HTTP请求
* @Author:SmallG
* @CreateTime:2023/8/16+10:43
*/
public class ServerBootApplication {
private ServerSocket serverSocket;
public void start() {
try {
// 创建ServerSocket对象,监听8088端口
serverSocket = new ServerSocket(8088);
while (true) {
System.out.println("等待客户端连接...");
// 等待客户端连接
Socket socket = serverSocket.accept();
//创建线程对象
ClientHandler handler = new ClientHandler(socket);
//启动线程
Thread t = new Thread(handler);
t.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ServerBootApplication application = new ServerBootApplication();
//启动服务器
application.start();
}
}
处理HTTP请求逻辑:
/**
* @document: 处理HTTP请求的操作逻辑
* @Author:SmallG
* @CreateTime:2023/8/16+17:45
*/
public class HttpServletRequest {
private Socket socket; //保存客户端和服务端之间的网络连接
private String method; //保持HTTP请求的方法
private String uri; //保存HTTP的请求路径
private String protocol; //客户端请求的协议版本
private HashMap<String, String> headers = new HashMap<>(); //存储请求头的集合
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//解析请求行
parseRequestLine();
//解析请求头
parseHeaders();
}
/**
* 解析请求行
*
* @throws IOException
*/
private void parseRequestLine() throws IOException {
String requestLine = readLine();
String[] lines = requestLine.split("\s");
method = lines[0];
uri = lines[1];
protocol = lines[2];
System.out.println("解析请求行:" + requestLine);
System.out.println("请求方法:" + method);
System.out.println("请求uri:" + uri);
System.out.println("协议版本:" + protocol);
}
/**
* 解析请求头,把结果存储到Map集合当中
*/
private void parseHeaders() throws IOException {
while (true) {
String line = readLine();
//解析到空行结束
if (line.isEmpty()) {
break;
}
System.out.println("解析到的请求头:" + line);
String[] parts = line.split(":\s");
headers.put(parts[0], parts[1]);
}
System.out.println("所有请求头:" + headers);
}
/**
* 查询请求头
*/
public String getHeader(String name) {
return headers.get(name);
}
/**
* 读取请求报文请求行信息
*/
public String readLine() throws IOException {
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
char current = 0;
char previous = 0;
int b = 0;
while ((b = in.read()) != -1) {
current = (char) b;
if (previous == '\r' && current == '\n') {
break;
} else if (current != '\r' && current != '\n') {
builder.append(current);
}
previous = current;
}
return builder.toString();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
}
解析请求和发送响应:
/**
* @document: 解析请求和发送响应
* @Author:SmallG
* @CreateTime:2023/8/16+14:48
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//解析请求
HttpServletRequest httpServletRequest = new HttpServletRequest(socket);
String uri = httpServletRequest.getUri();
//往客户端发送响应
//加载静态资源,通过类的加载器进行加载到target/classes目录
File root = new File(ClientHandler.class
.getClassLoader().getResource(".").toURI());
//加载target/classes/static目录
File staticDir = new File(root, "static");
//加载文件target/classes/static目录中的文件
File file = new File(staticDir, uri);
//读取文件的全部内容
byte[] bytes = new byte[(int) file.length()];
FileInputStream fis = new FileInputStream(file);
fis.read(bytes); //读取文件中的全部内容
//获取输出流
OutputStream out = socket.getOutputStream();
//发送响应
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.UTF_8)); //响应行
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8"
.getBytes(StandardCharsets.UTF_8));
out.write('\r');
out.write('\n');
out.write(("Content-Length: " + bytes.length)
.getBytes(StandardCharsets.UTF_8));
out.write('\r');
out.write('\n');
out.write('\r');
out.write('\n');
out.write(bytes); //响应体
fis.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
try {
// 关闭客户端
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、重构响应
- socket:一个socket实例变量,用于表示客户端连接的套接字
- statusCode:表示HTTP状态码,默认值为200
- statusReason:表示HTTP状态描述,默认值为"OK"
- contentFile:表示响应正文对应的实体文件
/**
* @document: 封装HTTP响应的逻辑
* @Author:SmallG
* @CreateTime:2023/8/17+9:21
*/
public class HttpServletResponse {
private Socket socket; //和客户端建立连接
//响应的状态行相关的信息
private int statusCode = 200; //响应的状态码
private String statusReason = "OK"; //响应的状态描述
//响应头相关的信息
//响应体相关的信息
private File contentFile; //响应正文对应的实体文件(HTML等)
/**
* 构造方法,设置socket
*/
public HttpServletResponse(Socket socket) {
this.socket = socket;
}
/**
* 将HTTP响应发送给客户端
*/
public void send() throws IOException {
//定义响应状态行
String statusLine = "HTTP/1.0 " + statusCode + " " + statusReason;
//发送状态行
println(statusLine);
System.out.println("发送了状态行:" + statusLine);
//发送响应头
println("Content-Type: text/html; charset=utf-8");
println("Content-Length: " + contentFile.length());//响应体长度
System.out.println("发送了响应头: " + "Content-Length: " + contentFile.length());
//发送空行
println("");
//发送响应体
FileInputStream in = new FileInputStream(contentFile); //加载要响应的文件
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[8 * 1024]; //一次写8K
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
/**
* 发送一行信息到网络流中
*
* @param line
*/
private void println(String line) throws IOException {
// 获取socket的输出流
//将异常抛出,抛给方法的调用者
OutputStream out = socket.getOutputStream();
//把一行数据信息转换为字节数组
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
//发送一行信息
out.write(data);
out.write('\r'); //发送回车符
out.write('\n'); //发送换行符
}
/**
* 设置响应的状态码
*
* @param statusCode
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
/**
* 设置响应的状态描述
*
* @param statusReason
*/
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
/**
* 设置响应体
*
* @param contentFile
*/
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
}
}
/**
* @document: 解析请求和发送响应
* @Author:SmallG
* @CreateTime:2023/8/16+14:48
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//解析请求
HttpServletRequest httpServletRequest = new HttpServletRequest(socket);
String uri = httpServletRequest.getUri();
//往客户端发送响应
//加载静态资源,通过类的加载器进行加载到target/classes目录
File root = new File(ClientHandler.class
.getClassLoader().getResource(".").toURI());
//加载target/classes/static目录
File staticDir = new File(root, "static");
//加载文件target/classes/static目录中的文件
File file = new File(staticDir, uri);
//2、发送响应
HttpServletResponse response = new HttpServletResponse(socket);
response.setContentFile(file); // 设置响应文件
response.send();
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
} finally {
try {
// 关闭客户端
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
**HTTP响应状态码
- 200 OK:请求已成功
- 201 Created:请求已经被实现,而且有一个新的资源已经依据请求的需要而建立
- 3XX:(重定向状态码)表示需要客户端执行进一步的操作才能完成请求
- 401 Unauthorized:请求需要用户验证,无法通过验证
- 404 Not Found :服务器无法找到请求的资源
- 500 Internal Server Error:服务器遇到了一个意外的情况,无法完成请求
解决404问题:
//判断检查文件是否存在,判断是不是文件
if (file.isFile()) {
//文件存在,发送响应文件
response.setContentFile(file); // 设置响应文件
} else {
//文件不存在,发送404文件
response.setStatusCode(404); //设置状态码
response.setStatusReason("Not Found"); //设置状态描述
File file404 = new File(staticDir, "404.html"); //执行404文件
response.setContentFile(file404); //设置响应的文件为404文件
}
重构请求过程
- root :代表当前classpath的根目录,是资源查找起始位置
- staticDir :静态资源的位置,静态网页和图片都存储在这个位置
/**
* @document: 封装请求处理过程逻辑
* @Author:SmallG
* @CreateTime:2023/8/17+11:10
*/
public class DispatcherServlet {
private static File root; //项目的根目录(classpath)
private static File staticDir; //静态文件所在的目录(html,css,js,图片,其他配置文件)
static {
try {
//加载项目的根目录
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//加载静态文件目录
staticDir = new File(root, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* 处理请求逻辑
*
* @param request 请求
* @param response 响应
*/
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
String uri = request.getUri(); //资源路径
//定义文件
File file = new File(staticDir, uri);
if (file.isFile()) {
//文件存在 正常发送响应
response.setContentFile(file);
} else {
//文件不存在 发送404
response.setStatusCode(404); //设置状态码
response.setStatusReason("Not Found"); //设置状态描述
File file404 = new File(staticDir, "404.html"); //执行404文件
response.setContentFile(file404); //设置响应的文件为404文件
}
response.send();
}
}
/**
* @document: 解析请求和发送响应
* @Author:SmallG
* @CreateTime:2023/8/16+14:48
*/
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);
HttpServletResponse response = new HttpServletResponse(socket);
//2、处理请求
DispatcherServlet servlet = new DispatcherServlet();
servlet.service(request, response);
//3、发送响应
response.send();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
// 关闭客户端
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
4、单例模式
(1)设计模式与单例模式
设计模式描述了一组经过测试和证明的解决方案,可以用来解决面向对象编程中的各种问题。
单例模式保证了一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实力,在单例模式中,通常将该类的构造函数私有化,防止外部直接创建实例,而通过一个静态方法或者变量来获取唯一的实例。
(2)饿汉式单例模式:
在类加载时就进行实例化,保证了线程安全性,但是可能存在资源浪费的问题,因为该实例即使在系统启动时就被创建出来,但可能在系统运行过程中一直没有被使用到。
/**
* @document: 单例模式的第一种实现方式:饿汉模式
* @Author:SmallG
* @CreateTime:2023/8/17+14:05
*/
public class Singleton {
//1、构造方法私有化
private Singleton() {
}
//2、定义静态变量,保存当前类的对象
private static Singleton instance = new Singleton();
//3、提供公有的静态方法,返回当前类的对象
public static Singleton getInstance(){
return instance;
}
}
(3)懒汉式单例模式
在需要使用实例对象时才进行创建,在开发过程中优先采用懒汉式。
/**
* @document: 单例模式:懒汉式,减少内存消耗
* @Author:SmallG
* @CreateTime:2023/8/17+14:12
*/
public class Singleton02 {
//1、将构造方法私有化,防止外部通过new创建实例
private Singleton02() {
}
//2、定义静态变量,保存当前类的对象
private static Singleton02 instance;
//3、提供公有的静态方法,返回当前类的对象
public static Singleton02 getInstance() {
//如果当前类的对象没有被创建,则创建该类的对象
if (instance == null) {
instance = new Singleton02();
}
return instance;
}
}
什么时候用单例模式:
1、类中有没有属性,如果没有属性,这个类可以设计成单例模式。
2、如果有属性,但是属性是单例的可以设置为单例模式。
5、Servlet概述
servlet是运行在web服务器上的java程序,用于接受和处理来自Web客户端的请求
DispatcherServlet就是Servlet的原型
Servlet处理流程
- Web客户端(通常是浏览器)向Web服务器发送HTTP请求;
- Web服务器接收到请求后,根据URL的映射关系,将请求转发到相应的Servlet进行处理;
- Servlet接收到请求后,根据请求的参数和业务逻辑进行处理,并生成相应的响应数据;
- Servlet将生成的响应数据发送回Web服务器;
- Web服务器将响应数据返回给Web客户端,完成整个请求-响应过程。
二、处理多种内容类型
1、内容类型Content-Type
Content-Type是HTTP协议头中的一个字段,用于描述HTTP请求或响应中所传输的实体数据的媒体类型(MIME类型)
Content-Type一般由两部分组成:媒体类型和字符集。其中,媒体类型指的是数据的格式类型,常见的媒体类型有text、image、audio(音频)、video(视频)等,字符集则是指所传输的数据所采用的字符编码格式,常见的字符集有UTF-8、GBK等。
如果想要显示图片,需要修改响应头内容:
/**
* @document: 封装HTTP响应的逻辑
* @Author:SmallG
* @CreateTime:2023/8/17+9:21
*/
public class HttpServletResponse {
private Socket socket; //和客户端建立连接
//响应的状态行相关的信息
private int statusCode = 200; //响应的状态码
private String statusReason = "OK"; //响应的状态描述
//响应头相关的信息
//响应体相关的信息
private File contentFile; //响应正文对应的实体文件(HTML等)
/**
* 构造方法,设置socket
*/
public HttpServletResponse(Socket socket) {
this.socket = socket;
}
/**
* 将HTTP响应发送给客户端
*/
public void send() throws IOException {
//定义响应状态行
String statusLine = "HTTP/1.1 " + statusCode + " " + statusReason;
//发送状态行
println(statusLine);
System.out.println("发送状态行:" + statusLine);
//发送响应头
if (contentFile.getName().endsWith(".html")){
println("Content-Type: text/html; charset=utf-8");
}else if(contentFile.getName().endsWith(".png")){
println("Content-Type: image/png");
}
println("Content-Length: " + contentFile.length());//响应体长度
System.out.println("发送响应头: " + "Content-Length: " + contentFile.length());
//发送空行
println("");
//发送响应体
FileInputStream in = new FileInputStream(contentFile); //加载要响应的文件
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[8 * 1024]; //一次写8K
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
/**
* 发送一行信息到网络流中
*
* @param line
*/
private void println(String line) throws IOException {
// 获取socket的输出流
//将异常抛出,抛给方法的调用者
OutputStream out = socket.getOutputStream();
//把一行数据信息转换为字节数组
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
//发送一行信息
out.write(data);
out.write('\r'); //发送回车符
out.write('\n'); //发送换行符
}
/**
* 设置响应的状态码
*
* @param statusCode
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
/**
* 设置响应的状态描述
*
* @param statusReason
*/
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
/**
* 设置响应体
*
* @param contentFile
*/
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
}
}
用Apache Tika来识别MIME:
/**
* @document: 封装HTTP响应的逻辑
* @Author:SmallG
* @CreateTime:2023/8/17+9:21
*/
public class HttpServletResponse {
private Socket socket; //和客户端建立连接
//响应的状态行相关的信息
private int statusCode = 200; //响应的状态码
private String statusReason = "OK"; //响应的状态描述
//响应头相关的信息
//响应体相关的信息
private File contentFile; //响应正文对应的实体文件(HTML等)
//定义响应头信息key是响应头名称,value是响应头的值
private HashMap<String, String> headers = new HashMap<>();
/**
* 识别媒体类型
*/
private Tika tika = new Tika();
/**
* 构造方法,设置socket
*/
public HttpServletResponse(Socket socket) {
this.socket = socket;
}
/**
* 保存响应头信息,添加自定义的响应头
*/
public void addHeader(String name, String value) {
headers.put(name, value);
}
/**
* 单独发送状态行
*/
private void sendStatusLine() throws IOException {
//定义响应状态行
String statusLine = "HTTP/1.1 " + statusCode + " " + statusReason;
//发送状态行
println(statusLine);
System.out.println("发送状态行:" + statusLine);
}
/**
* 发送响应头
*/
private void sendHeaders() throws IOException {
//遍历响应头集合,发送每个响应头
for (Map.Entry<String, String> entry : headers.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
println(name + ": " + value);
System.out.println("发送响应: " + name + ": " + value);
}
//发送空行
println("");
}
/**
* 发送响应体
*/
private void sendResponseBody() throws IOException {
FileInputStream in = new FileInputStream(contentFile); //加载要响应的文件
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[8 * 1024]; //一次写8K
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
/**
* 将HTTP响应发送给客户端
*/
public void send() throws IOException {
//发送状态行
sendStatusLine();
//发送响应头
sendHeaders();
//发送响应体
sendResponseBody();
}
/**
* 发送一行信息到网络流中
*
* @param line
*/
private void println(String line) throws IOException {
// 获取socket的输出流
//将异常抛出,抛给方法的调用者
OutputStream out = socket.getOutputStream();
//把一行数据信息转换为字节数组
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
//发送一行信息
out.write(data);
out.write('\r'); //发送回车符
out.write('\n'); //发送换行符
}
/**
* 设置响应的状态码
*
* @param statusCode
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
/**
* 设置响应的状态描述
*
* @param statusReason
*/
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
/**
* 设置响应体
*
* @param contentFile
*/
public void setContentFile(File contentFile) throws IOException {
this.contentFile = contentFile;
//添加响应头
//使用tika获取媒体类型
addHeader("Content-Type", tika.detect(contentFile));
addHeader("Content-Length", String.valueOf(contentFile.length()));
addHeader("File-Name", contentFile.getName());
}
}
2、HTML基础
(1)常用标签
<h1> 到 <h6>:用于定义标题
<p>:用于定义段落
<a>:用于创建超链接
<br>:用于插入换行符
<ul> 和 <ol>:用于创建无序列表和有序列表
<li>:用于定义列表中的项目
<hr>:用于插入水平线
<strong> 和 <em>:用于强调文本,<strong> 会使文本加粗显示,而 <em> 会使文本变成斜体
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文本标签</title>
</head>
<body>
<!--h标题标签-->
<h1>一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>
<p>段落标签</p>
<p>第一段内容</p>
<p>第二段内容</p>
<a href="https://www.baidu.com">超链接标签</a>
<ul>
<li>无序列表</li>
<li>无序列表</li>
</ul>
<ol>
<li>有序列表</li>
<li>有序列表</li>
</ol>
<hr/>
<p>
第一行<br>
第二行<br>
第三行<br>
<strong>加粗文本</strong><br>
<em>斜体文本</em>
</p>
</body>
</html>
(2)HTML显示图片
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>显示图片</h1>
<img src="img.png">
</body>
</html>
(3)HTML显示表单
<input>:输入框,默认为单行文本框,可以通过设置其type属性的不同取值定义不同类型的输入框,比如密码框、单选按钮、多选按钮等
<select>:选择框,默认为下拉列表
<button>:按钮,其type属性取值为 submit时表示这个按钮会触发表单提交事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>表单标签</title>
</head>
<body>
<form>
注册
<br>
<label>姓名:<input type="text" name="name" placeholder="请输入您的姓名"></label>
<br>
<label>籍贯:
<select name="hometown">
<option>--请选择--</option>
<option>河北</option>
<option>北京</option>
<option>上海</option>
<option>天津</option>
</select>
</label><br>
<label>
性别:
<input type="radio" name="gender" value="male" checked>男
<input type="radio" name="gender" value="female">女
</label><br>
<label>
爱好:
<input type="checkbox" name="interest" value="football">足球
<input type="checkbox" name="interest" value="basketball">篮球
<input type="checkbox" name="interest" value="pingpong">乒乓球
</label><br>
<button type="submit">提交</button>
</form>
</body>
</html>
三、动态处理表单数据
1、处理GET请求参数
表单GET请求参数是指在HTML表单中通过GET方法提交表单数据时所附带的参数信息。这些信息要通过表单,传给后台。
//定义一个集合存储所有的键值对
private HashMap<String, String> parameters = new HashMap<>(); //存储所有的请求参数
/**
* 解析请求参数
*/
private void parseUri() {
// 使用问号进行分隔请求路径,?是正则表达式的标识符需要转义字符进行截取
String[] parts = uri.split("\\?");
requestUri = parts[0];
//得到请求路径
String queryString = parts[1];
//使用与符号分割所有的请求参数
parts = queryString.split("&");
for (String part : parts) {
//使用=进行分割
String[] param = part.split("=");
parameters.put(param[0], param[1]);
}
System.out.println("所有请求参数:"+parameters);
}
解析GET请求参数
- 获取URL中的查询字符串部分:对于HTTP GET请求,查询字符串部分位于URL中的问号字符(?)之后,可以通过解析URI来获取该部分的内容。
- 解析查询字符串中的键值对:查询字符串中的键值对通常使用等号(=)连接,键值对之间使用和号(&)连接。可以通过字符串分割和循环遍历来解析键值对。
- 对键值对的值进行解码:由于URL中的查询字符串部分需要进行URL编码以确保参数值的正确性和安全性,因此在解析时需要对键值对的值进行解码。
使用Java API对URL编码数据进行解码
URLDecoder.decode()方法
- 第一个参数为需要解码的URL参数
- 第二个参数为字符编码方式:通常使用UTF-8
- 输出解码后的结果
/**
* @document: URL解码
* @Author:SmallG
* @CreateTime:2023/8/18+9:20
*/
public class URLDecoderDemo {
public static void main(String[] args) {
try {
//%E5%BC%A0
String encodedURL = "%E5%BC%A6";
String decodedURL = URLDecoder.decode(encodedURL, "utf-8");
System.out.println("源码:" + encodedURL);
System.out.println("解码:" + decodedURL);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
2、动态响应页面内容
动态响应页面是一种动态生成的Web页面,其内容可以根据用户的请求和其他动态变化的数据进行实时生成和更新。
在HttpServletResponse响应中添加动态内容响应的数组content
//定义保存响应正文的数组 实现动态响应一个网页
private byte[] content;
/**
* 设置响应正文内容 动态网页内容
*/
public void setContent(String html) {
//把动态网页的内容转化为字节数组
content = html.getBytes(StandardCharsets.UTF_8);
//设置响应头信息
addHeader("Content-Type", "text/html; charset=utf-8");
addHeader("Content-Length", Integer.toString(content.length));
}
/**
* 发送响应体
*/
private void sendResponseBody() throws IOException {
OutputStream out = socket.getOutputStream();
//如果响应的静态文件不存在,则发送动态网页
if (contentFile == null) {
out.write(content);
System.out.println("发送动态网页信息");
} else {
//加载要响应的文件 发送本地的静态资源
FileInputStream in = new FileInputStream(contentFile);
byte[] buf = new byte[8 * 1024]; //一次写8K
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
System.out.println("发送静态网页信息");
}
}
在处理请求过程逻辑的时候需要修改内容:
/**
* @document: 封装请求处理过程逻辑
* 单例模式封装,当前类
* @Author:SmallG
* @CreateTime:2023/8/17+11:10
*/
public class DispatcherServlet {
private static File root; //项目的根目录(classpath)
private static File staticDir; //静态文件所在的目录(html,css,js,图片,其他配置文件)
//提供静态的一个对象保存当前对象
private static DispatcherServlet instance;
private DispatcherServlet() {
}
static {
try {
//加载项目的根目录
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//加载静态文件目录
staticDir = new File(root, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* 为了考虑线程安全问题加上同步锁,获取当前对象的方法
*
* @return
*/
public static synchronized DispatcherServlet getInstance() {
if (instance == null) {
instance = new DispatcherServlet();
}
return instance;
}
/**
* 处理请求逻辑
*
* @param request 请求
* @param response 响应
*/
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
String uri = request.getRequestUri(); //资源路径
//如果请求是登录操作 做动态网页拼接 ,否则当一个文件去处理
if (uri.equals("/accounts/login")) {
//获取请求参数
String username = request.getParameter("username");
//动态拼接网页
String html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>欢迎登录</title>
</head>
<body>
<h1>欢迎""" + username + """
</h1>
</body>
</html>
""";
response.setContent(html);
} else {
//定义文件
File file = new File(staticDir, uri);
if (file.isFile()) {
//文件存在 正常发送响应
response.setContentFile(file);
} else {
//文件不存在 发送404
response.setStatusCode(404); //设置状态码
response.setStatusReason("Not Found"); //设置状态描述
File file404 = new File(staticDir, "404.html"); //执行404文件
response.setContentFile(file404); //设置响应的文件为404文件
}
}
}
}
3、处理POST请求参数
用于向服务器提交数据,在POST请求中,请求的数据被包含在请求体中,而不是像GET请求一样在URL中传递数据。因此,POST请求相对于GET请求来说更加安全,因为请求的参数不会明文显示在URL上。此外,POST请求没有URL长度限制,可以传输较大的数据。
/**
* 解析POST请求
*/
private void parseContent() throws IOException {
//判断请求方法是不是POST 同时是不是通过表单提交
if (method.equals("POST") && getHeader("Content-Type").equals("application/x-www-form-urlencoded")) {
//获取表单的数据长度(请求体长度)
int length = Integer.parseInt(getHeader("Content-Length"));
//读取表单数据
InputStream in = socket.getInputStream();
byte[] bytes = new byte[length];
in.read(bytes);
// 把读取到的表单数据转换为字符串 转换的结果需要用与符号进行分割
String formData = new String(bytes, StandardCharsets.ISO_8859_1);
//解析表单数据 使用与符号分割字符串 把结果存储到parameters集合中
String[] parts = formData.split("&");
for (String part : parts) {
//使用等号进行拆分
String[] param = part.split("=");
String name = param[0];
String value = URLDecoder.decode(param[1], "UTF-8");
parameters.put(name, value);
}
System.out.println("解析了参数:" + parameters);
}
}
四、练习
1 文字描述GET和POST请求参数传递区别?
提示:在请求消息中的位置区别、参数长度区别、安全性区别。
- GET请求通过URL传递参数,参数以键值对的形式出现在URL的末尾,参数之间使用&符号进行分隔,POST请求通过请求体传递参数,参数以键值对的形式出现在请求体中。
- GET请求的参数长度受到浏览器和服务器的限制,POST请求没有限制。
- GET不安全,POST安全。
2 编写两种单例模式案例
要求:类名为Sky。
/**
* @document: 单例模式:饿汉模式
* @Author:SmallG
* @CreateTime:2023/8/17+18:36
*/
public class Sky01 {
//1、构造方法私有化
private Sky01() {
}
//2、定义静态变量,保存当前类的对象
private static Sky01 instance = new Sky01();
//3、提供公有的静态方法,返回当前类的对象
public static Sky01 getInstance() {
return instance;
}
}
/**
* @document: 单例模式:懒汉式
* @Author:SmallG
* @CreateTime:2023/8/17+18:39
*/
public class Sky02 {
//1、构造方法私有化
private Sky02() {
}
//2、定义静态变量,保持当前类的对象
private static Sky02 instance;
//3、提供公有的静态方法,返回当前类的对象
public static Sky02 getInstance() {
if (instance == null) {
instance = new Sky02();
}
return instance;
}
}
3 列出String的5个常用方法,并且用实例说明用途?
要求:不能包含从Object继承的方法。
/**
* @document: 列出String的5个常用方法,并且用实例说明用途
* @Author:SmallG
* @CreateTime:2023/8/17+18:49
*/
public class homework3 {
public static void main(String[] args) {
//length()用于返回字符串的长度
String s1 = "SmallG";
System.out.println(s1.length()); //6
//split()用于字符串的分割
String s2 = "Tom,Jerry,Spike";
String[] split = s2.split(",");
System.out.println(Arrays.toString(split)); //[Tom, Jerry, Spike]
//indexOf()用于查询字符串中第一次出现子字符串的位置
String s3 = "SmallG";
System.out.println(s3.indexOf("ll")); //3
//chatAt()用于返回指定位置的字符
System.out.println(s3.charAt(5)); //G
//substring()用于分割字符串
System.out.println(s2.substring(4)); //Jerry,Spike
}
}
4 编写原生Web服务框架。
要求:
1、使用多线程处理客户端的请求;
2、将请求解析封装到HttpServletRequest对象,能够解析GET和POST请求行、请求头、请求参数;
3、将响应处理封装到HttpServletResponse对象,能过正确发送状态行、响应头、静态文件、动态内容、能过正确处理文件媒体类型;
4、将请求处理过程封装到DispatcherServlet,能过处理动态页面、静态文件、404等错误;DispatcherServlet采用单例模式。
ServerBootApplication类用于接收客户端的HTTP请求:
/**
* @document: 接收客户端的HTTP请求
* @Author:SmallG
* @CreateTime:2023/8/16+10:43
*/
public class ServerBootApplication {
private ServerSocket serverSocket;
public void start() {
try {
// 创建ServerSocket对象,监听8088端口
serverSocket = new ServerSocket(8088);
while (true) {
System.out.println("等待客户端连接...");
// 等待客户端连接
Socket socket = serverSocket.accept();
//创建线程对象
ClientHandler handler = new ClientHandler(socket);
//启动线程
Thread t = new Thread(handler);
t.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ServerBootApplication application = new ServerBootApplication();
//启动服务器
application.start();
}
}
HttpServletRequest处理请求逻辑:
/**
* @document: 处理HTTP请求的操作逻辑
* @Author:SmallG
* @CreateTime:2023/8/16+17:45
*/
public class HttpServletRequest {
private Socket socket; //保存客户端和服务端之间的网络连接
private String method; //保持HTTP请求的方法
private String uri; //保存HTTP的请求路径
private String protocol; //客户端请求的协议版本
private String requestUri; //只包含请求路径
private HashMap<String, String> headers = new HashMap<>(); //存储请求头的集合
//定义一个集合存储所有的键值对
private HashMap<String, String> parameters = new HashMap<>(); //存储所有的请求参数
public HttpServletRequest(Socket socket) throws IOException, BadRequestException {
this.socket = socket;
//解析请求行
parseRequestLine();
//解析请求头
parseHeaders();
}
/**
* 解析请求参数
*/
private void parseUri() {
// 使用问号进行分隔请求路径,?是正则表达式的标识符需要转义字符进行截取
String[] parts = uri.split("\\?");
requestUri = parts[0];
//得到请求路径
String queryString = parts[1];
//使用与符号分割所有的请求参数
parts = queryString.split("&");
for (String part : parts) {
//使用=进行分割
String[] param = part.split("=");
parameters.put(param[0], param[1]);
}
System.out.println("所有请求参数:" + parameters);
}
/**
* 解析请求行
*
* @throws IOException
*/
private void parseRequestLine() throws IOException, BadRequestException {
String requestLine = readLine();
// 根据HTTP协议的特点描述,readLine读到的可能是空行
int n = 0;
while (requestLine.isEmpty()) {
//读到了空行,跳过当前行
requestLine = readLine(); //重新读取下一行
if (n++ == 5) {
throw new BadRequestException("有过多的空行请求!");
}
}
//定义正则表达式,匹配正确的请求行
String regex = "^(GET|POST|PUT|DELETE|HEAD|OPTIONS)\\s+([^\\s]+)\\s+HTTP/1\\.1$";
if (!requestLine.matches(regex)) {
throw new BadRequestException("错误的请求行!");
}
String[] lines = requestLine.split("\s");
method = lines[0];
uri = lines[1];
protocol = lines[2];
System.out.println("解析请求行:" + requestLine);
System.out.println("请求方法:" + method);
System.out.println("请求uri:" + uri);
System.out.println("协议版本:" + protocol);
//有参数才解析,有问号说明有参数
if (uri.contains("?")) {
parseUri();
} else {
requestUri = uri;
}
}
/**
* 解析请求头,把结果存储到Map集合当中
*/
private void parseHeaders() throws IOException {
while (true) {
String line = readLine();
//解析到空行结束
if (line.isEmpty()) {
break;
}
System.out.println("解析到的请求头:" + line);
String[] parts = line.split(":\s");
headers.put(parts[0], parts[1]);
}
System.out.println("所有请求头:" + headers);
}
/**
* 查询请求头
*/
public String getHeader(String name) {
return headers.get(name);
}
/**
* 读取请求报文请求行信息
*/
public String readLine() throws IOException {
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();
char current = 0;
char previous = 0;
int b = 0;
while ((b = in.read()) != -1) {
current = (char) b;
if (previous == '\r' && current == '\n') {
break;
} else if (current != '\r' && current != '\n') {
builder.append(current);
}
previous = current;
}
return builder.toString();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
public String getRequestUri() {
return requestUri;
}
}
HttpServletResponse处理响应逻辑:
/**
* @document: 封装HTTP响应的逻辑
* @Author:SmallG
* @CreateTime:2023/8/17+9:21
*/
public class HttpServletResponse {
private Socket socket; //和客户端建立连接
//响应的状态行相关的信息
private int statusCode = 200; //响应的状态码
private String statusReason = "OK"; //响应的状态描述
//响应头相关的信息
//响应体相关的信息
private File contentFile; //响应正文对应的实体文件(HTML等)
//定义响应头信息key是响应头名称,value是响应头的值
private HashMap<String, String> headers = new HashMap<>();
/**
* 识别媒体类型
*/
private Tika tika = new Tika();
/**
* 构造方法,设置socket
*/
public HttpServletResponse(Socket socket) {
this.socket = socket;
}
/**
* 保存响应头信息,添加自定义的响应头
*/
public void addHeader(String name, String value) {
headers.put(name, value);
}
/**
* 单独发送状态行
*/
private void sendStatusLine() throws IOException {
//定义响应状态行
String statusLine = "HTTP/1.1 " + statusCode + " " + statusReason;
//发送状态行
println(statusLine);
System.out.println("发送状态行:" + statusLine);
}
/**
* 发送响应头
*/
private void sendHeaders() throws IOException {
//遍历响应头集合,发送每个响应头
for (Map.Entry<String, String> entry : headers.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
println(name + ": " + value);
System.out.println("发送响应: " + name + ": " + value);
}
//发送空行
println("");
}
/**
* 发送响应体
*/
private void sendResponseBody() throws IOException {
FileInputStream in = new FileInputStream(contentFile); //加载要响应的文件
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[8 * 1024]; //一次写8K
int len;
while ((len = in.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
/**
* 将HTTP响应发送给客户端
*/
public void send() throws IOException {
//发送状态行
sendStatusLine();
//发送响应头
sendHeaders();
//发送响应体
sendResponseBody();
}
/**
* 发送一行信息到网络流中
*
* @param line
*/
private void println(String line) throws IOException {
// 获取socket的输出流
//将异常抛出,抛给方法的调用者
OutputStream out = socket.getOutputStream();
//把一行数据信息转换为字节数组
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
//发送一行信息
out.write(data);
out.write('\r'); //发送回车符
out.write('\n'); //发送换行符
}
/**
* 设置响应的状态码
*
* @param statusCode
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
/**
* 设置响应的状态描述
*
* @param statusReason
*/
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
/**
* 设置响应体
*
* @param contentFile
*/
public void setContentFile(File contentFile) throws IOException {
this.contentFile = contentFile;
//添加响应头
//使用tika获取媒体类型
addHeader("Content-Type", tika.detect(contentFile));
addHeader("Content-Length", String.valueOf(contentFile.length()));
addHeader("File-Name", contentFile.getName());
}
}
DispatcherServlet封装请求过程处理逻辑:
/**
* @document: 封装请求处理过程逻辑
* 单例模式封装,当前类
* @Author:SmallG
* @CreateTime:2023/8/17+11:10
*/
public class DispatcherServlet {
private static File root; //项目的根目录(classpath)
private static File staticDir; //静态文件所在的目录(html,css,js,图片,其他配置文件)
//提供静态的一个对象保存当前对象
private static DispatcherServlet instance;
private DispatcherServlet() {
}
static {
try {
//加载项目的根目录
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//加载静态文件目录
staticDir = new File(root, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* 为了考虑线程安全问题加上同步锁,获取当前对象的方法
* @return
*/
public static synchronized DispatcherServlet getInstance() {
if (instance == null) {
instance = new DispatcherServlet();
}
return instance;
}
/**
* 处理请求逻辑
*
* @param request 请求
* @param response 响应
*/
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
String uri = request.getRequestUri(); //资源路径
//定义文件
File file = new File(staticDir, uri);
if (file.isFile()) {
//文件存在 正常发送响应
response.setContentFile(file);
} else {
//文件不存在 发送404
response.setStatusCode(404); //设置状态码
response.setStatusReason("Not Found"); //设置状态描述
File file404 = new File(staticDir, "404.html"); //执行404文件
response.setContentFile(file404); //设置响应的文件为404文件
}
}
}
ClientHandler解析请求和发送响应:
/**
* @document: 解析请求和发送响应
* @Author:SmallG
* @CreateTime:2023/8/16+14:48
*/
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);
HttpServletResponse response = new HttpServletResponse(socket);
//2、处理请求
DispatcherServlet servlet = DispatcherServlet.getInstance();
servlet.service(request, response);
//3、发送响应
response.send();
} catch (IOException e) {
e.printStackTrace();
} catch (BadRequestException e) {
e.printStackTrace();
} finally {
try {
// 关闭客户端
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
自定义异常:
/**
* @document: 错误请求异常
* @Author: SmallG
* @CreateTime: 2023/8/17+11:49
*/
public class BadRequestException extends Exception{
public BadRequestException() {
}
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
public BadRequestException(Throwable cause) {
super(cause);
}
public BadRequestException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}