淘宝网网站建设的需求分析,网站开发 加密保护,音乐网站建设视频教程,怎么做像知乎一样的网站3.1 重构原生Web服务框架
3.1.1 分析原生Web服务框架 在服务端代码的 ClientHandler 中#xff0c;请求解析、处理请求、返回响应的代码混杂在一起#xff0c;这样的设计会导致代码难以维护和理解。为了提高代码的可读性、可维护性和可扩展性#xff0c;我们需要对这些代码…3.1 重构原生Web服务框架
3.1.1 分析原生Web服务框架 在服务端代码的 ClientHandler 中请求解析、处理请求、返回响应的代码混杂在一起这样的设计会导致代码难以维护和理解。为了提高代码的可读性、可维护性和可扩展性我们需要对这些代码进行重构并按照功能抽取对应的类从而使后续的开发和维护更加方便。 分析目前的代码 重构后软件的整体结构如下图 重构后的结构主要包含以下几个主要组件 1. ClientHandlerWeb处理线程该组件是处理客户端请求的主要线程。它接收客户端发送的HTTP请求并将请求交给HttpServletRequest进行解析然后将解析得到的请求信息传递给DispatcherServlet进行核心请求处理。最后将处理得到的响应信息传递给HttpServletResponse进行缓存和发送给客户端。 2. HttpServletRequest请求解析和封装负责解析和封装客户端发送的HTTP请求的信息包括请求方法、URL、请求头和请求体等。它将原始的HTTP请求转换为一个请求对象以方便后续处理逻辑使用。 3. HttpServletResponse响应缓存和处理负责缓存和处理向客户端发送的响应信息。它将处理得到的响应内容暂存起来并在合适的时机发送给客户端。 4. DispatcherServlet请求分发器封装了核心请求处理逻辑。当ClientHandler接收到客户端请求后将请求信息传递给DispatcherServlet。它根据请求的URL和方法决定调用哪个业务处理模块来处理请求最终得到处理结果。 重构后的结构将不同功能的代码分离到独立的组件中增强了代码的可读性和可维护性。HttpServletRequest负责解析和封装请求信息HttpServletResponse负责缓存和处理响应信息而DispatcherServlet作为请求分发器负责将请求分发给相应的业务处理模块进行处理。这样的设计使得代码逻辑更加清晰方便后续的开发和维护。
3.1.2 重构请求 为了提高代码的模块化和清晰性我们将请求部分的代码抽取到一个新的类HttpServletRequest中该类封装了HTTP请求的解析逻辑并提供了访问解析结果的方法。 1. 定义类HttpServletRequest封装HTTP请求的逻辑。该类包括了以下成员变量
Socket socket: 保存客户端和服务器之间的网络连接String method: 保存HTTP请求的方法如GET、POST等String uri: 保存HTTP请求的URI即请求的资源路径String protocol: 保存HTTP请求使用的协议如HTTP/1.1、HTTP/2.0等HashMapString, String headers: 保存HTTP请求头的所有内容以键值对的形式存储 2. 定义属性访问方法
public String getMethod()返回HTTP请求的方法public String getUri()返回HTTP请求的URIpublic String getProtocol()返回HTTP请求使用的协议 3. 定义方法private void parseRequestLine()用于解析HTTP请求的请求行包括请求方法、URI和协议版本号将解析结果输出到控制台便于后续调试。 4. 定义方法解析请求头以及获取请求头
private void parseHeaders()解析HTTP请求头部的所有内容以键值对的形式存储。public String getHeader(String name)根据请求头的名称返回请求头的值 5. 定义构造函数public HttpServletRequest(Socket socket)接受一个Socket对象作为参数通过解析Socket中的输入流初始化该类的成员变量。并定义方法public String readLine()用于从Socket的输入流中读取一行数据并返回。 HttpServletRequest类的完整代码示意如下
/*封装HTTP请求逻辑 */
public class HttpServletRequest {private Socket socket;private String method;private String uri;private String protocol;private HashMapString, 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[] parts requestLine.split(\\s);method parts[0];uri parts[1];protocol parts[2];System.out.println(解析请求行requestLine);System.out.println(methodmethod);System.out.println(uriuri);System.out.println(protocolprotocol);}/*** 解析请求头方法将解析结构缓存到一个HashMap中* throws IOException 网络出现错误*/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);}/*** 这段代码的作用是从Socket的输入流中读取一行数据并返回。它通过InputStream获取Socket的输入流* 然后使用一个StringBuilder对象来存储读取的数据最终返回读取的数据。* 具体实现逻辑如下* 1. 创建一个InputStream对象in并将其设置为socket的输入流。* 2. 创建一个StringBuilder对象builder用于存储读取的数据。* 3. 定义两个字符变量previous和current用于记录前一个字符和当前字符。* 4. 定义一个int类型变量b用于记录从输入流中读取的字节。* 5. 使用while循环从输入流中读取字节直到读取完一行数据。* 6. 将读取到的字节转换成字符类型并赋值给变量current。* 7. 判断当前字符是否为行结束符\r\n如果是则退出循环否则将当前字符添加到builder中。* 8. 将当前字符赋值给previous以备下次循环使用。* 9. 循环结束后将builder转换成字符串并返回。* return 从Socket的输入流中读取一行数据并返回* throws IOException 出现网络IO错误*/public String readLine() throws IOException{InputStream in socket.getInputStream();StringBuilder builder new StringBuilder();// 前一个字符 当前字符char previous 0, current 0;int b;//解析请求行while ((bin.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();}/*** 获取当前的请求方式* return 请求方式*/public String getMethod() {return method;}/*** 获取当前请求的 uri* return 请求资源路径*/public String getUri() {return uri;}/*** 返回当前请求的 协议* return 返回请求协议*/public String getProtocol() {return protocol;}/*** 查询一个请求头* param name 请求头名字* return 请求头的值*/public String getHeader(String name) {return headers.get(name);}
}6. 重构ClientHandler类将解析请求部分替换为HttpServletRequest
//1. 解析请求
HttpServletRequest request new HttpServletRequest(socket);
String uri request.getUri();通过重构后现在ClientHandler类中的请求部分代码得到了简化提高了代码的可读性和可维护性。同时HttpServletRequest类封装了HTTP请求解析的逻辑使得ClientHandler更专注于业务处理部分使整体结构更清晰。这样的重构有助于提高代码的模块化和可维护性方便后续的开发和维护。
3.1.3 重构响应 在进行请求部分的重构后现在继续对响应逻辑进行重构将响应代码抽取到HttpServletResponse类中以优化ClientHandler。 1. 定义了一个名为HttpServletResponse的类封装HTTP响应的逻辑。包含
socket一个socket实例变量用于表示客户端连接的套接字statusCode表示HTTP状态码默认值为200statusReason表示HTTP状态描述默认值为OKcontentFile表示响应正文对应的实体文件 在构造函数中将客户端的套接字作为参数将其赋给socket实例变量。 2. 添加方法setContentFile、setStatusCode和setStatusReason用于设置响应正文文件、状态码和状态描述分别将它们赋给成员变量。 3. 抽取println方法用于将一行数据发送到网络流中首先通过socket的getOutputStream方法获取输出流然后将数据转换为ISO_8859_1编码的字节数组并发送回车符和换行符。 4. 抽取send方法用于将HTTP响应发送给客户端
它首先根据状态码和状态描述拼接一个状态行并发送给客户端然后发送响应头包括Content-Type和Content-Length最后发送一个空行表示响应头已经发送完成通过FileInputStream读取contentFile中的数据并通过OutputStream发送给客户端 HttpServletResponse类的完整代码示意如下
/*封装HTTP响应逻辑 */
public class HttpServletResponse {private Socket socket;//状态行相关信息private int statusCode 200; //状态代码private String statusReason OK; //状态描述//响应头相关信息//响应正文相关信息
private File contentFile; //响应正文对应的实体文件public HttpServletResponse(Socket socket){this.socket socket;}public void send() throws IOException {String statusLine HTTP/1.1 statusCode statusReason;//发送状态行println(statusLine);System.out.println(发送状态行 statusLine);//发送响应头println(Content-Type: text/html; charsetutf-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];int n;while ((nin.read(buf))!-1){out.write(buf, 0, n);}}public void setContentFile(File contentFile){this.contentFile contentFile;}public void setStatusCode(int statusCode) {this.statusCode statusCode;}public void setStatusReason(String statusReason){this.statusReason statusReason;}/*** 发送一行到网络流* param line 一行* throws IOException 网络故障*/private void println(String line) throws IOException {OutputStream out socket.getOutputStream();byte[] data line.getBytes(StandardCharsets.ISO_8859_1);out.write(data);out.write(\r);//发送回车符out.write(\n);//发送换行符}
}5重构ClientHandler使用HttpServletResponse类替换响应过程
//1. 解析请求
HttpServletRequest request new HttpServletRequest(socket);
HttpServletResponse response new HttpServletResponse(socket);
String uri request.getUri();//2. 发送响应
//根据找到静态资源
//类加载路径:target/classes
File root new File(ClientHandler.class.getClassLoader().getResource(.).toURI()
);
//定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)
File staticDir new File(root,static);
//定位target/classes/static目录中的文件
File file new File(staticDir,uri);response.setContentFile(file);
response.send();6. 重构后ClientHandler的代码变得非常简洁但是测试时候控制台出现了异常信息 这个显然是浏览器在请求favicon.ico文件然而我们的服务器端没有对应的资源造成的问题。解决方案就是按照通行的惯例在没有找到相应资源时候给浏览器响应一个错误码404错误原因是“Not Found”。 通过重构现在ClientHandler类中的响应部分代码也得到了简化提高了代码的可读性和可维护性。HttpServletResponse类封装了HTTP响应的逻辑使得ClientHandler更专注于业务处理部分。同时为了更好地处理未找到资源的情况我们返回了404错误页面提高了用户体验。 这样的重构有助于进一步优化代码结构提高代码的模块化和可维护性使整体逻辑更加清晰。
3.1.4 HTTP响应状态码 RFC2616是HTTP/1.1协议的规范其中定义了HTTP协议中的状态码。以下是RFC2616中定义的HTTP状态码及其含义。 1xx信息性状态码表示接收的请求正在处理。
100 Continue服务器已接收请求头部并且客户端应继续发送请求的主体部分101 Switching Protocols服务器已经理解了客户端的请求并将通过升级协议来完成这个请求 2xx成功状态码表示请求已成功被服务器接收、理解、并接受。
200 OK请求已成功请求所希望的响应头或数据体将随此响应返回201 Created请求已经被实现而且有一个新的资源已经依据请求的需要而建立202 Accepted服务器已接受请求但尚未处理204 No Content服务器成功处理了请求但没有返回任何内容 3xx重定向状态码表示需要客户端执行进一步的操作才能完成请求。
301 Moved Permanently请求的资源已被永久移动到新URI将来的引用应使用新URI302 Found请求的资源临时从不同的URI响应请求将来的引用仍然应该使用原来的URI303 See Other响应可以被找到在另一个URI应使用GET方法来检索此资源304 Not Modified请求的资源未被修改客户端可以使用缓存的版本 4xx客户端错误状态码表示客户端在请求的过程中出错。
400 Bad Request服务器无法理解请求的格式客户端不应该重复发送这个请求401 Unauthorized请求需要用户验证无法通过验证403 Forbidden服务器已经理解请求但是拒绝执行它404 Not Found服务器无法找到请求的资源 5xx服务器错误状态码表示服务器在处理请求的过程中出错。
500 Internal Server Error服务器遇到了一个意外的情况无法完成请求501 Not Implemented服务器不支持客户端请求的功能502 Bad Gateway服务器作为网关或代理从上游服务器收到了无效的响应503 Service Unavailable服务器当前无法处理请求可能是因为维护或过载 以上是RFC2616中定义的HTTP状态码及其含义可以帮助开发者更好地理解HTTP协议中的错误码信息。
3.1.5 处理404错误 首先在 resources/static 文件夹中创建一个 404 错误的html文件 “404.html”该文件的HTML代码如下所示
!DOCTYPE html
html langzh
headmeta charsetUTF-8title404/title
/head
bodyp404 文件没有找到/p
/body
/html然后重构ClientHandler处理404错误先检查文件是否存在如果文件存在就发送文件否则设置状态码“404”状态原因为“Not Found”并且设置发送404.html文件。代码如下所示
//1. 解析请求
HttpServletRequest request new HttpServletRequest(socket);
HttpServletResponse response new HttpServletResponse(socket);
String uri request.getUri();//2. 发送响应
//根据找到静态资源
//类加载路径:target/classes
File root new File(ClientHandler.class.getClassLoader().getResource(.).toURI()
);
//定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)
File staticDir new File(root,static);
//定位target/classes/static目录中的文件
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);response.setContentFile(file404);;
}
//3. 发送响应
response.send();重构后进行测试请求一个不存在的资源比如http://localhost:8088/hi.html 得到如下结果 3.1.6 重构处理请求过程 在对ClientHandler的请求和响应逻辑进行重构后现在可以进一步重构ClientHandler的请求处理过程。将请求处理逻辑抽取到一个新的类DispatcherServlet中该类作为请求处理器包含了处理HTTP请求的逻辑。 1. 抽取请求处理逻辑到一个新的类DispatcherServlet一个请求处理器包含了处理HTTP请求的逻辑。具体功能如下
根据请求中的URI定位到对应的静态资源文件如果该文件存在则将其发送给浏览器如果请求的资源不存在则设置HTTP响应的状态码为404状态描述为Not Found并将静态资源文件404.html发送给浏览器 该类的静态初始化块中通过类加载器获取到当前类所在的classpath目录然后找到其中的static目录作为静态资源文件的根目录。包含属性
root 代表当前classpath的根目录是资源查找起始位置staticDir 静态资源的位置静态网页和图片都存储在这个位置 2. 在service方法中通过HttpServletRequest的getUri方法获取到请求的URI然后在静态资源文件根目录下查找相应的文件如果存在则将其发送给浏览器如果不存在则发送静态资源文件404.html。 DispatcherServlet类的完整代码示意如下
/*封装请求处理逻辑 */
public class DispatcherServlet {private static File root;private static File staticDir;static {try {//根据找到静态资源//类加载路径:target/classesroot new File(ClientHandler.class.getClassLoader().getResource(.).toURI());//定位target/classes/static目录(SpringBoot中存放所有静态资源的目录)staticDir new File(root,static);} catch (URISyntaxException e) {e.printStackTrace();}}public void service(HttpServletRequest request, HttpServletResponse response){String uri request.getUri();//定位target/classes/static目录中的文件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);response.setContentFile(file404);;}}
}3. 重构ClientHandler 重构后的请求处理线程ClientHandler就非常清爽
public class ClientHandler implements Runnable {private Socket socket;public ClientHandler(Socket clientSocket){socket clientSocket;}Overridepublic void run() {try {//1. 解析请求HttpServletRequest request new HttpServletRequest(socket);HttpServletResponse response new HttpServletResponse(socket);String uri request.getUri();//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();}}}
}3.1.7 请求前的空行问题 在进行大量的测试时候有可能出现解析请求时候出现了空行情况收到空请求行后进行请求行解析就会出现异常 其原因是HTTP协议中允许客户端浏览器在HTTP请求前发送空行也就是一个空行符CRLF的作用是分隔请求头和请求体它表示请求头的结束。在请求头结束之后如果请求中包含请求体请求体将会跟在空行之后。由于存在空请求体的请求所以存在请求行之间有空行的意外。 在 HTTP/1.1 规范中如果服务器在开始读取一个消息时收到一个 CRLF则应该忽略它以确保服务器在遇到任何异常情况时都能正常工作。可以参考RFC2616 4.1 Message Types
3.1.8 检查请求行 为解决请求前的空行问题需要在解析请求行的时候忽略空行。然后再利用正则表达式检查请求行是否是合乎HTTP协议的标准进一步增强程序的可靠性。 可以使用AI工具帮助生成正则表达式。 一个检查请求行正确的正则表达式如下
^(GET|POST|PUT|DELETE|HEAD|OPTIONS) ([^?#\s])(\?[^#\s]*)? (HTTP\/1\.0|HTTP\/1\.1)$ 这个正则表达式匹配了HTTP请求行的四个部分请求方法、请求URL、请求参数、HTTP协议版本。
^表示字符串的开始(GET|POST|PUT|DELETE|HEAD|OPTIONS)匹配HTTP请求的方法这里使用了分组和|操作符表示多个可能的方法([^?#\s])匹配请求URI使用了非贪婪的正则表达式表示法不包含URI中可能存在的参数和锚点(\?[^#\s]*)?匹配请求URI中的参数使用了可选分组匹配以?开头的参数部分可以不出现(HTTP\/1\.0|HTTP\/1\.1)匹配HTTP协议的版本号同样使用了分组和|操作符 其中\是转义字符用于匹配特殊字符。正则表达式中的 . 和 | 都是特殊字符需要用\进行转义。 先添加错误请求的自定义异常 BadRequestException
/* 错误请求格式异常 */
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);}
}然后重构请求行解析方法
/*** 解析请求行方法* throws IOException 网络出现异常* throws BadRequestException 请求行格式错误*/
private void parseRequestLine() throws IOException, BadRequestException {String requestLine readLine();//根据HTTP协议描述requestLine 有可能是空行!int n 0;while (requestLine.isEmpty()){//跳过requestLine readLine();if (n 5){throw new BadRequestException(过多的空请求行);}}String regex ^(GET|POST|PUT|DELETE|HEAD|OPTIONS) ([^?#\\s])(\\?[^#\\s]*)? (HTTP\\/1\\.0|HTTP\\/1\\.1)$;if (! requestLine.matches(regex)){throw new BadRequestException(错误的请求行格式);}String[] parts requestLine.split(\\s);method parts[0];uri parts[1];protocol parts[2];System.out.println(解析请求行requestLine);System.out.println(methodmethod);System.out.println(uriuri);System.out.println(protocolprotocol);
}通过重构现在ClientHandler类中的请求处理过程变得非常清晰简洁。我们将请求处理逻辑抽取到了DispatcherServlet类中使得ClientHandler更专注于处理连接和调用请求处理器的功能。这样的设计提高了代码的模块化和可维护性使整体结构更清晰更易于后续的开发和维护。
3.2 单例模式
3.2.1 设计模式与单例模式 设计模式是针对面向对象编程中常见的问题和场景提出的一套经过反复实践验证的解决方案的方法论它描述了一组经过测试和证明的解决方案可以用来解决面向对象编程中的各种问题。 单例模式是一种常用的设计模式它保证一个类只有一个实例并提供一个全局访问点来访问这个唯一的实例。在单例模式中通常将该类的构造函数私有化防止外部直接创建实例而通过一个静态方法或者变量来获取唯一的实例。 单例模式可以避免在系统中出现多个相同的对象减小系统开销并且方便对这个唯一实例进行统一的管理和控制。在需要频繁创建和销毁对象的场景下采用单例模式可以提高系统的性能和可维护性。 在实际开发中单例模式的应用非常广泛例如线程池、数据库连接池、日志系统等等都可以采用单例模式来保证全局唯一性和统一管理。但是在使用单例模式时也需要注意一些问题例如线程安全性、延迟加载等等。
3.2.2 使用单例模式重构请求处理DispatcherServlet 使用单例模式重构请求处理DispatcherServlet可以优化资源的创建和提高软件效能。在Java中创建对象的过程涉及一定的内存和时间开销如果可以减少对象的创建次数可以提升程序性能。在这里我们可以使用饿汉单例模式来确保DispatcherServlet在整个应用程序中只有一个实例。 首先我们需要将DispatcherServlet类设计为单例模式。饿汉单例模式的实现比较简单可以在类加载时就创建唯一的实例对象保证了线程安全性。 接下来我们需要在ClientHandler类中使用DispatcherServlet的单例实例。 通过以上重构我们将DispatcherServlet类设计为了饿汉单例模式确保整个应用程序中只有一个DispatcherServlet实例。同时在ClientHandler类中使用DispatcherServlet.getInstance()来获取该单例实例。 以下是将DispatcherServlet重构为饿汉单例模式的代码
public class DispatcherServlet {// 1. 将构造方法私有化防止外部通过new创建实例private DispatcherServlet(){}//2. 定义一个静态变量来保存实例并进行初始化private static DispatcherServlet instance new DispatcherServlet();// 3. 提供一个公有的静态方法来获取实例public static DispatcherServlet getInstance() {return instance;}// 略去 请求处理代码 ...
}在上面的代码中我们将DispatcherServlet的构造函数设置为私有这样外部就无法通过new DispatcherServlet()来实例化对象。同时我们在类加载时就创建了一个唯一的DispatcherServlet实例并通过静态方法getInstance()来获取该实例。 接下来我们需要在ClientHandler类中使用DispatcherServlet的单例实例
DispatcherServlet servlet DispatcherServlet.getInstance(); 通过以上重构我们将DispatcherServlet类设计为了饿汉单例模式确保整个应用程序中只有一个DispatcherServlet实例。