网络协议基础学习(十): HTTP

260 阅读25分钟

一、HTTP

  • HTTP(Hyper Text Transfer Protocol), 译为超文本传输协议
    • 是互联网中应用最广泛的应用层协议之一
    • 设计HTTP最初的目的是: 提供一种发布和接收HTML页面的方法,由URI来标识具体的资源
    • 后面用HTTP来传递的数据格式不仅仅是HTML,应用非常广泛
  • HTML(Hyper Text Markup Language): 超文本标记语言, 用以编写网页
  • 维基百科

image.png

二、版本

  • 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, 可以译为: 请求意见稿)
    • HTTP/1.1最早是在1997年的RFC 2068中记录的
      • 该规范在1999年的RFC 2616中已作废
      • 2014年又由RFC 7230系列的RFC取代
    • HTTP/2标准于2015年5月以RFC 7540正式发表,取代HTTP/1.1成为HTTP的实现标准
  • 中国的RFC
    • 1996年3月,清华大学提交的适应不同国家和地区中文编码的汉字统一传输标准被IETF通过为RFC 1922
    • 成为中国大陆第一个被认可为RFC文件的提交协议

四、报文格式

1、运行项目

  • 打开终端, 启动安装的Tomcat

image.png

  • 在浏览器中输入127.0.0.1:8080, 看到下面的内容, 说明已经启动成功

image.png

image.png

  • 浏览器自动打开http://localhost:8888/hello/, 我这里将JAVA项目部署在了8888端口

image.png

  • 浏览器输入http://localhost:8888/hello/image/123.jpg, 加载image文件夹中的123.jpg图片

image.png

  • 浏览器输入http://localhost:8888/hello/file/login.html, 加载file文件夹中的login.html文件

image.png

  • 输入, 用户名: 123, 密码: 456, 点击登录, 展示Login Success!

image.png

2、抓取数据

  • 打开Wireshark, 选择环回地址抓取数据

image.png

  • 在浏览器中输入http://localhost:8888/hello/file/login.html

image.png

  • 输入用户名: 123, 密码: 456, 点击登录, 可以看到下面的数据

image.png

  • Wireshark中, 选择抓取到的请求数据, 可以看到请求头中, 每一行的最后都有\r\n字符, 即回车符换行符, 并且在请求体中可以看到username = 123password = 456

image.png

  • 查看真实的字节数据, 可以看到\r\n两个符号, 就是字节0x0d0x0a

image.png

  • Wireshark中选择抓取到的数据, 右键点击 -> 选择Follow -> 选择HTTP Stream, 查看抓取到的HTTP数据流

image.png

  • HTTP数据流, 如下图所示
    • 红色部分: 请求数据
    • 蓝色部分: 响应数据
    • 每一行最后的\r\n都显示成了换行
    • 请求头和请求体被\r\n分割
    • 响应头和响应体被\r\n分割

image.png

  • 请求报文
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

image.png

4、整体

  • 报文格式-整体, 如下图所示

image.png

5、ABNF-核心规则

  • 下面是ABNF中使用到的符号
规则形式定义意义
ALPHA%x41-5A / %x61-7A大写和小写ASCII字母 (A-Z, a-z)
DIGIT0x30-39数字 (0-9)
HEXDIGDIGIT / "A" / "B" / "C" / "D" / "E"/ "F"十六进制数字 (0-9, A-F, a-f)
DQUOTE%x22双引号
SP%x20空格
HTAB%x09横向制表符
WSPSP / HTAB空格或横向制表符
LWSP*(WSP / CRLF)直线空白 (晚于换行)
VCHAR%x21-7E可见(打印)字符
CHAR%x01-7F任何7-位US-ASCII字符, 不包括NUL(%0x00)
OCTET%x00-FF8位数据
CTL%x00-1F / %x7F控制字符
CR%x0D回车
LF%x0A换行
CRLFCR 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-versionHTTP的版本号
CRLF回车换行符号\r\n, 即: 0x0d、0x0a
DIGIT数字, 例如: 1
HTTP-nameHTTP的名称, 固定值: 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-versionHTTP版本号, 例如: HTTP/1.1
SP空格
DIGIT数字
status-code3个数字, 例如: 200、302、404
reason-phrase一个可能为空的描述状态码的文本短语, 例如: HTAB/SP/VCHAR/obs-text
CRLF回车空格符号\r\n, 即: 0x0d、0x0a
HTABtab键
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报文结构图

  • 请求报文格式模拟图, 如下所示

image.png

  • 响应报文格式模拟图, 如下所示

image.png

10、URL的编码

image.png

11、telent

  • 使用telent, 可以直接面向HTTP报文与服务器交互
    • 可以更清晰、直观地看到请求报文、响应报文的内容
    • 可以检验请求报文格式的正确与否
  • MAC电脑, 先安装Homebrew, 可以看这篇: 小码哥-音视频学习笔记(第三天): Mac安装Homebrew
  • 然后使用Homebrew安装telent
