1 Maven
1.1 什么是Maven
1.1.1 Maven概述
Maven是一种流行的构建工具,用于管理Java项目的构建,依赖管理和项目信息管理。它使用XML文件来定义项目结构和构建步骤,并使用插件来执行各种构建任务。Maven可以自动下载项目依赖项并管理它们的版本,从而使开发人员能够更轻松地创建和维护Java应用程序。
Maven的优点包括:
- 依赖管理:Maven可以自动下载和管理项目依赖项,从而简化了构建和部署过程
- 插件系统:Maven提供了一个强大的插件系统,使开发人员可以轻松地扩展和自定义构建过程
- 项目信息管理:Maven可以管理项目的元数据,例如项目版本号,作者信息等
- 统一的构建方式:Maven提供了一种统一的构建方式,使得团队成员能够更轻松地理解和管理项目的构建过程
- 生态系统:Maven拥有一个庞大的生态系统,包括许多插件和构建工具,使得开发人员可以快速创建高质量的Java应用程序
Maven也有一些缺点,例如:
- 学习曲线:Maven有一些复杂的概念和配置文件,需要一定的学习曲线
- 约束:Maven有一些约束和规范,开发人员需要遵守这些规范,否则会遇到问题
- 构建时间:Maven构建时间可能比手动构建要长一些,因为需要下载和处理许多依赖项
总的来说,Maven是一个强大的构建工具,可以帮助Java开发人员更轻松地管理和构建他们的项目。Maven本身过于复杂,不必进行整体性学习,可以逐步学习逐步掌握其复杂的功能。
Maven官网:https://maven.apache.org。
1.2 Maven的配置和使用
1.2.1 配置Maven 仓库
Maven仓库是Maven用于管理和存储项目依赖项和构建产物的地方。Maven仓库分为本地仓库和远程仓库两种类型:
- 本地仓库:位于开发人员的本地计算机上,存储本地构建的项目产物和下载的依赖项
- 远程仓库:位于服务器上,存储由开发人员上传的构建产物和其他公共的依赖项
默认情况下,Maven会将依赖项从中央仓库下载到本地仓库。但是,有时我们需要将Maven仓库配置为使用阿里云Maven仓库,以获得更快的下载速度和更可靠的构建依赖项。
要将Maven仓库配置为使用阿里云Maven仓库,需要在Maven的settings.xml配置文件中进行以下更改:
1、打开Maven的settings.xml配置文件:在Linux和MacOS上,它通常位于~/.m2/settings.xml;在Windows上,它通常位于C:\Users\你的用户名.m2\settings.xml。
2、找到<mirrors>标签,并在该标签中添加以下镜像配置:
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>*</mirrorOf>
</mirror>
</mirrors>
这里我们使用阿里云公共Maven仓库作为示例,你也可以使用其他的镜像仓库。
3、保存settings.xml文件。
现在,当你执行Maven构建时,它将从阿里云Maven仓库下载依赖项。如果你需要上传构建产物到阿里云Maven仓库,则需要在pom.xml文件中配置发布相关的信息和凭据。
提示:从境外Maven中央仓库下载组件,会非常慢,甚至会下载失败!
1.2.2 IDEA中使用Maven
下面是使用IntelliJ IDEA进行Maven项目开发的基本步骤:
1、安装Maven
可以从Maven官网下载最新版本的Maven,并按照安装说明进行安装。由于IDEA已经内嵌了Maven软件,所以这一步可以忽略。
2、创建Maven项目
在IntelliJ IDEA中创建一个新的Maven项目非常简单。选择"File"->"New"->"Project",然后选择"Maven"项目类型,接着按照向导的提示创建项目。如下图所示:
3、添加依赖
在Maven项目中添加依赖非常简单:只需编辑项目的pom.xml文件,并添加所需的依赖项。在保存pom.xml文件时,IntelliJ IDEA将自动从Maven存储库下载依赖项,存储到本地仓库。
在 Maven 中,每个项目的依赖关系都需要使用坐标(Coordinate)来描述,这个坐标包括以下三个信息:
- groupId:表示依赖项所属的组织或公司的唯一标识符,通常是组织或公司的域名倒序,例如 com.example
- artifactId:表示依赖项的唯一标识符,通常是项目的名称或模块的名称,例如 my-project
- version:表示依赖项的版本号,通常使用三位数字表示,例如 1.0.0
Google的guava工具包依赖项:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
打开项目的pom.xml文件,添加依赖项目:
检查下载的依赖项目:
4、运行Maven构建
只需右键单击pom.xml文件,并选择"Run 'clean install'",然后等待Maven完成构建。
可以通过Maven的命令行界面或使用IDEA提供的Maven工具栏来运行构建(这个功能后期在项目阶段讲解)。
5、测试
在项目的test目录下创建测试类,并通过IDEA提供的测试工具来运行测试。IDEA还支持使用Maven Surefire插件来运行测试(这个功能后期在项目阶段讲解)。
6、打包和部署
编辑项目的pom.xml文件,添加构建插件和相关配置,然后运行Maven构建。Maven可以生成多种类型的构建包,例如JAR、WAR和EAR,可以直接将这些构建包部署到Web服务器上(这个功能后期在项目阶段讲解)。
1.2.3 【案例】为 Maven 项目添加Thymeleaf依赖
Thymeleaf是Java模板引擎,这里利用Thymeleaf测试一下为Maven添加依赖的功能。
在IDEA中,为 Maven 项目添加 Thymeleaf依赖,大致步骤如下:
1、打开项目的 pom.xml 文件,找到 <dependencies> 标签;
2、在 <dependencies> 标签中添加以下代码:
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
3、保存 pom.xml 文件(Reload Maven),Maven 会自动下载并安装所需的 Thymeleaf依赖。
1.3 使用Maven创建多模块项目
1.3.1 使用Maven创建多模块项目
使用Maven创建多模块项目可以将大型项目拆分成更小的模块,每个模块都有自己的pom.xml文件和源代码目录,这使得开发、构建和部署更加容易和灵活。在我们课程中,一个课程阶段也会包含子案例模块,利用Maven创建多模块项目就可以更好的管理这些案例。
1.3.2 在IDEA中创建Maven多模块项目
在 IDEA 中创建多模块 Maven 项目的步骤如下:
1、打开 IDEA,选择 "File" -> "New" -> "Project"。
2、在左侧菜单中选择 "Maven",然后点击 "Next"。
3、输入项目的 GroupId、ArtifactId、Version 和项目名称,然后点击 " Finish", 这样就创建好了主项目。
4、由于主项目只提供容器作用,所以可以将主项目中的src文件夹删除。
5、创建子模块项目,在 IDEA 左侧的项目导航栏中,右键单击项目名称,选择 "New" -> "Module";在 "New Module" 窗口中,选择 "Maven"。
6、在 "New Module" 窗口中,点击 "Next"。
7、在 "New Module" 窗口中,输入项目的 GroupId、ArtifactId、Version 和项目名称,选择模块存储路径,然后点击 "Finish"。
8、重复步骤 5~8 创建多个模块。
完成以上步骤后,就可以开始开发多模块 Maven 项目了。
2 XML
2.1 XML 概述
2.1.1 什么是XML
XML,全称为“可扩展标记语言”(eXtensible Markup Language),是一种用于描述数据的标记语言。它的设计目的是传输数据,并且具有良好的可读性和可扩展性,被广泛应用于各种应用程序之间的数据传输和存储。
XML 的好处包括:
- 简单易学:XML 采用标记的形式来描述数据,标记的含义很明确,很容易理解和学习
- 通用性:XML 不仅仅适用于 Web 应用程序,还可以用于各种不同的应用程序之间的数据传输和存储
- 可读性:XML 的标记很容易阅读,易于人类理解,可以帮助开发人员更快地理解数据的结构和内容
- 可扩展性:XML 允许用户自定义标记,因此可以根据具体的需求扩展标记,实现更为灵活的数据描述方式
- 跨平台性:XML 不依赖于特定的操作系统或开发环境,因此可以在各种平台上使用
- 数据独立性:XML 与具体的数据存储方式无关,可以存储在不同的数据库中,也可以直接存储在文件中,因此可以实现数据独立性
总的来说,XML 是一种通用的数据传输和存储格式,具有简单易学、通用性、可读性、可扩展性、跨平台性和数据独立性等优点,被广泛应用于各种应用程序之间的数据传输和存储。
2.2 XML 核心语法
2.2.1 XML核心语法
XML (eXtensible Markup Language) 是一种用于表示数据的标记语言。它使用标记来标识数据元素,并使用属性来提供有关这些元素的更多信息。下面我们将循序渐进地介绍 XML 基本语法。
1、XML 声明
XML 文件通常以 XML 声明开头,它用于指定 XML 版本和编码方式。例如:
<?xml version="1.0" encoding="UTF-8"?>
在这个例子中,version 属性指定 XML 版本,encoding 属性指定编码方式。UTF-8 是一种通用的编码方式,它支持世界上大多数语言的字符集。
2、根元素
XML 文件必须有一个根元素,只能有一个根元素,它是整个 XML 文档的起点。例如:
<catalog>
...
</catalog>
在这个例子中,<catalog> 是根元素。
3、元素和标签
XML 使用标记来标识数据元素,标记由开始标签、结束标签和内容组成。例如:
<book>
<title>XML Developer's Guide</title>
<author>Gambardella, Matthew</author>
<price>44.95</price>
</book>
在这个例子中,<book> 是开始标签,</book> 是结束标签,<title>、<author>、<price> 是元素,它们包含了书籍的标题、作者和价格信息。
4、属性
XML 元素可以包含属性,属性提供了有关元素的更多信息。例如:
<book id="bk101">
...
</book>
在这个例子中,id 是属性名,bk101 是属性值。
5、注释
XML 支持注释,注释用于在 XML 文件中添加注释性文字,不会被解析器处理。例如:
<!-- 这是一段注释 -->
在这个例子中,<!-- 是注释的开始标记,--> 是注释的结束标记。
6、XML 大小写敏感
这个与Java语法类似,大写字符和小写字符不能混用。一个XML的例子:
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<!-- 这是一段注释 -->
<book id="bk101">
<title>XML Developer's Guide</title>
<author>Gambardella, Matthew</author>
<price>44.95</price>
</book>
<!-- 这是第二段注释 -->
<book id="bk102">
<title>Thinking in Java</title>
<author>Eckel, Bruce</author>
<price>39.56</price>
</book>
</catalog>
以上是 XML 基本语法的介绍,它们构成了 XML 文件的基本结构。
2.2.2 XML实例
XML的用途很多,在软件开发中经常用来作为参数配置文件使用,比如Maven 的pom.xml文件就是XML格式的配置文件,一个完整的pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>MavenDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<!-- 依赖管理 -->
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
</dependencies>
</project>
可以尝试从pom.xml 中找到xml的:XML声明、根元素、元素、属性、注释。
3 手写WebServer
3.1 Web原理
3.1.1 Web概述
Web是指互联网上的万维网(World Wide Web),是一个由超文本、超链接和多媒体内容组成的信息空间。Web的基础技术是HTTP协议、URL、HTML、CSS和JavaScript等。Web被广泛应用于信息检索、在线购物、社交媒体、在线游戏、在线视频和音乐等领域。
Web的好处如下:
- 全球范围的信息共享:Web使得人们可以通过互联网共享信息、知识和文化。用户可以在全球范围内获取和分享信息,这为人们提供了前所未有的便利。
- 便利的在线服务:Web使得人们可以轻松地访问在线服务,如电子邮件、社交媒体、在线银行、在线购物和在线学习等。这些服务可以大大提高人们的生产力和便利性。
- 多媒体内容的呈现:Web使得多媒体内容,如图像、视频和音频,可以轻松地在互联网上呈现和传播。这些内容不仅丰富了用户的体验,也为教育和娱乐等领域提供了新的机会。
- 云计算和Web应用程序:Web应用程序可以在云计算环境中运行,使得用户可以使用网络浏览器轻松访问和使用这些应用程序。这些应用程序包括在线办公套件、在线协作工具、电子商务网站等。
Web作为一种重要的信息和娱乐渠道,它可以为用户提供全球信息共享、便利的在线服务、多媒体内容的呈现、云计算和Web应用程序等。
3.1.2 Web工作原理
Web的工作原理是基于客户端-服务器模型(B/S)的。简单来说,Web由Web服务器、Web客户端和通信协议组成。
1、Web服务器
Web服务器是一个可以接收客户端请求的软件程序。它运行在一个计算机上,一般是指提供Web服务的主机,可以在这个主机上存储Web页面、图像和其他资源。当Web服务器接收到一个客户端请求后,它会发送一个HTTP响应,包括被请求资源的内容和元数据。
2、Web客户端
Web客户端是通过浏览器访问Web的用户设备,如电脑、手机等。当用户在浏览器中输入URL时,浏览器会发送一个HTTP请求到Web服务器。Web服务器接收到请求后,会查找请求的资源并将响应返回给浏览器,浏览器会将响应显示在用户的屏幕上。
3、HTTP协议
Web的通信是基于HTTP协议进行的。HTTP是一种客户端-服务器协议,用于传输超文本文档(HTML、XML、图片等)。它定义了浏览器和Web服务器之间的请求和响应交互方式。当浏览器发送HTTP请求时,请求会包含HTTP方法(GET、POST、PUT等)、请求的URL和HTTP头部信息。Web服务器会解析HTTP请求并生成HTTP响应。HTTP响应会包括状态码、HTTP头部信息和响应正文。状态码表示请求是否成功,HTTP头部信息包含了响应的元数据,响应正文则包含了请求的数据。
4、HTML
HTML是用于创建Web页面的标记语言。HTML标签描述了文本和其他内容在Web页面上的显示方式。浏览器可以读取HTML文件,并将其转换成可视化的Web页面。
因此,Web的工作原理是基于HTTP协议和客户端-服务器模型的。Web服务器接收HTTP请求,查找并生成响应,并将其发送回浏览器。浏览器读取响应并将其转换成可视化的Web页面。
3.2 手写WebServer
3.2.1 手写WebServer的意义
手写WebServer对学习编程有很大的意义:有助于深入理解Web的工作原理和HTTP协议的细节,可以提高对计算机网络和操作系统的理解,并增强编程和软件开发的能力。
1、理解Web工作原理:通过编写代码,实现客户端和服务端的信息交互,从而理解Web的工作原理和HTTP通信协议,掌握Web应用程序和网络通信的底层原理。
2、提高编程能力:手写WebServer需要掌握网络编程、操作系统和Web开发等多种技能,有利于更好地理解软件开发的基本原理和技术,并掌握高效编程的方法和技巧。
3、加强调试能力:手写WebServer需要不断地测试和调试,从而加强调试能力,有助于更好地掌握软件调试的方法和技巧,从而提高开发效率和代码质量。
4、培养创新意识:手写WebServer需要不断地思考和创新,可以培养创新意识,并学会在开发过程中不断提高自己的能力和水平。
懂源码的程序员才是真正的程序员。要想原生手写WebServer就需要从深入学习HTTP协议开始。
3.2.2 HTTP协议
HTTP(Hypertext Transfer Protocol)协议是一种应用层协议,用于在Web浏览器和Web服务器之间传输数据。HTTP协议是Web的基础技术之一,它定义了客户端(如Web浏览器)和服务器之间的通信规则,使得Web可以实现信息的交互和共享。
HTTP协议的设计是为了解决在Web上传输数据的问题。在早期的Web中,各种应用程序使用不同的通信协议,导致Web上的信息交流困难,信息共享也受到了限制。HTTP协议的出现解决了这些问题,使得Web的发展更加迅速和广泛。
HTTP协议的优势如下:
- 简单易用:HTTP协议的设计非常简单,易于理解和实现。这使得开发人员可以更快地开发Web应用程序,并且更容易调试和维护。
- 可扩展性:HTTP协议的设计具有良好的可扩展性。这意味着可以通过添加新的功能和特性来改进HTTP协议,从而满足不断变化的需求。
- 无状态:HTTP协议是无状态协议,它不保存任何关于请求或响应的状态信息。这使得Web服务器可以处理大量的请求,并提高了Web应用程序的可伸缩性。
- 可靠性:HTTP协议的设计非常可靠,可以确保数据在客户端和服务器之间的安全传输。HTTP协议还支持数据压缩、数据加密等技术,提高了数据传输的效率和安全性。
HTTP协议是一种简单易用、可扩展、无状态和可靠的应用层协议,它使得Web应用程序可以高效地传输和共享数据,从而推动了Web的发展和应用。
3.2.3 HTTP协议工作原理
HTTP协议是一个客户端-服务器协议,它的工作原理可以分为以下几个步骤:
1、建立连接:客户端向服务器发送一个连接请求,请求连接到服务器。客户端可以通过TCP/IP协议或TLS/SSL协议建立连接。
2、发送请求:客户端向服务器发送HTTP请求,请求包括请求方法、请求头、请求体等信息。常见的请求方法包括GET、POST、PUT、DELETE等。
3、处理请求:服务器接收到HTTP请求后,根据请求的方法和URL等信息进行处理。服务器可以返回HTTP响应,包括响应状态码、响应头和响应体等信息。
4、发送响应:服务器向客户端发送HTTP响应,响应包括响应状态码、响应头、响应体等信息。
5、关闭连接:一旦HTTP响应发送完毕,服务器和客户端都可以选择关闭连接。关闭连接可以释放网络资源,提高性能和安全性。
在HTTP协议中,客户端和服务器之间的通信是通过HTTP报文进行的。HTTP报文分为请求报文和响应报文,分别用于客户端向服务器发送请求和服务器向客户端发送响应。HTTP报文包括起始行、头部字段和消息体等部分,它们用于传输数据和控制信息。
总之,HTTP协议是一个客户端-服务器协议,它通过HTTP报文来传输数据和控制信息。HTTP协议的工作原理是建立连接、发送请求、处理请求、发送响应和关闭连接等步骤。
3.2.4 WebServer的处理步骤
Java WebServer的大致处理步骤如下:
1、创建一个ServerSocket对象:ServerSocket对象用于监听指定的端口,并接受客户端的请求。
2、等待客户端连接:通过调用ServerSocket的accept()方法,等待客户端的连接请求。当有客户端连接时,accept()方法返回一个Socket对象,用于和客户端进行通信。
3、解析HTTP请求:从Socket对象中读取客户端的请求数据,并将其解析为HTTP请求。HTTP请求由请求行、请求头和请求体组成。
4、处理HTTP请求:根据HTTP请求中的方法、路径和参数等信息,处理客户端的请求,并生成HTTP响应。HTTP响应由状态行、响应头和响应体组成。
5、发送HTTP响应:将HTTP响应发送回客户端,并关闭Socket连接。
实现一个WebServer涉及到很多细节,例如解析HTTP请求、处理GET和POST请求、生成HTTP响应等。
3.3 接收HTTP请求
3.3.1 HTTP请求结构
HTTP请求报文由三个部分组成:请求行、请求头和请求体。查看一个简单的HTTP GET请求的示例:
GET /hello.txt HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
1、请求行
请求行是HTTP请求报文的第一行,包括HTTP方法、请求URI和HTTP版本。请求行的格式如下:
METHOD URI HTTP_VERSION
其中:METHOD为HTTP方法,通常为GET、POST、PUT、DELETE等;URI为请求资源的路径,可以包含查询参数;HTTP_VERSION为HTTP协议版本,通常为HTTP/1.1或HTTP/2。
上个示例中,请求行包含了HTTP方法GET、请求路径/hello.txt和HTTP版本号HTTP/1.1。
2、请求头
请求头紧随请求行之后,以一或多个以冒号分隔的键值对的形式提供附加信息。每个键值对为一行,键和值之间用冒号和空格分隔。请求头包含了客户端发送请求时的各种信息,如Accept、User-Agent、Host等。
上个示例中,请求头包含了Host、User-Agent和Accept三个键值对。Host指定了服务器的域名或IP地址,User-Agent指定了浏览器的类型和版本,Accept指定了浏览器能够接受的响应格式。
3、请求体
请求体是HTTP请求报文的可选部分,通常在使用POST或PUT方法提交表单数据时出现。请求体包含了客户端发送到服务器的实际数据,如表单字段、文件内容等。
上个示例中,GET请求不包含请求消息体,以空行作为结束标识。
下面是一个HTTP POST请求报文的示例:
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
username=john&password=doe
这个示例中,请求行为POST /login HTTP/1.1,表示使用POST方法向/login路径提交请求。请求头包括了Host、Content-Type和Content-Length三个键值对。请求体为username=john&password=doe,表示提交了用户名和密码两个表单字段的值。注意请求头和请求体之间有一个空行。其中Content-Length的长度25就是请求体中数据“username=john&password=doe”的字节数量。
关于编码:请求行和请求头都是 ISO8859-1编码,不能直接使用中文,中文需要进行编码处理。
HTTP协议的详细内容可以参考HTTP协议官方文档 RFC2616标准:https://www.rfc-editor.org/rfc/rfc2616。
3.3.2 接收HTTP请求
使用Java编程实现接收浏览器的HTTP请求,可以使用Java的Socket和ServerSocket类来实现一个简单的HTTP服务器。具体步骤如下:
1、创建ServerSocket对象,并指定监听的端口号8088。
ServerSocket serverSocket = new ServerSocket(8088);
2、使用accept()方法等待客户端的连接请求,并获取客户端的Socket对象。
Socket clientSocket = serverSocket.accept();
3、从客户端Socket对象中获取输入流,先尝试一个简单方式读取HTTP请求报文(请求消息),读取代码示意如下:
InputStream in = clientSocket.getInputStream();
int b;
while ((b=in.read())!=-1){
System.out.print((char) b);
}
in.close();
3.3.3 【案例】编程实现接收HTTP请求
编写服务端代码,实现接收HTTP请求,完整案例代码如下:
public class ServerBootApplication {
private ServerSocket serverSocket;
public void start(){
try {
//创建ServerSocket对象,并指定监听的端口号8088。
serverSocket = new ServerSocket(8088);
//使用accept()方法等待客户端的连接请求,并获取客户端的Socket对象
Socket clientSocket = serverSocket.accept();
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = clientSocket.getInputStream();
int b;
while ((b=in.read())!=-1){
System.out.println((char) b);
}
in.close();
//关闭客户端连接
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建ServerBoot对象
ServerBootApplication1 application = new ServerBootApplication1();
//启动服务器
application.start();
}
}
打开浏览器向 http://localhost:8088 发起请求,在开发工具控制台上输出如下信息:
这些信息是一个浏览器发送的一个HTTP GET请求,不同浏览器信息略有区别。
这个案例存在问题,客户浏览器会一直卡住“转圈圈”,原因是浏览器没有主动断开网络,只有断开网络时候,服务器端才能收到“-1”程序才能继续执行,否则就会在in.read()位置进行阻塞等待,客户端效果就是“转圈圈”。
如何解决这个问题呢?要分析一下HTTP GET请求消息结构:
GET请求消息每个行结束符号为“\r\n”,最后发送了空行“\r\n”为结束,我们改进程行读取到空行就结束读取,让循环结束,代码改进如下:
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = clientSocket.getInputStream();
StringBuilder builder= new StringBuilder();
// 前一个字符 当前字符
char previous = 0, current = 0;
int b;
while ((b=in.read())!=-1){
//将读取的字节存储到当前字符, 由于请求头采用了ISO8859-1编码,
// 所以可以讲字节直接转化为字符类型
current = (char) b;
//如果发现了 前一个字符是 \r 当前字符是 \n 就读取到了行末尾
if (previous == '\r' && current == '\n'){
//如果这一行是空行就结束处理了
if (builder.toString().isEmpty()){
break;
}
//输出这一行数据当前一行数据并且清空builder,为下次缓存数据做准备
System.out.println(builder);
builder.delete(0, builder.length());
}else if (current != '\r' && current != '\n'){
//当前的不是 \r \n 就是一行中的字符
builder.append(current);
}
//最后将当前的字符作为下次的前一个字符
previous = current;
}
in.close();
这段代码从客户端Socket对象的输入流中读取HTTP请求报文(请求消息)。该代码使用一个StringBuilder对象来存储读取到的数据,并使用一个while循环遍历输入流中的字节。
在while循环中,代码将当前字节转换为字符类型并存储到变量current中,同时检查前一个字符是否是回车符(\r)并且当前字符是否是换行符(\n),如果是,就表示读取到了一行的末尾,将该行数据输出并清空StringBuilder对象。
如果当前字符不是回车符或换行符,那么就是一行中的字符,将该字符添加到StringBuilder对象中。
在处理完一行数据后,将当前字符作为下次循环的前一个字符;最后,关闭输入流。
需要注意的是,请求报文采用了ISO8859-1编码,因此可以将字节直接转换为字符类型。
案例的完整代码如下:
public class ServerBootApplication {
private ServerSocket serverSocket;
public void start(){
try {
//创建ServerSocket对象,并指定监听的端口号8088。
serverSocket = new ServerSocket(8088);
//使用accept()方法等待客户端的连接请求,并获取客户端的Socket对象
Socket clientSocket = serverSocket.accept();
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = clientSocket.getInputStream();
StringBuilder builder= new StringBuilder();
// 前一个字符 当前字符
char previous = 0, current = 0;
int b;
while ((b=in.read())!=-1){
//将读取的字节存储到当前字符, 由于请求头采用了ISO8859-1编码,
// 所以可以讲字节直接转化为字符类型
current = (char) b;
//如果发现了 前一个字符是 \r 当前字符是 \n 就读取到了行末尾
if (previous == '\r' && current == '\n'){
//如果这一行是空行就结束处理了
if (builder.toString().isEmpty()){
break;
}
//输出这一行数据当前一行数据并且清空builder,为下次缓存数据做准备
System.out.println(builder);
builder.delete(0, builder.length());
}else if (current != '\r' && current != '\n'){
//当前的不是 \r \n 就是一行中的字符
builder.append(current);
}
//最后将当前的字符作为下次的前一个字符
previous = current;
}
in.close();
//关闭客户端连接
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建ServerBoot对象
ServerBootApplication application = new ServerBootApplication();
//启动服务器
application.start();
}
}
服务器端可以正常显示浏览器的请求信息,并且服务端程序可以正常结束:
3.4 发送HTTP响应
3.4.1 HTTP响应
在上一节案例中虽然服务器端结束了,但是客户端端得到了一个不正常的结果:
其原因是:服务器没有向浏览器发送任何响应消息,浏览器没有收到任何信息。解决办法就是在服务端程序,向浏览器发送响应消息。
3.4.2 响应消息结构
要能正确发送响应消息就必须了解完整的响应消息结构。HTTP响应消息由三部分组成:状态行、响应头和响应正文。
1、状态行
状态行由HTTP协议版本、状态码和状态描述组成,通常格式如下:
HTTP/1.1 200 OK
其中,HTTP/1.1表示HTTP协议的版本,200表示状态码,OK是状态描述。
2、响应头
响应头包含了一些关于响应消息的元数据,如响应日期、内容类型、内容长度等,格式如下:
Content-Type: text/html
Content-Length: 1234
Date: Fri, 25 Feb 2023 10:00:00 GMT
3、响应正文(响应体)
响应正文是服务器返回的实际数据,可以是HTML网页、图片、文本等等。响应正文的格式和内容取决于服务器返回的数据类型和内容。
完整的HTTP响应消息结构如下:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Date: Fri, 25 Feb 2023 10:00:00 GMT
<html>
<head>
<title>Example</title>
</head>
<body>
<p>This is an example.</p>
</body>
</html>
其中响应头Content-Type: text/html; charset=utf-8 用于说明,响应正文中的内容类型,这是text/html表示,响应正文中是一个html网页,charset=utf-8表示响应正文中的网页采用UTF-8编码。
响应头Content-Length: 1234,用于说明响应正文中内容长度,单位是字节数量。
需要注意的是,HTTP响应消息中的每个部分都使用特定的分隔符进行分割。状态行和响应头之间使用一个空行进行分割,响应头和响应正文之间也使用一个空行进行分割。服务器端需要按照HTTP协议规定的格式构造响应消息,客户端收到响应消息后也需要按照HTTP协议规定的方式解析响应消息。
3.4.3 【案例】向浏览器发送HTTP响应
在Java中向浏览器发送HTTP响应需要借助Java中的Socket和OutputStream等类。以下是一个简单的Java程序示例,可以向浏览器发送一段HTML内容的HTTP响应:
OutputStream out = clientSocket.getOutputStream();
//一个简单的网页内容
String html = "<html>\n" +
"<head>\n" +
"<title>Example</title>\n" +
"</head>\n" +
"<body>\n" +
"<p>Hello World!</p>\n" +
"</body>\n" +
"</html>";
byte[] body = html.getBytes(StandardCharsets.UTF_8);
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8"
.getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write(("Content-Length: "+body.length)
.getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write('\r'); //空行
out.write('\n');
out.write(body);
//关闭客户端连接
out.close();
这段代码用于向客户端发送HTTP响应。具体来说,它先构造了一个简单的HTML网页,然后将HTML内容转换成UTF-8编码的字节数组,将HTTP响应头和响应体分别写入到客户端的输出流中。
HTTP响应的第一行为状态行,这里写入了“HTTP/1.1 200 OK”,表示HTTP版本为1.1,状态码为200,状态码200表示请求成功。接着写入了响应头信息,包括“Content-Type”表示响应体类型为HTML文本,“charset=utf-8”表示响应体采用的字符集为UTF-8,“Content-Length”表示响应体长度为body的字节数组长度。之后写入一个空行,表示响应头和响应体的分隔符,最后将响应体内容写入到输出流中。
最后,关闭客户端的输出流,表示该响应已经发送完毕,可以断开与客户端的连接。
完整案例:
package cn.highedu.boot;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class ServerBootApplication {
private ServerSocket serverSocket;
public void start(){
try {
//创建ServerSocket对象,并指定监听的端口号8088。
serverSocket = new ServerSocket(8088);
//使用accept()方法等待客户端的连接请求,并获取客户端的Socket对象
Socket clientSocket = serverSocket.accept();
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = clientSocket.getInputStream();
StringBuilder builder= new StringBuilder();
// 前一个字符 当前字符
char previous = 0, current = 0;
int b;
while ((b=in.read())!=-1){
//将读取的字节存储到当前字符, 由于请求头采用了ISO8859-1编码,
// 所以可以讲字节直接转化为字符类型
current = (char) b;
//如果发现了 前一个字符是 \r 当前字符是 \n 就读取到了行末尾
if (previous == '\r' && current == '\n'){
//如果这一行是空行就结束处理了
if (builder.toString().isEmpty()){
break;
}
//输出这一行数据当前一行数据并且清空builder,为下次缓存数据做准备
System.out.println(builder);
builder.delete(0, builder.length());
}else if (current != '\r' && current != '\n'){
//当前的不是 \r \n 就是一行中的字符
builder.append(current);
}
//最后将当前的字符作为下次的前一个字符
previous = current;
}
OutputStream out = clientSocket.getOutputStream();
//一个简单的网页内容
String html = "<html>\n" +
"<head>\n" +
"<title>Example</title>\n" +
"</head>\n" +
"<body>\n" +
"<p>Hello World!</p>\n" +
"</body>\n" +
"</html>";
byte[] body = html.getBytes(StandardCharsets.UTF_8);
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write(("Content-Length: "+body.length).getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write('\r'); //空行
out.write('\n');
out.write(body);
//关闭客户端连接
out.close();
in.close();
clientSocket.close();
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建ServerBoot对象
ServerBootApplication application = new ServerBootApplication();
//启动服务器
application.start();
}
}
4 WebServer进阶
4.1 使用多线程处理多用户请求
4.1.1 多线程Socket通信
上述案例服务端显然只能处理一次浏览器请求,请求一次浏览器端就结束程序。如何解决这个问题呢?可以采用“聊天室”案例的中的多线程Socket通信技术,解决多用户并发请求。
在多线程Socket通信中,服务端会启动一个主线程用于监听客户端的连接请求,并为每个客户端连接请求创建一个新的子线程进行处理。这样可以保证服务端能够同时处理多个客户端的请求,提高系统的并发性能和稳定性。
具体流程如下:
1. 服务端启动主线程监听客户端的连接请求;
2. 当有新的客户端连接请求时,主线程创建一个新的子线程来处理该客户端请求;
3. 子线程接收客户端的请求信息,并根据请求内容进行相应的业务处理;
4. 子线程将处理结果封装成响应报文发送给客户端;
5. 子线程关闭连接,结束线程。
这样,服务端就可以同时处理多个客户端请求,实现了高并发处理能力。同时,使用多线程编程也能提高代码的可维护性和可扩展性,减少代码耦合度。
4.1.2 【案例】使用多线程处理HTTP通信
首先定义ClientHandler,作为线程处理HTTP请求和发生HTTP响应:
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket clientSocket){
socket = clientSocket;
}
@Override
public void run() {
try {
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = socket.getInputStream();
StringBuilder builder= new StringBuilder();
// 前一个字符 当前字符
char previous = 0, current = 0;
int b;
while ((b=in.read())!=-1){
//将读取的字节存储到当前字符, 由于请求头采用了ISO8859-1编码,
// 所以可以讲字节直接转化为字符类型
current = (char) b;
//如果发现了 前一个字符是 \r 当前字符是 \n 就读取到了行末尾
if (previous == '\r' && current == '\n'){
//如果这一行是空行就结束处理了
if (builder.toString().isEmpty()){
break;
}
//输出这一行数据当前一行数据并且清空builder,为下次缓存数据做准备
System.out.println(builder);
builder.delete(0, builder.length());
}else if (current != '\r' && current != '\n'){
//当前的不是 \r \n 就是一行中的字符
builder.append(current);
}
//最后将当前的字符作为下次的前一个字符
previous = current;
}
OutputStream out = socket.getOutputStream();
//一个简单的网页内容
String html = "<html>\n" +
"<head>\n" +
"<title>Example</title>\n" +
"</head>\n" +
"<body>\n" +
"<p>Hello World!</p>\n" +
"</body>\n" +
"</html>";
byte[] body = html.getBytes(StandardCharsets.UTF_8);
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write(("Content-Length: "+body.length).getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write('\r'); //空行
out.write('\n');
out.write(body);
//关闭客户端连接
out.close();
in.close();
}catch (IOException e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
然后重构start()方法:
public class ServerBootApplication {
private ServerSocket serverSocket;
public void start(){
try {
//创建ServerSocket对象,并指定监听的端口号8088。
serverSocket = new ServerSocket(8088);
while (true) {
//使用accept()方法等待客户端的连接请求,并获取客户端的Socket对象
Socket clientSocket = serverSocket.accept();
ClientHandler clientHandler = new ClientHandler(clientSocket);
Thread thread = new Thread(clientHandler);
thread.start();
}
}catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建ServerBoot对象
ServerBootApplication application = new ServerBootApplication();
//启动服务器
application.start();
}
}
该代码是一个典型的多线程Socket通信的服务器端代码结构。在主线程中,创建了一个ServerSocket对象,并指定要监听的端口号8088。接着,通过一个while循环不断地使用accept()方法等待客户端的连接请求,一旦接收到请求,就会创建一个新的ClientHandler对象,将客户端的Socket对象传递给它,然后将ClientHandler对象封装成一个新的线程并启动,用于处理客户端的请求。在这个过程中,主线程一直保持监听状态,等待下一个客户端连接。
为了处理多个客户端的并发请求,每个ClientHandler对象都运行在一个单独的线程中,这使得服务器可以同时处理多个客户端的请求,提高了系统的并发处理能力。
经过上述重构我们的WebServer就可以处理多用户的并发请求了。
4.1.3 关于favicon.ico
实现了多线程Web请求处理功能以后,控制台上出现了如下请求信息:
由图可以看出:显然请求了favicon.ico文件。
favicon.ico是一个网站上常见的文件,它是网站的图标文件,通常会显示在网站的标签页和书签上。当用户访问一个网站时,浏览器会自动请求这个文件,以便在标签页和书签上显示网站的图标。因此,在服务器的请求日志中,我们会看到很多关于 favicon.ico 的请求记录。这些请求记录是非常正常的现象,不必过于关注。
理解这些请求信息有助于我们对网站访问进行监控和分析。通常情况下,这些请求不会对服务器性能产生重大影响,因为 favicon.ico 文件通常是相对较小的图标文件,并且浏览器会进行缓存,减少了对服务器的重复请求。
因此,不必担心看到这些 favicon.ico 请求记录,除非你注意到在短时间内出现异常的请求量,这可能是有人在恶意攻击或者其他异常情况,需要进一步分析和处理。否则,这些请求是正常的,不需要特别处理。
4.2 解析请求行
4.2.1 解析请求行
HTTP请求行是HTTP请求报文的第一行,包括HTTP方法、请求URL和HTTP协议版本号。例如:
GET /index.html HTTP/1.1
其中,GET表示HTTP请求方法,/index.html表示请求的资源URL,HTTP/1.1表示使用的HTTP协议版本号。
解析HTTP请求行可以获取请求方法、请求URL和协议版本等信息,这些信息对于服务器来说非常重要,可以根据这些信息对请求进行处理和响应。例如,根据请求URL可以确定请求的资源类型和位置,从而进行处理和响应;根据请求方法可以确定请求的类型(如GET、POST、PUT、DELETE等),从而采取相应的处理方式。因此,解析HTTP请求行是Web服务器处理HTTP请求的重要步骤之一。
前述多线程的WebServer案例虽然能过处理用户请求,但是用户发起任何请求都会得到相同的响应结果,比如发送:http://localhost:8088/ 、http://localhost:8088/index.html和 http://localhost:8088/demo.html,都得到如下结果:
这个结果显然不理想:不可能任何请求都返回相同的响应结果。正确情况应该是:请求index.html就显示index.html文件的内容,请求demo.index文件就显示demo.html的内容。
4.2.2 显示正确的请求内容
如何解决这个问题呢?解决的办法就是将请求行进行解析,找出客户端发起的请求资源路径,根据请求资源的路径找到响应的资源,发送响应到客户端浏览器。这样就可以在用户请求不同资源时候,响应不同的结果:
4.2.3 【案例】读取请求行
读取请求行的算法,可以利用AI工具帮助分析和生成。
请求行在请求消息的第一行,请求行以\r\n为结尾,可以使用算法在读取请求头之前读取第一行作为请求行,如下代码可以读取请求的第一行:
InputStream in = socket.getInputStream();
StringBuilder builder= new StringBuilder();
// 前一个字符 当前字符
char previous = 0, current = 0;
int b;
String requestLine = null;
//解析请求行
while ((b=in.read())!=-1){
current = (char) b;
if (previous == '\r' && current == '\n'){
requestLine = builder.toString();
builder.delete(0, builder.length());
break;
}else if (current != '\r' && current != '\n'){
builder.append(current);
}
previous = current;
}
System.out.println(requestLine);
显然这个算法和读取请求头的代码是重复的,所以可以尝试将代码重构抽取一个从输入流中读取一行的方法:
public String readLine() throws IOException{
InputStream in = socket.getInputStream();
StringBuilder builder= new StringBuilder();
// 前一个字符 当前字符
char previous = 0, current = 0;
int b;
//解析请求行
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();
}
这段代码的作用是从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转换成字符串并返回。
重构后的ClientHandler代码就清爽许多:
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket clientSocket){
socket = clientSocket;
}
@Override
public void run() {
try {
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = socket.getInputStream();
//读取请求行
String requestLine = readLine();
System.out.println(requestLine);
//读取请求头
String requestHeader;
//读取到空行就不在读取请求头了
while (!(requestHeader = readLine()).isEmpty()){
System.out.println(requestHeader);
}
OutputStream out = socket.getOutputStream();
//一个简单的网页内容
String html = "<html>\n" +
"<head>\n" +
"<title>Example</title>\n" +
"</head>\n" +
"<body>\n" +
"<p>Hello World!</p>\n" +
"</body>\n" +
"</html>";
byte[] body = html.getBytes(StandardCharsets.UTF_8);
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write(("Content-Length: "+body.length).getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write('\r'); //空行
out.write('\n');
out.write(body);
//关闭客户端连接
out.close();
in.close();
}catch (IOException e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 这段代码的作用是从Socket的输入流中读取一行数据并返回。
* @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 ((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();
}
}
4.2.4 【案例】解析请求行
上述代码实现了,读取请求行,读取后需要从请求行中解析其中的每个部分,然后可以根据请求行找到相应的本地文件资源,发送响应给浏览器,显示不同的资源内容。
解析请求行:
String[] line = requestLine.split("\\s");
String method = line[0];
String uri = line[1];
String protocol = line[1];
System.out.println("method: "+method);
System.out.println("uri: " + uri);
System.out.println("protocol: " + protocol);
这段代码用于解析HTTP请求报文中的请求行。首先,将请求行按照空白字符进行分割,得到一个包含请求方法、URI和协议版本三个字段的字符串数组。接着,将这三个字段分别存储到对应的变量中,并打印出来以供调试或其他用途。
具体解释如下:
- 使用String类的split()方法按照空白字符(包括空格、制表符和换行符)对请求行进行分割,得到一个包含请求方法、URI和协议版本三个字段的字符串数组line
- 将line数组中的第一个元素存储到字符串变量method中,第二个元素存储到字符串变量uri中,第三个元素存储到字符串变量protocol中
- 最后,使用System.out.println()方法打印出method、uri和protocol的值,方便调试和查看解析结果
需要注意的是,该代码仅仅是对HTTP请求报文中的请求行进行了最基本的解析,仅适用于最简单的HTTP请求,对于复杂的HTTP请求报文,需要进行更加严谨和完整的解析。
重新启动后服务端后,用浏览器发起 http://localhost:8088/index.html 请求,控制台信息包含如下信息:
这个信息表示请求行解析成功。
4.3 响应静态资源
4.3.1 创建静态资源
前述项目完成了解析请求行,从请求行中得到了请求资源的路径URI,为了能响应客户端需求,返回对应的资源,所以要建立静态文件夹存储静态资源:
index.html 文件是一个html文件,格式类似于html,内容如下:
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
demo.html 文件内容如下:
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Demo Page</h1>
</body>
</html>
有了这了两个资源,就可以实现根据用户请求的uri,找到这两个资源,并且发送到响应到浏览器,实现根据用户请求响应不同文件的功能。
4.3.2 响应静态资源
重构服务端代码,根据URI在resources/static文件夹中找到静态资源,并且将静态资源响应给客户端,原理为:
4.3.3 【案例】响应静态资源
重构后的代码:
//发送响应
//根据找到静态资源
//类加载路径: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);
//读取文件的全部内容
byte[] bytes = new byte[(int)file.length()];
FileInputStream fin = new FileInputStream(file);
fin.read(bytes);
fin.close();
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write(("Content-Length: "+bytes.length).getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write('\r'); //空行
out.write('\n');
out.write(bytes);
//关闭客户端连接
out.close();
in.close();
这段代码是一个简单的HTTP服务器响应客户端请求的部分,主要功能是根据请求的URI定位服务器上的静态资源,并将其发送给客户端。
具体来说,代码首先根据当前类的类加载器获取类加载路径,然后根据该路径找到服务器上的静态资源所在目录。接着,根据URI定位静态资源文件,并读取该文件的全部内容到一个字节数组中。
然后,代码使用Java的Socket API将HTTP响应发送给客户端。响应包括HTTP响应头和响应体两部分。响应头中包含HTTP协议版本、状态码和响应内容类型等信息,而响应体则包含实际的静态资源内容。发送响应的代码使用OutputStream将响应数据写入到客户端的Socket连接中,并在最后关闭客户端连接。
完成的ClientHandler参考如下:
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket clientSocket){
socket = clientSocket;
}
@Override
public void run() {
try {
//从客户端Socket对象中获取输入流,读取HTTP请求报文(请求消息)。
InputStream in = socket.getInputStream();
//读取请求行
String requestLine = readLine();
System.out.println(requestLine);
//解析请求行
String[] line = requestLine.split("\\s");
String method = line[0];
String uri = line[1];
String protocol = line[1];
System.out.println("method: "+method);
System.out.println("uri: " + uri);
System.out.println("protocol: " + protocol);
//读取请求头
String requestHeader;
//读取到空行就不在读取请求头了
while (!(requestHeader = readLine()).isEmpty()){
System.out.println(requestHeader);
}
//发送响应
//根据找到静态资源
//类加载路径: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);
//读取文件的全部内容
byte[] bytes = new byte[(int)file.length()];
FileInputStream fin = new FileInputStream(file);
fin.read(bytes);
fin.close();
OutputStream out = socket.getOutputStream();
out.write("HTTP/1.1 200 OK".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write("Content-Type: text/html; charset=utf-8".getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write(("Content-Length: "+bytes.length).getBytes(StandardCharsets.ISO_8859_1));
out.write('\r');
out.write('\n');
out.write('\r'); //空行
out.write('\n');
out.write(bytes);
//关闭客户端连接
out.close();
in.close();
}catch (IOException | URISyntaxException e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 这段代码的作用是从Socket的输入流中读取一行数据并返回。它通过InputStream获取Socket的输入流,
* 然后使用一个StringBuilder对象来存储读取的数据,最终返回读取的数据。
* @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 ((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();
}
}
4.3.4 简述HTTP协议
HTTP(Hypertext Transfer Protocol)是一种用于在计算机网络上传输超文本数据的协议。它是Web应用程序中最基本的通信协议,用于在客户端(例如浏览器)和服务器之间传输数据。
HTTP的主要特点包括:
- 状态无关:HTTP是一种无状态协议,即服务器不会记录客户端之前的请求信息。每个HTTP请求都是独立的,服务器不会保留客户端的状态信息,这样可以降低服务器的负担,也使得HTTP协议具有良好的扩展性。
- 请求-响应模型:HTTP是基于请求-响应模型的协议。客户端发送HTTP请求到服务器,然后服务器返回HTTP响应。请求包括请求方法(GET、POST等)、请求头、请求体等信息,响应包括状态码、响应头、响应体等信息。
- 可靠性:HTTP在传输过程中使用TCP协议作为传输层协议,因此具有可靠性。TCP协议会确保数据的正确传输和接收,如果数据丢失或损坏,TCP会自动重传。
- 简单灵活:HTTP协议采用文本形式传输数据,易于阅读和调试。同时,HTTP协议也非常灵活,可以传输不同类型的数据,支持多种编码和内容类型。
- 支持缓存:HTTP协议支持缓存机制,通过在响应中添加缓存相关的头信息,可以让浏览器在下次请求相同资源时直接从缓存中获取,提高性能和加载速度。
HTTP协议的工作方式是客户端向服务器发送请求,服务器根据请求进行处理并返回响应。客户端和服务器通过URL(统一资源定位符)来定位资源,URL由协议类型(例如http)、服务器地址和资源路径组成。
HTTP协议是Web开发中非常重要的基础,它使得浏览器能够请求并获取Web页面、图片、视频、文件等资源,并实现了Web应用程序的交互性。同时,HTTP也不断发展,出现了新的版本,例如HTTP/1.1和HTTP/2,以满足不断增长的Web应用需求。
4.3.5 什么是幂等性
在计算机科学和网络编程中,幂等性(Idempotence)是指对同一个操作进行一次或多次的操作,产生的结果是相同的。换句话说,无论对一个操作进行多少次重复,其结果都是一致的。
幂等性在计算机系统设计和网络通信中具有重要意义,特别是在处理故障、网络延迟或重试等情况下。幂等性操作保证了系统对相同请求的重复处理不会导致副作用或错误结果。
举个简单的例子,假设有一个用于更新用户信息的API接口。如果这个API是幂等的,那么当多个请求同时更新同一个用户信息时,无论请求执行多少次,最终用户的信息都只会更新一次,而不会因为重复的请求导致用户信息被错误地更新多次。
在实际应用中,一些常见的幂等性操作包括:
- GET请求:GET请求是幂等的,因为对于同一个URL的GET请求,无论请求执行多少次,都只会返回相同的响应结果,不会对服务器产生任何副作用。
- PUT请求:PUT请求通常用于更新资源,在幂等性的设计下,对于同一个URL的PUT请求,重复执行对资源的更新操作将得到相同的结果。
- DELETE请求:DELETE请求通常用于删除资源,在幂等性的设计下,对于同一个URL的DELETE请求,重复执行对资源的删除操作将得到相同的结果。
幂等性的接口设计:在设计API接口时,如果接口的操作具有幂等性,可以增强系统的稳定性和可靠性。通过一些设计措施,比如生成唯一的请求标识,对于相同的请求标识只处理一次,就能实现幂等性。
总的来说,幂等性操作对于构建健壮和可靠的系统非常重要,可以避免重复操作导致的不一致性和错误结果。在网络通信中,幂等性的操作可以增加系统的容错性,确保请求的可靠传输和正确处理。