文章目录
- 抓包工具 Fiddler
- HTTP 请求和响应结构
- URL 唯一资源定位符
- HTTP 协议中的方法
- 请求报头(header)
- HTTP响应
- 构造 HTTP 请求
- 基于 form 标签
- 基于 ajax
- 使用 Postman
- HTTPS
- 和 HTTP 的区别
- 对称密钥和非对称密钥
- 数字证书
- Tomcat
- Servlet
- 创建 Maven 项目
- 引入依赖
- 创建目录
- 编写代码
- 打包
- 部署和验证
- 更快地部署:Smart Tomcat 插件
- 常见错误总结
- Servlet 详解
- HttpServlet
- HttpServletRequest
- 构造和解析 json
- HttpServletResponse
- 例子:自动刷新
- 例子:重定向
- 例子:表白墙服务器
- Cookie 和 Session
- 简介
- HttpServletRequest 类中的方法
- HttpServletResponse 类中的相关方法
- HttpSession 接口中的相关方法
- 例子:用户登录
- 上传文件
- HttpServletRequest 相关方法
- Part 类方法
抓包工具 Fiddler
Fiddler 是一款用于 Web 调试和网络流量分析的工具。它是一种代理服务器,可以捕获和检查从计算机到互联网之间的所有 HTTP 流量。Fiddler 可以帮助开发人员诊断问题、监视流量、修改请求和响应等。
前往官网下载安装即可使用:https://www.telerik.com/
这里下载 Classic 版,下载完成后点击 Tools->Options->HTTPS,将 4 个勾全部勾选上,使其可以抓 HTTPS 包
HTTP 请求和响应结构
随便访问一个页面,点开详细信息,查看 Raw
-
首行,请求/响应的第一行
HTTP/1.1 200 OK 版本 状态码 状态码描述
-
请求/响应报头 header
header 里面都是键值对,每个键值对占一行,键和值以 : 分隔
-
空行:
是请求/响应报头(header)的结束标记
报头里有多少个键值对(有多少行)是不确定的,所以以空行作为结束标记 -
请求/响应正文:
不是所有的 HTTP 请求都有正文
URL 唯一资源定位符
URL 里面如果没有写端口,此时浏览器就会给一个默认值:http:// 默认端口 80,https:// 默认端口 443
查询字符串:请求发给服务器的时候带的参数
?
作为查询字符串的起始标志,后面的内容就是查询字符串的本体- 通过键值对的方式来组织,键值对之间使用
&
来分割,键和值之间使用=
来分割
路径:指定了服务器上资源的具体位置
片段标识符:起到页面内部跳转的效果
HTTP 协议中的方法
方法 | 说明 |
---|---|
GET | 获取资源 |
POST | 传输实体主体 |
PUT | 传输文件 |
HEAD | 获得报文首部 |
DELETE | 删除文件 |
OPTIONS | 询问支持的方法 |
TRACE | 追踪路径 |
CONNECT | 要求用隧道协议连接代理 |
LINK | 建立和资源之间的联系 |
UNLINE | 断开连接关系 |
触发 GET 请求的场景:
- 浏览器地址栏输入一个 URL,回车
- HTML 标签,如 a,img,link,script
- form
- ajax
GET 的典型特点:
- URL 的 query string 有时候有,有时候没有
- body 通常是空
POST 的特点
- URL 里通常没有 query string
- 通常有 body
GET 和 POST 的区别
- 没有本质区别,它们之间可以相互替代
- 在使用习惯上存在区别:
- GET 主要用来获取数据,POST主要用来给服务器提交数据
- GET 主要通过 query string 传递数据,POST 使用 body 传递数据
- GET 请求一般建议实现成“幂等的”,POST 则不要求。幂等:多次输入的相同内容,得到的结果也相同,称为“幂等”
- GET 一般是可以被缓存的,POST 不要求
请求报头(header)
Host:表示服务器主机的地址和端口
Content-Length:表示 body 中的数据长度
Content-Type:表示请求的 body 中的数据格式
常见的 HTTP 请求的 body 数据格式
- json
- urlencoded
- form-data
User-Agent(UA):描述使用设备
Referer:表示这个页面是从哪个页面跳转过来的
Cookie:是浏览器在本地存储数据的一种机制,每个网站分配的cookie独立,内部存储的是键值对
HTTP响应
状态码
以下是一些常见的 HTTP 响应状态码:
- 1xx(信息性状态码):
- 100 Continue:服务器已经收到请求的头部,并且客户端应继续发送请求的其余部分。
- 2xx(成功状态码):
- 200 OK:请求成功,服务器已经成功处理了请求。
- 201 Created:请求已经被实现,而且有一个新的资源已经依据请求的需要而建立。
- 3xx(重定向状态码):
- 301 Moved Permanently:被请求的资源已被永久移动到新位置。
- 302 Found:请求的资源现在临时从不同的 URI 响应请求。
- 304 Not Modified:如果客户端的缓存是最新的,则返回此状态码。
- 4xx(客户端错误状态码):
- 400 Bad Request:服务器无法理解请求的语法。
- 401 Unauthorized:请求要求身份验证。
- 403 Forbidden:服务器理解请求,但拒绝执行。
- 404 Not Found:服务器无法找到请求的资源。
- 405 请求的方法不支持
- 5xx(服务器错误状态码):
- 500 Internal Server Error:服务器遇到不可预知的情况。
- 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效响应。
- 503 Service Unavailable:服务器目前无法处理请求。
- 504 超时
Content-Type
Content-Type
是 HTTP 响应报头之一,用于指示响应体的媒体类型(Media Type)或者内容类型。它告诉客户端如何解析响应体的数据。以下是一些常见的 Content-Type
值及其含义:
- text/plain: 纯文本,不包含任何格式的样式或标记。
- text/html: HTML 文档,用于网页内容。
- text/css: CSS 样式表,用于定义文档的样式和布局。
- text/javascript: JavaScript 脚本。
- application/json: JSON 格式,用于在客户端和服务器之间传输数据。
- application/xml: XML 格式,用于表示结构化的数据。
- image/jpeg、image/png、image/gif: 分别表示 JPEG、PNG、GIF 格式的图像。
- application/pdf: 表示 Adobe Portable Document Format(PDF)文件。
- application/octet-stream: 二进制流数据,通常用于传输不属于以上任何类型的二进制文件。
- multipart/form-data: 用于在 HTML 表单中上传文件。
构造 HTTP 请求
使用代码构造,主要是两种方式
基于 form 标签
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GET 请求表单</title>
</head>
<body>
<form action="/example" method="get">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<label for="age">Age:</label>
<input type="number" id="age" name="age" required>
<button type="submit">Submit</button>
</form>
</body>
</html>
action="/example"
指定了表单提交的目标 URL。method="get"
指定了使用 GET 请求。- 每个输入字段都有一个
name
属性,这些属性的值将作为参数发送到服务器。
当用户填写表单并点击 “Submit” 按钮时,浏览器将构造一个类似于以下的 URL:
/example?name=zhangsan&age=18
将 method="get"
改为 method="post"
,请求方法改为 post
表单数据将被包含在请求的消息体中,而不是作为 URL 的一部分。
注意:form 只支持 get 和 post
基于 ajax
Ajax(Asynchronous JavaScript and XML)是一种用于在 Web 页面中进行异步数据交换的技术。它允许在不刷新整个页面的情况下,通过后台与服务器进行数据交互,获取或发送数据。Ajax 的核心是通过 JavaScript 和 XML(尽管现在更常用 JSON)来实现异步通信。
jQuery 对 ajax 进行了封装,方便使用
首先引入 jQuery ,搜索 jQuery cdn,选择 minified,将 j s代码粘贴到本地,或者直接将远程 cdn 服务器上的js文件url引入。
$.ajax();
// jquery 构造 ajax 请求的核心函数
// $ 是一个变量名,是 jquery 中的一个特殊对象
// jquery 里面提供的各种 API 都是以 $ 对象的方法的方式来体现的
使用对象作为参数,例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>jQuery AJAX 示例</title>
<!-- 引入 jQuery 库 -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body>
<div id="result"></div>
<script>
// 使用 jQuery 发起 AJAX 请求
$.ajax({
url: 'https://jsonplaceholder.typicode.com/posts/1', // 服务器端 API 地址
type: 'GET', // 请求类型
dataType: 'json', // 预期的数据类型
success: function(data) {
// 请求成功时的处理
// 在这个例子中,我们将返回的数据显示在页面上
$('#result').html('<p>Title: ' + data.title + '</p><p>Body: ' + data.body + '</p>');
},
error: function(xhr, status, error) {
// 请求失败时的处理
console.error('Error: ' + status + ', ' + error);
}
});
</script>
</body>
</html>
使用 Postman
HTTPS
和 HTTP 的区别
- 安全性:
- HTTP: HTTP 是一种不加密的协议,数据在传输过程中是以明文形式传送的。这意味着如果攻击者能够截获网络通信,他们可以轻松地读取或修改传输的数据。因此,HTTP 不适合传输敏感信息,如登录凭证、支付信息等。
- HTTPS: HTTPS 使用了 TLS/SSL 协议进行加密,确保数据在传输过程中是加密的。这使得通过 HTTPS 传输的数据更难被窃取或篡改。HTTPS 是一种更安全的协议,适用于需要保护隐私和安全性的场景,如在线支付、登录等。
- 协议标识:
- HTTP: 使用标准的 “http://” URL 标识,通常在浏览器中显示为 “http://”.
- HTTPS: 使用标准的 “https://” URL 标识,通常在浏览器中显示为 “https://”. 还可能会显示一个锁图标,表示连接是安全的。
- 端口:
- HTTP: 默认使用端口 80。
- HTTPS: 默认使用端口 443。
- 证书:
- HTTP: 不需要使用数字证书。
- HTTPS: 需要使用由可信任的证书颁发机构(CA)颁发的数字证书。这个证书用于验证服务器的身份,确保连接是安全的。
- 性能:
- HTTP: 由于不涉及加密解密过程,通常比 HTTPS 稍微快一些。
- HTTPS: 由于数据加密和解密的额外开销,可能会稍微减缓数据传输速度。
对称密钥和非对称密钥
对称密钥和非对称密钥是两种常见的加密算法使用的密钥体制,它们分别用于加密和解密信息。
- 对称密钥(Symmetric Key):
- 定义: 对称密钥是一种使用相同密钥进行加密和解密的加密算法。加密和解密都使用相同的密钥,这就是为什么它被称为“对称”的原因。
- 特点: 算法简单、加解密速度快,但密钥的安全分发是一个挑战。因为发送方和接收方都必须事先共享相同的密钥,传输过程中,密钥也可能被黑客截获,存在安全风险。
- 示例: 常见的对称加密算法包括 DES(Data Encryption Standard)、AES(Advanced Encryption Standard)等。
- 非对称密钥(Asymmetric Key):
- 定义: 非对称密钥使用一对密钥,分别是公钥和私钥。公钥用于加密信息,而私钥用于解密信息。这使得公钥可以安全地发布,而私钥则必须保持机密。
- 特点: 相对于对称密钥,非对称密钥更安全,但由于其复杂性,加解密速度较慢。非对称密钥通常用于安全通信的密钥交换阶段。
- 示例: 常见的非对称加密算法包括 RSA(Rivest-Shamir-Adleman)、ECC(Elliptic Curve Cryptography)等。
数字证书
数字证书是一种用于验证网络通信中身份的安全工具,通常用于建立加密通信。数字证书是由称为证书颁发机构(Certificate Authority,简称CA)的可信任实体签发的一种电子文档,用于确认某个实体(通常是一个网络服务器)的身份。
数字证书包含了以下关键信息:
- 公钥: 数字证书包含一个公钥,用于加密和解密信息。公钥是一个用于加密数据的密码学密钥,同时也用于验证由该证书签发者签名的数据。
- 身份信息: 数字证书通常包含与公钥相关联的实体的身份信息,如域名(对于服务器证书)或个人信息(对于个人证书)。这个信息被称为主题(Subject)。
- 数字签名: 证书颁发机构使用其私钥对证书的内容进行数字签名,以确保证书的完整性和真实性。这个数字签名可以被其他人使用 CA 的公钥进行验证。
在网络通信中,数字证书的传输和验证通常涉及以下步骤:
- 握手阶段: 在建立安全连接(如HTTPS)时,通信的两端(客户端和服务器)会执行握手阶段。在这个阶段,双方协商加密算法、生成会话密钥等。
- 服务器发送证书: 在握手过程中,服务器将其数字证书发送给客户端。这个证书包含了服务器的公钥、服务器的身份信息(主题)、证书颁发机构的签名以及其他相关信息。
- 客户端验证证书: 客户端收到服务器的证书后,会验证证书的有效性。这个验证过程包括以下步骤:
- 验证数字签名: 客户端使用证书颁发机构的公钥验证证书的数字签名,确保证书的完整性和真实性。
- 验证有效期: 客户端检查证书的有效期,确保证书在当前时间内有效。
- 验证域名: 对于服务器证书,客户端会检查证书中的域名信息与实际连接的域名是否匹配,以防止钓鱼攻击。
- 生成共享密钥: 如果证书验证通过,客户端使用服务器的公钥加密一个随机生成的会话密钥,并将其发送给服务器。
- 服务器解密会话密钥: 服务器使用其私钥解密客户端发送的会话密钥,从而两端都获得了相同的会话密钥。
- 加密通信: 之后的通信将使用共享的会话密钥进行加密和解密,保障通信的保密性和完整性。
Tomcat
Apache Tomcat 是一个轻量级的开源 Java 应用服务器,用于托管和运行 Java Web 应用。
官方网站:https://tomcat.apache.org/
下载:这里选择 Tomcat 8.5 的版本
解压好进入 bin 目录,执行 startup 来启动服务器。如果启动失败,可以尝试在 cmd 中打开,会返回报错信息。
启动成功,打开浏览器访问 http://127.0.0.1:8080/ 可以看到 Tomcat 自带的页面
webapps 目录下存放我们自己写的 html 页面。
url 请求根目录为 webapps ,其中的 ROOT 是一个特殊的目录,如果请求的是 ROOT 里的页面,不用在 url 中加 ROOT
Servlet
Servlet 是一种用于在服务器端处理客户端请求的 Java 编程接口(API)。Servlets 是 Java 语言编写的服务器端程序,主要用于创建动态的网站。它们可以接收来自客户端的请求、处理请求并生成响应。
创建 Maven 项目
Apache Maven(通常称为 Maven)是一个用于管理项目构建、依赖关系和文档的开源项目管理工具。
IDEA 内置 Maven,创建项目时选上即可。
一个 Maven 项目的目录:
main 用于存放业务代码,test 用于存放测试代码
java 用于存放 java 代码,resources 存放项目中依赖的图片、音频、字体等资源文件
pom.xml 是 Maven 项目的核心文件,描述了这个项目的属性信息
引入依赖
去 Maven Repository 搜索 servlet,选择 3.1.0 版本
将方框内的代码复制粘贴到 pom.xml 里面,具体步骤是在<project>
标签内创建 <dependencies>
标签,将代码粘贴到<dependencies>
内。
复制进去,点击刷新按钮,就会自动开始下载了。
默认的下载位置是家目录下的 .m2 文件中
创建目录
为了我们的程序能被 Tomcat 识别,需要创建如下的目录结构
在 main 目录下创建 webapp 目录,在其中创建 WEB-INF 目录,在其中创建 web.xml 文件
web.xml 中复制如下内容:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
html 文件就是存放在 webapp 目录下
编写代码
接下来我们可以在 java 目录中创建类了
创建一个类,继承 HttpServlet
并重写 doGet
方法。
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Hello world"); // 打印到服务器的控制台
resp.getWriter().write("Hello world"); // 打印到响应报文,显示在页面上
}
}
-
Tomcat 会自动识别合适的时机来调用 doGet 方法(合适的时机比较复杂,但可以肯定一定是通过 GET 请求触发),参数 req 和 resp 包含了 HTTP 请求响应的信息。
回顾一下网络编程的步骤:
- 读取请求并解析
- 根据请求计算响应
- 将响应返回给客户端
其中的1、3两步都是由 Tomcat 自动完成,第 2 步由我们自己重写的 doGet 方法完成
-
@WebServlet("/hello")
注解,用来约定 HTTP 请求的 url 是什么样的 path 才会调用到当前这个 Servlet 类
打包
点开右边的 m 图标,展开 Lifecycle,双击 package 或者右键 package 运行。
接下来它就会执行从 clean 到 package 的过程
在 target 目录下可以找到最终生成的 jar 包
然而 jar 包并不能被 Tomcat 识别,Tomcat 的可以识别的是 war 包,需要到 pom.xml 中修改配置,让它生成 war 包:
在 <project>
标签里添加 :
<packaging>war</packaging>
<build>
<finalName>hello_servlet</finalName>
</build>
<packaging>
标签指定了生成 war 包,<build>
中的 <finalName>
标签指定生成的包的名字
重新双击 package,打包操作完成。
部署和验证
把刚才得到的 war 包,拷贝到 Tomcat 的 webapps 目录中
然后启动 Tomcat,启动时会自动对我们拷贝的 war 包进行解压,生成文件夹。
打开浏览器,指定好路径,进行验证:
/hello_servlet/hello 路径:
- 其中 /hello_servlet 是 webapps 里的目录,又叫上下文路径。
- /hello 是
@WebServlet("/hello")
注解里的路径,又叫 Servlet 路径
更快地部署:Smart Tomcat 插件
Smart Tomcat 插件可以帮我们省略打包、部署和启动 Tomcat 的过程。
安装好 Smart Tomcat 插件后,进入 Edit Configurations…
点 + 号,选择 Smart Tomcat
配置 Tomcat 路径,其他保持默认
点击 OK 之后,右上角出现配置好的Smart Tomcat,点击绿色三角按钮即可自动启动 Tomcat 并完成部署。
使用 Smart Tomcat 插件,Context path 就是在这个页面里面设置的。
常见错误总结
- 404 大概率是 url 写错,也有可能是 webapp 没有加载正确(比如 web.xml 写错了)
- 405 可能收到 GET 请求,但是你没有实现 doGet。或者写了 doGet,但是没有把 super.doGet 这一行删掉
- 500 服务器内部错误,比如你的代码抛异常了
- 返回空白页面,可能是忘记填写 resp
- 无法访问此网站,可能是 Tomcat 没有正确启动
Servlet 详解
HttpServlet
方法 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化后被调用一次 |
destroy | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求时调用 |
doGet | 收到 GET 请求时调用(由 service 方法调用) |
doPost | 收到 POST 请求时调用(由 service 方法调用) |
doPut/doDelete/doOptions/... | 收到其他请求时调用(由 service 方法调用) |
HttpServlet 的生命周期:
- 首次使用,先调用一次
init
- 每次收到请求,调用
service
,在service
内部通过方法来决定调用哪个 doXXX - 销毁之前调用
destroy
HttpServletRequest
方法 | 说明 |
---|---|
String getProtocol() | 返回请求使用的协议的名称和版本 |
String getMethod() | 返回请求的HTTP方法的名称 |
String getRequestURI() | 返回此请求的URL的一部分 |
String getContextPath() | 返回请求URI中指示请求上下文的部分 |
String getQueryString() | 返回包含在请求URL的路径后的查询字符串 |
Enumeration<String> getParameterNames() | 返回包含参数名称的 String 对象的枚举 |
String getParameter(String name) | 以字符串形式返回请求参数的值,如果该参数不存在,则返回null |
String[] getParameterValues(String name) | 返回一个字符串数组,包含给定请求参数的所有值,如果该参数不存在,则返回null |
Enumeration<String> getHeaderNames() | 返回此请求包含的所有标头名称的枚举。如果请求没有标头,则此方法返回一个空枚举。 |
String getHeader(String name) | 以字符串形式返回指定请求头的值 |
String getCharacterEncoding() | 返回此请求正文中使用的字符编码的名称 |
String getContentType() | 返回请求正文的 MIME 类型,如果类型未知,则返回 null |
int getContentLength() | 返回请求主体的长度(以字节为单位),并由输入流提供;如果长度未知,则返回-1 |
ServletInputStream getInputStream() | 使用 ServletInputStream 将请求的正文检索为二进制数据 |
例子:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet("/showRequest")
public class ShowRequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(req.getProtocol());
stringBuilder.append("<br>");
stringBuilder.append(req.getMethod());
stringBuilder.append("<br>");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("<br>");
stringBuilder.append(req.getContextPath());
stringBuilder.append("<br>");
stringBuilder.append(req.getQueryString());
stringBuilder.append("<br>");
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
stringBuilder.append(name).append(": ").append(req.getHeader(name)).append("<br>");
}
resp.setContentType("text/html");
resp.getWriter().write(stringBuilder.toString());
}
}
结果:
回显 query string:
@WebServlet("/studentInfo")
public class StudentInfoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String queryString = req.getQueryString();
resp.getWriter().write(queryString);
String classId = req.getParameter("classId");
String studentId = req.getParameter("studentId");
resp.getWriter().write("classId: " + classId + "studentId: " + studentId);
}
}
对 POST 请求获取 body 中的参数,使用的依然是 getParameter
方法:
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html; charset=utf8");
// 获取 body 中的参数
// 约定使用 application/x-www-form-urlencoded 格式传参
// 这个格式和 query string 相同,只是数据在 body 中
String classId = req.getParameter("classId");
String studentId = req.getParameter("studentId");
resp.getWriter().write("classId: " + classId + "studentId: " + studentId);
}
使用 Postman 来构造 POST 请求,获取返回结果
构造和解析 json
处理 json 需要用到第三方库,这里使用 Jackson,去 Maven 仓库搜索然后引入自己的项目中就可以了。
处理 Json 的核心只有两个方法:
- 将 Json 转换为 Java 对象——readValue
- 将 Java 对象转换为 Json——writeValue
解析 json:
转换成的 Java 对象是什么类,需要事先定义:
class Student {
public int classId;
public int studentId;
}
注意:属性名要和 json 中的 key 一致
然后将使用 readValue
将 json 转换成 Java 对象
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
// 从请求的 body 中进行读取并解析
// 使用 readValue 把 json 字符串转成 Java 对象
Student student = objectMapper.readValue(req.getInputStream(), Student.class);
resp.getWriter().write(student.classId + ", " + student.studentId);
}
使用 Postman 构造 Json 格式的 body,可以看到结果正确返回
HttpServletResponse
方法 | 说明 |
---|---|
void setStatus(int sc) | 设置此响应的状态码 |
void setHeader(String name, String value) | 设置具有给定名称和值的响应 header. 如果已经设置了 header,则新值将覆盖上一个值 |
void addHeader(String name, String value) | 添加具有给定名称和值的响应 header. 此方法允许响应标头具有多个值 |
void setContentType(String type) | 如果尚未提交响应,则设置发送到客户端的响应的内容类型 |
void setCharacterEncoding(String charset) | 设置发送到客户端的响应的字符编码(MIME字符集) |
void sendRedirect(String location) | 使用指定的重定向位置 URL 向客户端发送临时重定向响应并清除缓冲区 |
PrintWriter getWriter() | 返回一个PrintWriter对象,该对象可以向客户端发送字符文本 |
ServletOutputStream getOutputStream() | 返回适用于在响应中写入二进制数据的 Servlet 输出流。 |
例子:自动刷新
设置 header 中的 refresh 和刷新时间,可以让浏览器自动刷新
@WebServlet("/refresh")
public class RefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("Refresh", "1");
resp.getWriter().write(System.currentTimeMillis() + "");
}
}
每刷新一次就会请求新的时间戳
例子:重定向
设置状态码为 3xx(典型的是302)
给 header 里设置一个 Location,表示跳转到的页面
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(302);
resp.setHeader("Location", "https://www.baidu.com/");
}
}
更简单的写法:
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.sendRedirect("https://www.baidu.com/");
}
}
例子:表白墙服务器
实现表白墙服务器,并且提交的数据可以持久化到数据库中。
前端页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>表白墙</title>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.container {
width: 800px;
margin: 10px auto;
}
.container h2 {
text-align: center;
margin: 30px 0px;
}
.row {
height: 50px;
display: flex;
justify-content: center;
margin-top: 5px;
line-height: 50px;
}
.row span {
height: 50px;
width: 100px;
line-height: 50px;
}
.row input {
height: 50px;
width: 300px;
line-height: 50px;
}
.row button {
width: 400px;
height: 50px;
color: white;
background-color: orange;
border: none;
border-radius: 10px;
}
.row button:active {
background-color: grey;
}
</style>
</head>
<body>
<!-- 这是一个顶层容器, 放其他元素 -->
<div class="container">
<h2>表白墙</h2>
<div class="row">
<span>谁</span>
<input type="text" id="from">
</div>
<div class="row">
<span>对谁</span>
<input type="text" id="to">
</div>
<div class="row">
<span>说什么</span>
<input type="text" id="message">
</div>
<div class="row">
<button>提交</button>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.1.min.js"></script>
<script>
let container = document.querySelector('.container');
let fromInput = document.querySelector('#from');
let toInput = document.querySelector('#to');
let messageInput = document.querySelector('#message');
let button = document.querySelector('button');
button.onclick = function() {
// 1. 把用户输入的内容获取到.
let from = fromInput.value;
let to = toInput.value;
let message = messageInput.value;
if (from == '' || to == '' || message == '') {
return;
}
// 2. 构造一个 div, 把这个 div 插入到 .container 的末尾
let newDiv = document.createElement('div');
newDiv.className = 'row';
newDiv.innerHTML = from + " 对 " + to + " 说: " + message;
// 3. 把 div 挂在 container 里面
container.appendChild(newDiv);
// 4. 把之前的输入框内容进行清空
fromInput.value = '';
toInput.value = '';
messageInput.value = '';
// 5. 把输入框里取到的数据, 构造成 POST 请求, 交给后端服务器!
let messageJson = {
"from": from,
"to": to,
"message": message
};
$.ajax({
type: 'post',
// 相对路径的写法
url: 'message',
contentType: 'application/json;charset=utf8',
// 绝对路径的写法
// url: '/MessageWall/message',
data: JSON.stringify(messageJson),
success: function(body) {
alert("提交成功!");
},
error: function() {
// 会在服务器返回的状态码不是 2xx 的时候触发这个 error.
alert("提交失败!");
}
});
}
// 这个函数在页面加载的时候调用. 通过这个函数从服务器获取到当前的消息列表.
// 并且显示到页面上.
function load() {
$.ajax({
type: 'get',
url: 'message',
success: function(body) {
// 此处得到的 body 已经是一个 js 对象的数组了.
// ajax 自动帮我们进行类型转换了.
// 遍历数组的元素, 把内容构造到页面上.
let container = document.querySelector('.container');
for (let message of body) {
let newDiv = document.createElement('div');
newDiv.className = 'row';
newDiv.innerHTML = message.from + " 对 " + message.to + " 说 " + message.message;
container.appendChild(newDiv);
}
}
})
}
// 函数调用写在这里, 就表示在页面加载的时候来进行执行.
load();
</script>
</body>
</html>
对 JDBC 的封装:
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// 期望通过这个类来完成数据库建立连接的过程.
// 建立连接需要使用 DataSource . 并且一个程序有一个 DataSource 实例即可. 此处就使用单例模式来实现.
public class DBUtil {
private static DataSource dataSource = null;
private static DataSource getDataSource() {
if (dataSource == null) {
dataSource = new MysqlDataSource();
((MysqlDataSource)dataSource).setURL("jdbc:mysql://127.0.0.1:3306/MessageWall?characterEncoding=utf8&useSSL=false");
((MysqlDataSource)dataSource).setUser("root");
((MysqlDataSource)dataSource).setPassword("2222");
}
return dataSource;
}
public static Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
// 此处还是推荐大家写成分开的 try catch.
// 保证及时一个地方 close 异常了, 不会影响到其他的 close 的执行.
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
后端 java 代码:
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
// 对应到前端传来的请求的 body 格式.
// 此处要保证, 每个属性的名字都和 json 里的 key 对应 (顺序可以不一样)
// 同时也要保证这几个属性是 public 或者提供 public 的 getter 方法
class Message {
public String from;
public String to;
public String message;
@Override
public String toString() {
return "Message{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", message='" + message + '\'' +
'}';
}
}
@WebServlet("/message")
public class MessageServlet extends HttpServlet {
// 由于 ObjectMapper 会在多个方法中使用, 就提出来, 作为成员变量
private ObjectMapper objectMapper = new ObjectMapper();
// 负责实现客户端提交数据给服务器.
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 把 body 的 json 数据解析出来.
Message message = objectMapper.readValue(req.getInputStream(), Message.class);
// 2. 把这个对象保存起来.
save(message);
System.out.println("message: " + message);
// 3. 返回保存成功的响应
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write("{ \"ok\": 1 }");
}
// 负责实现客户端从服务器拿到数据.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 由于约定的请求, 没有参数, 不需要进行任何解析操作
resp.setContentType("application/json;charset=utf8");
// 把对象转成 json 格式的字符串. 此处 messageList 是一个 List, 直接就被转成 json 数组了.
List<Message> messageList = load();
String respString = objectMapper.writeValueAsString(messageList);
resp.getWriter().write(respString);
}
// 把当前的消息存到数据库中
private void save(Message message) {
Connection connection = null;
PreparedStatement statement = null;
try {
// 1. 和数据库建立连接
connection = DBUtil.getConnection();
// 2. 构造 SQL 语句
String sql = "insert into message values(?, ?, ?)";
statement = connection.prepareStatement(sql);
statement.setString(1, message.from);
statement.setString(2, message.to);
statement.setString(3, message.message);
// 3. 执行 SQL 语句
int ret = statement.executeUpdate();
if (ret != 1) {
System.out.println("插入失败!");
} else {
System.out.println("插入成功!");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 4. 关闭连接.
DBUtil.close(connection, statement, null);
}
}
// 从数据库查询到记录
private List<Message> load() {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
List<Message> messageList = new ArrayList<>();
try {
// 1. 建立连接
connection = DBUtil.getConnection();
// 2. 构造 SQL
String sql = "select * from message";
statement = connection.prepareStatement(sql);
// 3. 执行 SQL
resultSet = statement.executeQuery();
// 4. 遍历结果集
while (resultSet.next()) {
Message message = new Message();
message.from = resultSet.getString("from");
message.to = resultSet.getString("to");
message.message = resultSet.getString("message");
messageList.add(message);
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
// 5. 释放资源
DBUtil.close(connection, statement, resultSet);
}
return messageList;
}
}
Cookie 和 Session
简介
Cookie 经典应用场景就是用户登录,我们访问很多网站都只要第一次输入一下用户名密码,而后续访问这个网站一进去就已经是登录状态,这就用到了 Cookie
Cookie 使用键值对来保存信息,键是服务器自动生成的一串唯一的字符串,叫做 sessionId,值就是用户的详细信息。服务器只需要把键通过 Set-Cookie 返回给浏览器,就可以验证用户身份。
流程如图:
- cookie 存在客户端,session 存在服务端,sessionId 在客户端和服务端都有
HttpServletRequest 类中的方法
方法 | 说明 |
---|---|
HttpSession getSession() | 返回与此请求关联的当前会话,如果请求没有会话,则创建一个会话 |
HttpSession getSession(boolean create) | 返回与此请求关联的当前会话,如果没有当前会话并且 create 为 true ,则返回新会话,如果没有当前会话并且 create 为 false ,则返回 null |
Cookie[] getCookies() | 返回一个数组,该数组包含客户端随此请求发送的所有 Cookie 对象。如果未发送 cookie,此方法将返回 null |
HttpServletResponse 类中的相关方法
方法 | 说明 |
---|---|
void addCookie(Cookie cookie) | 将指定的 cookie 添加到响应中。可以多次调用此方法来设置多个 cookie |
HttpSession 接口中的相关方法
方法 | 说明 |
---|---|
void setAttribute(String name, Object value) | 使用指定的名称将对象绑定到此会话。如果具有相同名称的对象已绑定到会话,则会替换该对象 |
Object getAttribute(String name) | 返回此会话中与指定名称绑定的对象,如果该名称下未绑定任何对象,则返回 null |
这两个方法一个是设置键值对,一个是根据键获取对应的值
例子:用户登录
包含一个登录页和一个主页,登录页可以输入用户名和密码,服务器验证正确性,如果正确就跳转到主页,主页显示当前用户的身份信息,并且显示出当前用户登录后的访问次数。实现用户登录一次后,不必重新登录。
一个简单的登录页面 login.html:用 form 构造 post 请求向服务器提交数据
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录页面</title>
</head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="提交">
</form>
</body>
</html>
package login;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
// 处理登录请求
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
// 读取请求中的参数,判定当前用户的身份信息是否正确
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html; charset=utf8");
String username = req.getParameter("username");
String password = req.getParameter("password");
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
// 参数不正确
resp.sendRedirect("用户名或密码不完整,登录失败");
return;
}
// 验证用户名密码正确性
if (!username.equals("zhangsan") || !password.equals("123")) {
// 登录失败
resp.getWriter().write("用户名或密码错误,登录失败");
return;
}
// 登录成功
// 创建会话,把用户信息填入 session
HttpSession session = req.getSession();
session.setAttribute("username", "zhangsan");
Integer visitCount = (Integer) session.getAttribute("visitCount");
if (visitCount == null) {
session.setAttribute("visitCount", 0);
}
// 这里的页面跳转是一个 GET 请求
resp.sendRedirect("index");
}
}
package login;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 将当前的用户信息展示到页面上
HttpSession session = req.getSession(false);
if (session == null) {
// 没有对应的session,重新登录
resp.sendRedirect("login.html");
return;
}
// 登录
String username = (String) session.getAttribute("username");
Integer visitCount = (Integer) session.getAttribute("visitCount");
++visitCount;
session.setAttribute("visitCount", visitCount);
resp.setContentType("text/html; charset=utf8");
resp.getWriter().write("当前用户为" + username + "访问次数" + visitCount);
}
}
使用 Fiddler 抓包查看过程:
第一次请求:首次访问 login.html 页面:
GET http://127.0.0.1:8080/hello_servlet/login.html HTTP/1.1
请求里面没有cookie
HTTP/1.1 304
响应也没有 Set-Cookie。
第二次请求:输入用户名密码,提交:
POST http://127.0.0.1:8080/hello_servlet/login HTTP/1.1
可以看到确实是 POST 请求
username=zhangsan&password=123
请求 body 部分。
这个请求中也没有 Cookie。
HTTP/1.1 302
Set-Cookie: JSESSIONID=92263EBADDECE1E20FADE869B7715301; Path=/hello_servlet; HttpOnly
响应中出现了 Set-Cookie,JSESSIONID 是 Servlet 自动生成的 key,= 后面的就是 sessionId 了
第三次请求:重定向到主页
GET http://127.0.0.1:8080/hello_servlet/index HTTP/1.1
...
...
Cookie: JSESSIONID=92263EBADDECE1E20FADE869B7715301
这一次请求,有 Cookie 了,后续反复请求,都会带有这个 Cookie 了
上传文件
HttpServletRequest 相关方法
方法 | 说明 |
---|---|
Part getPart(String name) | 获取请求中具有给定名称的 Part |
Collection<Part> getParts() | 获取此请求的所有 Part 组件,前提是该请求的类型为multipart/form-data。 |
Part 类方法
方法 | 说明 |
---|---|
String getSubmittedFileName() | 获取由客户端指定的文件名 |
String getContentType() | 获取此 part 的内容类型 |
long getSize() | 返回此文件的大小 |
void write(String fileName) | 将此上传项目写入磁盘的一种方便方法 |
例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<form action="upload" method="post" enctype="multipart/form-data">
<input type="file" name="MyFile">
<input type="submit" value="上传">
</form>
</body>
</html>
注意:一定要写 enctype="multipart/form-data"
才能上传文件
package upload;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
@MultipartConfig
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Part part = req.getPart("MyFile");
System.out.println(part.getSubmittedFileName());
System.out.println(part.getSize());
System.out.println(part.getContentType());
part.write("d:/result.jpg");
resp.getWriter().write("upload ok");
}
}
注意:一定要加 @MultipartConfig
注解
结果:
网页显示:
upload ok
控制台输出:
刺客信条:英灵殿2022-9-16-0-46-30.jpg
477980
image/jpeg
查看 D 盘,确实有刚刚上传的 result.jpg,查看也确实是上传的图片
抓包查看上传文件时的 HTTP 请求:
POST http://localhost:8080/hello_servlet/upload HTTP/1.1
......
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBSLCs05c8ots3YZN
......
------WebKitFormBoundaryBSLCs05c8ots3YZN
Content-Disposition: form-data; name="MyFile"; filename="刺客信条:英灵殿2022-9-16-0-46-30.jpg"
Content-Type: image/jpeg
...图片内容,在这里显示是乱码...
------WebKitFormBoundaryBSLCs05c8ots3YZN--
- 可以看到确实是 POST 请求,请求的 url 也没问题。
- 在 Content-Type 里可以看到类型是 multipart/form-data,boundary 是边界,表示上传文件的起始和结束位置