brew install telent
  • 使用telent连接本机的8888端口, 也就是上面服务器项目部署的端口

image.png

  • 使用GET方法连接hello项目, 可以看到返回的结果
    • 注意: hello左右都必须有/, 即/hello/, 这是格式要求
GET /hello/ HTTP/1.1
Host: localhost:8888

image.png

  • 在没有指定具体文件或接口的情况下, 会默认加载服务器中index.jsp文件

image.png

  • 加载file文件夹中login.html文件
GET /hello/file/login.html HTTP/1.1
Host: localhost:8888

image.png

  • 文件内容:

image.png

  • 使用GET方法请求login接口, 并传递参数
GET /hello/login?username=123&password=456 HTTP/1.1
Host: localhost:8888

image.png

  • login接口的具体实现

image.png

  • 查看8888端口支持的所有方法
OPTIONS * HTTP/1.1
Host: localhost:8888

image.png

  • 也可以查看服务器支持的方法
OPTIONS /hello/ HTTP/1.1
Host: localhost:8888

image.png

五、请求方法

方法描述
GET常用于读取的操作, 请求参数直接拼接在URL的后面(浏览器对URL是有长度限制的)
POST常用于添加、修改、删除的操作, 请求参数可以放到请求体中(没有大小限制)
HEAD请求得到与GET请求相同的响应, 但没有响应体
使用场景举例: 在下载一个大文件前, 先获取其大小, 再决定是否要下载。以此可以节约带宽资源请求方法
OPTIONS用于获取目的资源所支持的通信选项, 比如服务器支持的请求方法
OPTIONS * HTTP/1.1
PUT用于对已存在的资源进行整体覆盖
PATCH用于对资源进行部分修改(资源不存在,会创建新的资源)
DELETE用于删除指定的资源
TRACE请求服务器回显其收到的请求信息, 主要用于HTTP请求的测试或诊断
CONNECT可以开启一个客户端与所请求资源之间的双向沟通的通道, 它可以用来创建隧道(tunnel)
可以用来访问采用了 SSL (HTTPS) 协议的站点

六、头部字段

1、请求头字段

  • 常用请求头字段如下图所示:

image.png

2、响应头字段

  • 常用响应头字段如下图所示:

image.png

七、状态码

  • 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创建方式相同

image.png

  • 创建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());
    }
}

image.png

  • 服务器返回下面的数据, 这里为了方便, 直接通过简单拼接的方式, 组成目标数据
[
    {
        "name" : "zhagnsan1",
        "age" : "11"
    },
    {
        "name" : "zhagnsan2",
        "age" : "12"
    },
    {
        "name" : "zhagnsan3",
        "age" : "13"
    },
    {
        "name" : "zhagnsan4",
        "age" : "14"
    },
]

image.png

  • 删掉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>

image.png

  • 点击Debug进行调试

image.png

  • 在浏览器中输入http://localhost:8888/cors/index.html

image.png

  • 点击显示用户数据按钮, 可以看到添加了四条数据

image.png

  • 上面的代码中, 前端代码和后端代码都放到了一个项目中, 并不适用于现在主流的前后端分离

image.png

2、前后端分离

  • 现在公司中前后端开发是分开的, 并且前后端各自都有对应的服务器

image.png

  • 首先创建一个空项目, 取名03_CORS_FrontEdge, 然后将02_CORS中的index.htmlweb文件夹下的js文件夹移动到03_CORS_FrontEdge

image.png

  • 重新点击Debug进行调试

image.png

  • index.html文件中, 只要点击右边的浏览器图标, IntelliJ IDEA会自动开启一个本地服务器加载界面

image.png

  • 查看浏览器, 可以看到临时本地服务器使用的是63342端口

image.png

  • 点击显示用户数据按钮, 可以在检查的Console中看到错误信息, 并且没有用户数据显示

image.png

  • 这个错误, 就是因为跨域引起的

image.png

3、同源策略

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请求时, 允许跨域
    • 真实开发会做很多工作, 这里只做了简单的处理

image.png

  • 部署服务器后, 再次点击显示用户数据, 就可以看到有4条数据显示

image.png

  • 此时模拟图如下

image.png

  • 可以使用Wireshark抓取请求数据和相应数据
  • 可以看到请求头中包含Origin字段, 这就是跨域的源地址, 响应头对应的Access-Control-Allow-Origin: http://localhost:63342

image.png

如果允许任何源地址都可以跨域, 可以在响应头中添加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"

image.png

  • 部署后, 输入账号: 123, 密码: 456, 然后点击登录

image.png

  • 使用Wireshark抓取登录数据, 如下图所示

image.png

  • application/x-www-form-urlencoded的格式为: 用&分隔参数, 用=分隔键和值, 字符用URL编码方式进行编码
  • 不过application/x-www-form-urlencoded有个缺点, 就是不能上传文件
  • login.html中添加文件选择标签

