一、HTTP
- HTTP(Hyper Text Transfer Protocol), 译为
超文本传输协议
- 是互联网中应用最广泛的应用层协议之一
- 设计HTTP最初的目的是: 提供一种发布和接收
HTML页面
的方法,由URI
来标识具体的资源 - 后面用
HTTP
来传递的数据格式不仅仅是HTML
,应用非常广泛
- HTML(Hyper Text Markup Language): 超文本标记语言, 用以编写网页
- 维基百科
二、版本
- 1991年, HTTP/0.9
- 只支持GET请求方法获取文本数据(比如HTML文档), 且不支持请求头、响应头等, 无法向服务器传递太多信息
- 1996年, HTTP/1.0
- 支持POST、HEAD等请求方法, 支持请求头、响应头等, 支持更多种数据类型(不再局限于文本数据)
- 浏览器的每次请求都需要与服务器建立一个TCP连接, 请求处理完成后立即断开TCP连接
- 1997年, HTTP/1.1(最经典、使用最广泛的版本)
- 支持PUT、DELETE等请求方法
- 采用持久连接(Connection: keep-alive), 多个请求可以共用同一个TCP连接
- 2015年, HTTP/2.0
- 2018年, HTTP/3.0
三、标准
- HTTP的标准
- 由万维网协会(W3C)、互联网工程任务组(IETF)协调制定, 最终发布了一系列的RFC
- RFC(Request For Comments, 可以译为: 请求意见稿)
- 中国的RFC
- 1996年3月,清华大学提交的适应不同国家和地区中文编码的汉字统一传输标准被IETF通过为RFC 1922
- 成为中国大陆第一个被认可为RFC文件的提交协议
四、报文格式
1、运行项目
- 打开终端, 启动安装的
Tomcat
- 在浏览器中输入
127.0.0.1:8080
, 看到下面的内容, 说明已经启动成功
- 可以打开网络协议基础学习(一): 环境搭建中创建的JAVA项目
01_HelloWorld
, 点击Debug
, 启动服务器
- 浏览器自动打开
http://localhost:8888/hello/
, 我这里将JAVA项目部署在了8888
端口
- 浏览器输入
http://localhost:8888/hello/image/123.jpg
, 加载image
文件夹中的123.jpg
图片
- 浏览器输入
http://localhost:8888/hello/file/login.html
, 加载file
文件夹中的login.html
文件
- 输入,
用户名: 123
,密码: 456
, 点击登录, 展示Login Success!
2、抓取数据
- 打开
Wireshark
, 选择环回地址
抓取数据
- 在浏览器中输入
http://localhost:8888/hello/file/login.html
- 输入用户名: 123, 密码: 456, 点击登录, 可以看到下面的数据
- 在
Wireshark
中, 选择抓取到的请求数据, 可以看到请求头中, 每一行的最后都有\r\n
字符, 即回车符
和换行符
, 并且在请求体中可以看到username = 123
和password = 456
- 查看真实的字节数据, 可以看到
\r
和\n
两个符号, 就是字节0x0d
和0x0a
- 在
Wireshark
中选择抓取到的数据, 右键点击 -> 选择Follow -> 选择HTTP Stream, 查看抓取到的HTTP数据流
- HTTP数据流, 如下图所示
- 红色部分: 请求数据
- 蓝色部分: 响应数据
- 每一行最后的
\r\n
都显示成了换行
- 请求头和请求体被
\r\n
分割 - 响应头和响应体被
\r\n
分割
- 请求报文
POST /hello/login HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Content-Length: 25
Cache-Control: max-age=0
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8888
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8888/hello/file/login.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=E2489124D3F1FFCA4639CDFE3FCA27A6; Idea-ca754424=0e5e47c5-a13b-4034-97e1-7ef77253e0ca
username=123&password=456
- 响应报文
HTTP/1.1 200
Content-Length: 14
Date: Thu, 06 Jan 2022 01:47:03 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Login Success!
3、ABNF
-
ABNF(Augmented BNF)
- 是BNF(Backus-Naur Form, 译为: 巴科斯-瑙尔范式)的修改、增强版
- 在RFC 5234中表明: ABNF用作internet中通信协议的定义语言
- ABNF是最严谨的HTTP报文格式描述形式, 脱离ABNF谈论HTTP报文格式, 往往都是片面、不严谨的
-
关于HTTP报文格式的定义
-
打开RFC 7230 3.Message Format(新), 可以看到如下图中的报文格式
4、整体
- 报文格式-整体, 如下图所示
5、ABNF-核心规则
- 下面是ABNF中使用到的符号
规则 | 形式定义 | 意义 |
---|---|---|
ALPHA | %x41-5A / %x61-7A | 大写和小写ASCII字母 (A-Z, a-z) |
DIGIT | 0x30-39 | 数字 (0-9) |
HEXDIG | DIGIT / "A" / "B" / "C" / "D" / "E"/ "F" | 十六进制数字 (0-9, A-F, a-f) |
DQUOTE | %x22 | 双引号 |
SP | %x20 | 空格 |
HTAB | %x09 | 横向制表符 |
WSP | SP / HTAB | 空格或横向制表符 |
LWSP | *(WSP / CRLF) | 直线空白 (晚于换行) |
VCHAR | %x21-7E | 可见(打印)字符 |
CHAR | %x01-7F | 任何7-位US-ASCII字符, 不包括NUL(%0x00) |
OCTET | %x00-FF | 8位数据 |
CTL | %x00-1F / %x7F | 控制字符 |
CR | %x0D | 回车 |
LF | %x0A | 换行 |
CRLF | CR LF | 互联网标准换行(%x0D%x0A) |
BIT | "0" / "1" | 二进制数字 |
5、start-line: request-line
start-line
: 表示开始行, 根据请求报文和响应报文, 分别有不同的内容- 请求报文的开始行, 又称为
请求行
, 格式如下所示
request-line = method SP request-target SP HTTP-version CRLF
HTTP-version = HTTP-name "/" DIGIT "." DIGIT
HTTP-name = %x48.54.54.50 ; HTTP
- 符号描述
符号 | 描述 |
---|---|
method | 请求的方法名, 例如: GET、POST |
SP | 空格 |
request-target | 请求目标, 例如: /login/login |
HTTP-version | HTTP的版本号 |
CRLF | 回车换行符号\r\n, 即: 0x0d、0x0a |
DIGIT | 数字, 例如: 1 |
HTTP-name | HTTP的名称, 固定值: HTTP, 对应字节: 0x48、0x54、0x54、0x50 |
- 内容组成如下
请求行 = 方法 + 空格 + 请求目标 + 空格 + HTTP版本号 + 回车换行
HTTP版本号 = HTTP名称 + / + 数字 + . + 数字
HTTP名称 = HTTP
- 例如上面抓取到的请求数据第一行
- 方法:
POST
- 空格
- 请求目标:
/hello/login
- 空格
- HTTP版本:
HTTP/1.1
- 回车换行:
\r\n
- 方法:
POST /hello/login HTTP/1.1\r\n
6、start-line: status-line
- 响应报文的开始行, 又称为
状态行
, 格式如下
status-line = HTTP-version SP status-code SP reason-phrase CRLF
status-code = 3DIGIT
reason-phrase = *( HTAB / SP / VCHAR / obs-text )
- 符号描述
符号 | 描述 |
---|---|
HTTP-version | HTTP版本号, 例如: HTTP/1.1 |
SP | 空格 |
DIGIT | 数字 |
status-code | 3个数字, 例如: 200、302、404 |
reason-phrase | 一个可能为空的描述状态码的文本短语, 例如: HTAB/SP/VCHAR/obs-text |
CRLF | 回车空格符号\r\n, 即: 0x0d、0x0a |
HTAB | tab键 |
SP | 空格 |
VCHAR | 字符串文本 |
obs-text | 字符串文本 |
- 内容组成如下:
状态行 = HTTP版本 + 空格 + 状态码 + 空格 + 描述状态码的文本短语 + 回车换行
状态码: 由3位数字组成
描述状态码的文本短语: 可以为空, 可以是tab、空格或字符串文本
- 例如上面抓取到的响应数据第一行
- HTTP版本号: HTTP/1.1
- 空格
- 状态码: 200
- 空格
- 描述状态码的文本短语: 空
- 回车换行: \r\n
HTTP/1.1 200 \r\n
7、*(header-field CRLF)
- *(header-field CRLF):
请求头
或响应头
中的键值对, 可以没有, 也可以有多个, 以\r\n
结尾
header-field = field-name ":" OWS field-value OWS
field-name = token
field-value = *( field-content / obs-fold )
field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
field-vchar = VCHAR / obs-text
obs-fold = CRLF 1*( SP / HTAB )
; obsolete line folding
; see Section 3.2.4
OWS = *( SP / HTAB )
注意: RFC中的分号";", 表示注释
- 符号描述
符号 | 描述 |
---|---|
field-name | 键值对中的Key, 例如: Accept |
field-value | 键值对中的value, 0个或多个, 如果有多个就用逗号","分隔, 例如: text/html |
OWS | 空格或tab, 可以没有, 也可以有多个 |
field-vchar | 字符串文本 |
obs-fold | 已经过时, 不再使用, 可以看3.2.4 |
- 内容组成如下:
header-field = 键 + 冒号 + 0个或多个空格/tab + 值 + 0个或多个空格/tab + \r\n
- 例如上面抓取到的部分请求头数据, key-value中的value, 可以有多个, 用逗号"
,
"分隔
Host: localhost:8888 \r\n
Connection: keep-alive \r\n
Content-Length: 25 \r\n
Cache-Control: max-age=0 \r\n
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91" \r\n
sec-ch-ua-mobile: ?0 \r\n
8、message-body
- 消息体: 请求体 或 响应体
- 格式
message-body = *OCTET
- 符号描述
符号 | 描述 |
---|---|
OCTET | 占八位的字节, 可以没有, 也可以有多个 |
- 内容组成如下:
消息体: 0个或多个的字节
Wireshark
抓到的请求体
username=123&password=456
Wireshark
抓到的响应体
Login Success!
9、HTTP报文结构图
- 请求报文格式模拟图, 如下所示
- 响应报文格式模拟图, 如下所示
10、URL的编码
- URL中一旦出现了一些特殊字符(比如中文、空格),需要进行编码
- 在浏览器地址栏输入URL时,是采用UTF-8进行编码
- 比如:
11、telent
- 使用
telent
, 可以直接面向HTTP报文与服务器交互- 可以更清晰、直观地看到请求报文、响应报文的内容
- 可以检验请求报文格式的正确与否
- MAC电脑, 先安装
Homebrew
, 可以看这篇: 小码哥-音视频学习笔记(第三天): Mac安装Homebrew - 然后使用
Homebrew
安装telent
brew install telent
- 使用
telent
连接本机的8888
端口, 也就是上面服务器项目部署的端口
- 使用
GET
方法连接hello
项目, 可以看到返回的结果- 注意:
hello
左右都必须有/
, 即/hello/
, 这是格式要求
- 注意:
GET /hello/ HTTP/1.1
Host: localhost:8888
- 在没有指定具体文件或接口的情况下, 会默认加载服务器中
index.jsp
文件
- 加载
file
文件夹中login.html
文件
GET /hello/file/login.html HTTP/1.1
Host: localhost:8888
- 文件内容:
- 使用
GET
方法请求login
接口, 并传递参数
GET /hello/login?username=123&password=456 HTTP/1.1
Host: localhost:8888
login
接口的具体实现
- 查看
8888
端口支持的所有方法
OPTIONS * HTTP/1.1
Host: localhost:8888
- 也可以查看服务器支持的方法
OPTIONS /hello/ HTTP/1.1
Host: localhost:8888
五、请求方法
- RFC 7231, section 4:Request methods: 描述了8种请求方法
- GET、HEAD、POST、PUT、DELETE、CONNECT、OPTIONS、TRACE
- 9RFC 5789, section 2: Patch method: 描述了PATCH方法
方法 | 描述 |
---|---|
GET | 常用于读取的操作, 请求参数直接拼接在URL的后面(浏览器对URL是有长度限制的) |
POST | 常用于添加、修改、删除的操作, 请求参数可以放到请求体中(没有大小限制) |
HEAD | 请求得到与GET请求相同的响应, 但没有响应体 使用场景举例: 在下载一个大文件前, 先获取其大小, 再决定是否要下载。以此可以节约带宽资源请求方法 |
OPTIONS | 用于获取目的资源所支持的通信选项, 比如服务器支持的请求方法 OPTIONS * HTTP/1.1 |
PUT | 用于对已存在的资源进行整体覆盖 |
PATCH | 用于对资源进行部分修改(资源不存在,会创建新的资源) |
DELETE | 用于删除指定的资源 |
TRACE | 请求服务器回显其收到的请求信息, 主要用于HTTP请求的测试或诊断 |
CONNECT | 可以开启一个客户端与所请求资源之间的双向沟通的通道, 它可以用来创建隧道(tunnel) 可以用来访问采用了 SSL (HTTPS) 协议的站点 |
六、头部字段
1、请求头字段
- 常用请求头字段如下图所示:
2、响应头字段
- 常用响应头字段如下图所示:
七、状态码
-
100 Continue
- 请求的初始部分已经被服务器收到,并且没有被服务器拒绝。客户端应该继续发送剩余的请求,如果请求已经完成,就忽略这个响应
- 允许客户端发送带请求体的请求前,判断服务器是否愿意接收请求(服务器通过请求头判断)
- 在某些情况下,如果服务器在不看请求体就拒绝请求时,客户端就发送请求体是不恰当的或低效的
-
200 OK:请求成功
-
302 Found:请求的资源被暂时的移动到了由Location头部指定的URL上
-
304 Not Modified:说明无需再次传输请求的内容,也就是说可以使用缓存的内容
-
400 Bad Request:由于语法无效,服务器无法理解该请求
-
401 Unauthorized:由于缺乏目标资源要求的身份验证凭证
-
403 Forbidden:服务器端有能力处理该请求,但是拒绝授权访问
-
404 Not Found:服务器端无法找到所请求的资源
-
405 Method Not Allowed:服务器禁止了使用当前HTTP方法的请求
-
406 Not Acceptable:服务器端无法提供与Accept-Charset以及Accept-Language指定的值相匹配的响应
-
408 Request Timeout:服务器想要将没有在使用的连接关闭
- 一些服务器会在空闲连接上发送此信息,即便是在客户端没有发送任何请求的情况下
-
500 Internal Server Error:所请求的服务器遇到意外的情况并阻止其执行请求
-
501 Not Implemented:请求的方法不被服务器支持,因此无法被处理
- 服务器必须支持的方法(即不会返回这个状态码的方法)只有 GET 和 HEAD
-
502 Bad Gateway:作为网关或代理角色的服务器,从上游服务器(如tomcat)中接收到的响应是无效的
-
503 Service Unavailable:服务器尚未处于可以接受请求的状态
- 通常造成这种情况的原因是由于服务器停机维护或者已超载
八、跨域
1、配置代码
- 创建一个新模块, 取名
02_CORS
, 与01_HelloWorld
创建方式相同
- 创建
JAVA
文件, 取名UserServlet
, 并编写如下代码
package com.bw.servlent;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/users")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("application/json; charset=UTF-8");
StringBuffer sb = new StringBuffer();
sb.append("[");
sb.append("{"name" : "zhangsan1", "age" : "11"},");
sb.append("{"name" : "zhangsan2", "age" : "12"},");
sb.append("{"name" : "zhangsan3", "age" : "13"},");
sb.append("{"name" : "zhangsan4", "age" : "14"}");
sb.append("]");
resp.getWriter().write(sb.toString());
}
}
- 服务器返回下面的数据, 这里为了方便, 直接通过简单拼接的方式, 组成目标数据
[
{
"name" : "zhagnsan1",
"age" : "11"
},
{
"name" : "zhagnsan2",
"age" : "12"
},
{
"name" : "zhagnsan3",
"age" : "13"
},
{
"name" : "zhagnsan4",
"age" : "14"
},
]
- 在
web
中创建js
文件夹, 并引入jqury.min.js
文件, 用于网络请求, 可以在 cdn.bootcss.com/jquery/3.2.… 中查看内容
- 删掉
index.jsp
, 并创建index.html
文件, 编写如下代码, 使用jqury
进行网络请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
table {border: 1px solid #000;}
</style>
<script src="js/jquery.min.js"></script>
</head>
<body>
<button id="load-btn">显示用户数据</button>
<table>
<tr>
<th>用户名</th>
<th>年龄</th>
</tr>
</table>
<script src="js/jquery.min.js"></script>
<script>
$('#load-btn').click(() => {
$.getJSON("http://localhost:8888/cors/users", (users)=>{
$table = $('table')
for (const user of users) {
const $tr = $('<tr>')
$tr.append($(`<td>${user.name}<td>`))
$tr.append($(`<td>${user.age}<td>`))
$table.append($tr)
}
})
})
</script>
</body>
</html>
- 点击
Debug
进行调试
- 在浏览器中输入
http://localhost:8888/cors/index.html
- 点击
显示用户数据
按钮, 可以看到添加了四条数据
- 上面的代码中, 前端代码和后端代码都放到了一个项目中, 并不适用于现在主流的
前后端分离
2、前后端分离
- 现在公司中前后端开发是分开的, 并且前后端各自都有对应的服务器
- 首先创建一个空项目, 取名
03_CORS_FrontEdge
, 然后将02_CORS
中的index.html
和web
文件夹下的js
文件夹移动到03_CORS_FrontEdge
中
- 重新点击
Debug
进行调试
- 在
index.html
文件中, 只要点击右边的浏览器图标,IntelliJ IDEA
会自动开启一个本地服务器加载界面
- 查看浏览器, 可以看到临时本地服务器使用的是
63342
端口
- 点击
显示用户数据
按钮, 可以在检查的Console
中看到错误信息, 并且没有用户数据显示
- 这个错误, 就是因为跨域引起的
3、同源策略
-
浏览器有个同源策略
- 它规定了: 默认情况下, AJAX请求只能发送给同源的URL
- 同源是指
3
个相同: 协议、域名、端口
-
下表给出了与URL
http://store.compay.com/dir/page/html
的源进行对比的示例,http
默认使用80
端口 |URL|结果|原因| |:--|:--|:--| |store.compay.com/dir/other.h… |store.compay.com/dir/inner/o… |store.compay.com/dir/secure.… |store.compay.com:81/dir/etc.htm… |news.compay.com/dir/other.h… -
img
、link
、iframe
、video
、audio
等标签不受同源策略的约束
4、跨域资源共享
- 解决AJAX跨域请求的常用方法
- CORS(Cross-Origin Resource Sharing), 跨域资源共享
- CORS的实现需要客户端和服务器同时支持
- 客户端
- 所有的浏览器都支持(IE至少是IE10版本)
- 服务器
- 需要返回响应的响应头(比如: Access-Control-Allow-Origin)
- 告知浏览器这是一个允许跨域访问的请求
- 客户端
- 在
UserServlet
中添加响应头字段Access-Control-Allow-Origin: http://localhost:63342
, 这样就可以在服务器http://localhost:63342
请求时, 允许跨域- 真实开发会做很多工作, 这里只做了简单的处理
- 部署服务器后, 再次点击
显示用户数据
, 就可以看到有4
条数据显示
- 此时模拟图如下
- 可以使用
Wireshark
抓取请求数据和相应数据 - 可以看到请求头中包含
Origin
字段, 这就是跨域的源地址, 响应头对应的Access-Control-Allow-Origin: http://localhost:63342
如果允许任何源地址都可以跨域, 可以在响应头中添加
Access-Control-Allow-Origin: *
, 不过为了安全, 不建议这么做
九、请求体的编码格式
- 在
HTML
中,form
表单在使用POST
方法时, 有一个enctype
属性, 可以设置请求体的编码格式 enctype
的默认值是application/x-www-form-urlencoded
1、application/x-www-form-urlencoded
- 在
01_HelloWorld
项目中, 修改file
文件夹下login.html
文件,form
标签的方法修改为post
, 添加请求体编码格式enctype="application/x-www-form-urlencoded"
- 部署后, 输入账号: 123, 密码: 456, 然后点击登录
- 使用
Wireshark
抓取登录数据, 如下图所示
application/x-www-form-urlencoded
的格式为: 用&
分隔参数, 用=
分隔键和值, 字符用URL编码方式进行编码- 不过
application/x-www-form-urlencoded
有个缺点, 就是不能上传文件 - 在
login.html
中添加文件选择标签
- 部署后, 浏览器内容如下所示
- 选择
image
文件夹下名为123.jpg
的图片
- 点击登录后, 使用
Wireshark
抓取数据如下所示, 可以看到请求头
中只有图片的名字, 并没有图片的内容
2、multipart/form-data
(1) 数据格式
- 将
form
中enctype
的值修改为multipart/form-data
, 并且先去掉文件选择
- 部署后, 浏览器内容如下
- 点击登录后, 可以看到服务器返回状态码
500
- 此时,
Wireshark
抓取到的数据, 如下所示
- 请求头中, 表示编码格式的字段为
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryHTu8aLxSINlCtpy1
- 请求体如下
------WebKitFormBoundaryHTu8aLxSINlCtpy1
Content-Disposition: form-data; name="username"
123
------WebKitFormBoundaryHTu8aLxSINlCtpy1
Content-Disposition: form-data; name="password"
456
------WebKitFormBoundaryHTu8aLxSINlCtpy1--
- 可以参考RFC 1521中
multipart/form-data
的格式
multipart-body := preamble 1*encapsulation
close-delimiter epilogue
encapsulation := delimiter body-part CRLF
delimiter := "--" boundary CRLF ; taken from Content-Type field.
; There must be no space
; between "--" and boundary.
close-delimiter := "--" boundary "--" CRLF ; Again, no space by "--",
preamble := discard-text ; to be ignored upon receipt.
epilogue := discard-text ; to be ignored upon receipt.
discard-text := *(*text CRLF)
body-part := <"message" as defined in RFC 822,
with all header fields optional, and with the
specified delimiter not occurring anywhere in
the message body, either on a line by itself
or as a substring anywhere. Note that the
semantics of a part differ from the semantics
of a message, as described in the text.>
- 绘制成图片, 如下所示, 其中标灰色的部分, 是可以忽略的
- 去掉忽略内容后
- 所以, 最终的格式如下
multipart-body: *("--" + boundary + CRLF + body-part + CRLF) + "--" + boundary + "--" + CRLF
- 在上面抓到的数据中
boundary
的值, 就是请求头字段boundary=----WebKitFormBoundaryHTu8aLxSINlCtpy1
的值
----WebKitFormBoundaryHTu8aLxSINlCtpy1
- 所以请求体第一行是
--
+boundary
+CRLF
, 其中的CRLF
是回车换行, 显示为换行
------WebKitFormBoundaryHTu8aLxSINlCtpy1
- 然后是请求体的字段
Content-Disposition: form-data; name="username"
123
- 接着是下一个
boundary
接下一个字段
------WebKitFormBoundaryHTu8aLxSINlCtpy1
Content-Disposition: form-data; name="password"
456
- 最后结尾格式是
"--" + boundary + "--" + CRLF
- 结尾内容: 前后各有两个
--
------WebKitFormBoundaryHTu8aLxSINlCtpy1--
multipart/form-data
格式的模拟图如下
(2) 上传文件
login.html
中添加上传文件标签
- 部署后, 输入内容, 选择图片
Wireshark
抓取数据, 可以看到上传的图片数据
- 请求体的最后部分, 依旧以
"--" + boundary + "--" + CRLF
格式结尾
十、会话跟踪
- HTTP是一种"无状态"(stateless)的协议
- 每次客户端访问网页时, 客户端都会打开与Web服务器的单独连接
- 并且服务器不会自动保留之前客户端请求的任何记录
- 所以服务器无法识别多个请求是否来自同一个客户端(比如浏览器)
- 在很多应用场景中, 都有以下需求
- 服务器能够识别出多个请求是否来自同一个客户端
- 在来自同一个客户端的多个请求之间共享数据
- 以上需求可以使用会话跟中技术来完成, 在Java中, 实现会话跟中的常用方案是
- Cookie
- Session
1、Cookie
- Cookie是直接存储在浏览器本地的一小串数据, 如果没有设置Cookie的过期时间, 则当浏览器关闭时, Cookie就失效了
(1) Cookie的作用域
- domain和path标识定义了Cookie的作用域, 即: Cookie应该发送给哪些URL
- domain:
- 标识制定了哪些主机可以接受Cookie
- 如果不指定, 默认为当前文档的主机(不包含子域名); 如果制定了domain, 则一般包含子域名
- 例如: 如果设置domain=sina.com, 则Cookie也包含在子域名中(如: mail.sima.com)
- path:
- 标识指定了主机下的那些路径可以接受Cookie, 子路径也会被匹配
- 例如: 设置path=/docs, 则以下地址都会匹配
- /docs
- /docs/one/
- /docs/one/img
(2) 服务器设置Cookie
- Cookie通常是由Web服务器使用响应头Set-Cookie设置的
resp.setHeader("Set-Cookie", "xxxxxxx")
2、Session
- 在Java服务器中, 可以使用
getSession()
函数检查或创建Session
getSession
检查客户端是否发送一个叫做JSESSIONID
的Cookie- 如果没有
- 创建一个新的
Session
对象, 并且这个Session
对象会有一个id - 这个
Session
对象会保留在服务器的内存中 - 在响应的时候, 会添加一个
Cookie
(JSESSIONID
=Session
对象的id
)给客户端
- 创建一个新的
- 如果有
- 返回
id为``JSESSIONID
的Session
对象
- 返回
- 如果没有
(1) JSESSIONID
- 默认情况下, 当用户关闭浏览器时, Cookie中存储的JSESSION就会被销毁
- 可以通过以下代码延长JSESSIONID在客户端的寿命
Cookie cookie = new Cookie("JSESSIONID", req.getSession().getId());
cookie.setMaxAge(3600);
resp.addCookie(cookie);
3、Cookie-Session: 应用模拟
- Cookie: 在客户端(浏览器)存储一些数据, 存放到本地磁盘(硬盘)
- 服务器可以返回Cookie交给客户端去存储
- Session: 在服务器存储一些数据, 存储到内存中
- 现在的网站, 在登录成功之后, 就不需要再次登陆, 仅仅直接打开网站, 就可以看到用户信息, 这正是由
Cookie
和Session
做到的
- 这是因为, 当用户登录后, 会在服务器端的内存中, 创建一个对应的
Session
对象
- 服务器在响应头中添加
Set-Cookie
字段, 让浏览器缓存到本地磁盘, 其中还包含两个字段domain
和path
domain
: 服务器的域名path
: 项目在服务器中的路径
- 当浏览器获取用户数据时, 会携带
Cookie
, 服务器判断Cookie
值, 就知道是哪一个Session
, 也就知道是哪个用户, 然后返回数据
- 当多个浏览器登录时, 服务器会根据不同的浏览器创建不同的
Session
, 返回不同的Cookie
值
4、代码实现Cookie
和Session
- 创建新的模块, 取名
04_Cookie_Session
- 配置到
Tomcat
, 取名/cs
- 在
web
中创建login.html
文件, 并编写代码, 以及删除index.jsp
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/cs/login">
<div>用户名: <input type="text", name="username"></div>
<div>密码: <input type="text", name="password"></div>
<button>登录</button>
</form>
</body>
</html>
- 导入
Tomcat
的两个库:jsp-api.jar
和servlet-api.jar
- 然后创建两个JAVA文件, 分别取名
LoginServlet
和UserServlet
- 在
LoginServlet
中编写代码- 通过
req.getSession();
创建一个Session
- 获取用户名和密码, 这里配置了两个用户, 分别是
- 用户名: 123, 密码: 456
- 用户名: abc, 密码: xyz
- 当符合用户名和密码时, 存入
Session
中, 并显示用户名
+空格
+LoginSuccess!
- 当不符合用户时, 销毁Session, 并显示
LoginFailure!
- 通过
package com.bw.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
// 响应体编码格式
resp.setContentType("application/json; charset=UTF-8");
// 创建Session, 自动生成ID: JSESSIONID
// 如果已经存在Session, 会根据浏览器返回的Cookie获取对应的Session
HttpSession session = req.getSession();
// 默认两个用户, 分别是: 123 和 abc
// 用户名: 123, 密码: 456
// 用户名: abc, 密码: xyz
if ((username.equals("123") && password.equals("456"))
|| (username.equals("abc") && password.equals("xyz"))
) {
// 存储用户数据
session.setAttribute("username", username);
session.setAttribute("password", password);
// 返回信息
resp.getWriter().write(username + " " + "Login Success!");
}else {
// 销毁session
session.invalidate();
// 返回信息
resp.getWriter().write("Login Failure!");
}
}
}
- 在
UserServlet
中编写代码- 取出浏览器Cookie对应的Session, 取出用户名和密码
- 如果用户名和密码是否符合下面两条数据之一
- 用户名: 123, 密码: 456
- 用户名: abc, 密码: xyz
- 符合: 显示
用户名 + 空格 + 密码
- 不符合: 返回状态码
302
, 并重定向到登录页/cs/login.html
package com.bw.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet("/user")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 响应体编码格式
resp.setContentType("application/json; charset=UTF-8");
// 创建Session, 自动生成ID: JSESSIONID
// 如果已经存在Session, 会根据浏览器返回的Cookie获取对应的Session
HttpSession session = req.getSession();
// 取出Session中缓存的用户名和密码, 取出的数据默认是对象类型, 强转为String类型
String username = (String) session.getAttribute("username");
String password = (String) session.getAttribute("password");
System.out.println("用户名:" + username);
System.out.println("密码:" + password);
if ((username.equals("123") && password.equals("456"))
|| (username.equals("abc") && password.equals("xyz"))
) {
// 返回信息
resp.getWriter().write(username + " " + password);
}else {
// 设置状态码: 302, 表示重定向
resp.setStatus(302);
// 跳转到登录页面
resp.setHeader("Location", "/cs/login.html");
}
}
}
- 点击
Debug
部署服务器, 然后在浏览器中输入http://localhost:8888/cs/login.html
- 然后使用
Wireshark
抓取数据, 查看HTTP Stream
, 可以看到有个默认的Cookie
, 这是IDEA的标识, 可以忽略
- 输入用户名:
123
, 密码:567
, 这是一个错误的密码
- 点击登录, 可以看到登录失败
- 查看
Wireshark
抓取的数据, 可以看到响应头中包含了Set-Cookie
字段, 这正是登录接口中req.getSession();
这句代码生成的Session
自动添加的
- 此时浏览器会缓存
Set-Cooike
的值, 只不过这个Session
因为登录失败, 直接销毁了JSESSIONID
:Session
的唯一标识, 可以在服务器找到对应的Session
Path
: 项目路径, 可以访问这个项目下的所有目录、文件以及接口HttpOnly
: 表示仅仅在HTTP请求中使用Domain
: 没有返回, 默认就是localhost
, 并且子域名无法传递这个Cookie
Set-Cookie: JSESSIONID=4EBE27F27A78D3D15FE2B71780B9D751; Path=/cs; HttpOnly
- 然后在浏览器中新创建一个标签页, 输入
http://localhost:8888/cs/user
, 可以看到重定向到了login.html
界面, 这因为服务器中与浏览器Cookie
对应的Session
, 没有存储用户名密码
- 此时,
Wireshark
抓取的数据如下
- 回到登录界面, 重新输入用户名:
123
, 密码456
, 点击登录, 可以看到登陆成功
Wireshark
抓取的数据, 可以看到Set-Cookie
的值
JSESSIONID=CBBFEFDA6314F08518825709048C8AD0; Path=/cs; HttpOnly
- 此时浏览器新建标签页, 输入
http://localhost:8888/cs/user
, 就可以看到Session
中存储的用户名和密码了
- 此时,
Wireshark
抓取的数据如下, 可以看到请求头中Cookie
的值包含CBBFEFDA6314F08518825709048C8AD0
, 正是登录成功后, 在服务器端生成的Session
自动添加响应头字段Set-Cookie
中的值
- 此时更换
Safiri
浏览器输入http://localhost:8888/cs/user
, 浏览器自动跳转到login.html
界面, 这是因为Safiri
浏览器的Cookie
值, 在服务器中没有对应的Session
- 服务器会根据不同的浏览器生成不同的
Session
, 并返回不同的数据