image.png

  • 部署后, 浏览器内容如下所示

image.png

  • 选择image文件夹下名为123.jpg的图片

image.png

  • 点击登录后, 使用Wireshark抓取数据如下所示, 可以看到请求头中只有图片的名字, 并没有图片的内容

image.png

2、multipart/form-data

(1) 数据格式
  • formenctype的值修改为multipart/form-data, 并且先去掉文件选择

image.png

  • 部署后, 浏览器内容如下

image.png

  • 点击登录后, 可以看到服务器返回状态码500

image.png

  • 此时, Wireshark抓取到的数据, 如下所示

image.png

  • 请求头中, 表示编码格式的字段为
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 1521multipart/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.>
  • 绘制成图片, 如下所示, 其中标灰色的部分, 是可以忽略的

image.png

  • 去掉忽略内容后

image.png

  • 所以, 最终的格式如下
multipart-body: *("--" + boundary + CRLF + body-part + CRLF) + "--" + boundary + "--" + CRLF

image.png

  • 在上面抓到的数据中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格式的模拟图如下

image.png

(2) 上传文件
  • login.html中添加上传文件标签

image.png

  • 部署后, 输入内容, 选择图片

image.png

  • Wireshark抓取数据, 可以看到上传的图片数据

image.png

  • 请求体的最后部分, 依旧以"--" + boundary + "--" + CRLF格式结尾

image.png

十、会话跟踪

  • 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为``JSESSIONIDSession对象
(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: 在服务器存储一些数据, 存储到内存中
  • 现在的网站, 在登录成功之后, 就不需要再次登陆, 仅仅直接打开网站, 就可以看到用户信息, 这正是由CookieSession做到的

image.png

  • 这是因为, 当用户登录后, 会在服务器端的内存中, 创建一个对应的Session对象

image.png

  • 服务器在响应头中添加Set-Cookie字段, 让浏览器缓存到本地磁盘, 其中还包含两个字段domainpath
    • domain: 服务器的域名
    • path: 项目在服务器中的路径

image.png

  • 当浏览器获取用户数据时, 会携带Cookie, 服务器判断Cookie值, 就知道是哪一个Session, 也就知道是哪个用户, 然后返回数据

image.png

  • 当多个浏览器登录时, 服务器会根据不同的浏览器创建不同的Session, 返回不同的Cookie

image.png

4、代码实现CookieSession

  • 创建新的模块, 取名04_Cookie_Session

image.png

  • 配置到Tomcat, 取名/cs

image.png

  • web中创建login.html文件, 并编写代码, 以及删除index.jsp文件

image.png

<!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.jarservlet-api.jar

image.png

  • 然后创建两个JAVA文件, 分别取名LoginServletUserServlet
  • LoginServlet中编写代码
    • 通过req.getSession();创建一个Session
    • 获取用户名和密码, 这里配置了两个用户, 分别是
      • 用户名: 123, 密码: 456
      • 用户名: abc, 密码: xyz
    • 当符合用户名和密码时, 存入Session中, 并显示用户名 + 空格 + LoginSuccess!
    • 当不符合用户时, 销毁Session, 并显示LoginFailure!

image.png

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

image.png

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

image.png

  • 然后使用Wireshark抓取数据, 查看HTTP Stream, 可以看到有个默认的Cookie, 这是IDEA的标识, 可以忽略

image.png

  • 输入用户名: 123, 密码: 567, 这是一个错误的密码

image.png

  • 点击登录, 可以看到登录失败

image.png

  • 查看Wireshark抓取的数据, 可以看到响应头中包含了Set-Cookie字段, 这正是登录接口中req.getSession();这句代码生成的Session自动添加的

image.png

  • 此时浏览器会缓存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, 没有存储用户名密码

image.png

  • 此时, Wireshark抓取的数据如下

image.png

  • 回到登录界面, 重新输入用户名: 123, 密码456, 点击登录, 可以看到登陆成功

image.png

  • Wireshark抓取的数据, 可以看到Set-Cookie的值
JSESSIONID=CBBFEFDA6314F08518825709048C8AD0; Path=/cs; HttpOnly

image.png

  • 此时浏览器新建标签页, 输入http://localhost:8888/cs/user, 就可以看到Session中存储的用户名和密码了

image.png

  • 此时, Wireshark抓取的数据如下, 可以看到请求头中Cookie的值包含CBBFEFDA6314F08518825709048C8AD0, 正是登录成功后, 在服务器端生成的Session自动添加响应头字段Set-Cookie中的值

image.png

  • 此时更换Safiri浏览器输入http://localhost:8888/cs/user, 浏览器自动跳转到login.html界面, 这是因为Safiri浏览器的Cookie值, 在服务器中没有对应的Session
  • 服务器会根据不同的浏览器生成不同的Session, 并返回不同的数据

image.png