HTTP协议、Java前后端交互、Servlet

文章目录

  • 抓包工具 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协议、Java前后端交互、Servlet_第1张图片

HTTP 请求和响应结构

随便访问一个页面,点开详细信息,查看 Raw

  1. 首行,请求/响应的第一行

    img

    HTTP/1.1 200 OK
    版本 状态码 状态码描述
    
  2. 请求/响应报头 header

    header 里面都是键值对,每个键值对占一行,键和值以 : 分隔

  3. 空行:

    是请求/响应报头(header)的结束标记
    报头里有多少个键值对(有多少行)是不确定的,所以以空行作为结束标记

  4. 请求/响应正文:

    不是所有的 HTTP 请求都有正文

URL 唯一资源定位符

HTTP协议、Java前后端交互、Servlet_第2张图片

URL 里面如果没有写端口,此时浏览器就会给一个默认值:http:// 默认端口 80,https:// 默认端口 443

查询字符串:请求发给服务器的时候带的参数

  • ? 作为查询字符串的起始标志,后面的内容就是查询字符串的本体
  • 通过键值对的方式来组织,键值对之间使用 & 来分割,键和值之间使用 = 来分割

路径:指定了服务器上资源的具体位置

片段标识符:起到页面内部跳转的效果

HTTP 协议中的方法

方法 说明
GET 获取资源
POST 传输实体主体
PUT 传输文件
HEAD 获得报文首部
DELETE 删除文件
OPTIONS 询问支持的方法
TRACE 追踪路径
CONNECT 要求用隧道协议连接代理
LINK 建立和资源之间的联系
UNLINE 断开连接关系

触发 GET 请求的场景

  1. 浏览器地址栏输入一个 URL,回车
  2. HTML 标签,如 a,img,link,script
  3. form
  4. ajax

GET 的典型特点

  1. URL 的 query string 有时候有,有时候没有
  2. body 通常是空

POST 的特点

  1. URL 里通常没有 query string
  2. 通常有 body

GET 和 POST 的区别

  • 没有本质区别,它们之间可以相互替代
  • 在使用习惯上存在区别:
  • GET 主要用来获取数据,POST主要用来给服务器提交数据
  • GET 主要通过 query string 传递数据,POST 使用 body 传递数据
  • GET 请求一般建议实现成“幂等的”,POST 则不要求。幂等:多次输入的相同内容,得到的结果也相同,称为“幂等”
  • GET 一般是可以被缓存的,POST 不要求

请求报头(header)

Host:表示服务器主机的地址和端口

Content-Length:表示 body 中的数据长度

Content-Type:表示请求的 body 中的数据格式

常见的 HTTP 请求的 body 数据格式

  1. json
  2. urlencoded
  3. form-data

User-Agent(UA):描述使用设备

Referer:表示这个页面是从哪个页面跳转过来的

Cookie:是浏览器在本地存储数据的一种机制,每个网站分配的cookie独立,内部存储的是键值对

HTTP响应

状态码

以下是一些常见的 HTTP 响应状态码:

  1. 1xx(信息性状态码):
    • 100 Continue:服务器已经收到请求的头部,并且客户端应继续发送请求的其余部分。
  2. 2xx(成功状态码):
    • 200 OK:请求成功,服务器已经成功处理了请求。
    • 201 Created:请求已经被实现,而且有一个新的资源已经依据请求的需要而建立。
  3. 3xx(重定向状态码):
    • 301 Moved Permanently:被请求的资源已被永久移动到新位置。
    • 302 Found:请求的资源现在临时从不同的 URI 响应请求。
    • 304 Not Modified:如果客户端的缓存是最新的,则返回此状态码。
  4. 4xx(客户端错误状态码):
    • 400 Bad Request:服务器无法理解请求的语法。
    • 401 Unauthorized:请求要求身份验证。
    • 403 Forbidden:服务器理解请求,但拒绝执行。
    • 404 Not Found:服务器无法找到请求的资源。
    • 405 请求的方法不支持
  5. 5xx(服务器错误状态码):
    • 500 Internal Server Error:服务器遇到不可预知的情况。
    • 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到无效响应。
    • 503 Service Unavailable:服务器目前无法处理请求。
    • 504 超时

Content-Type

Content-Type 是 HTTP 响应报头之一,用于指示响应体的媒体类型(Media Type)或者内容类型。它告诉客户端如何解析响应体的数据。以下是一些常见的 Content-Type 值及其含义:

  1. text/plain: 纯文本,不包含任何格式的样式或标记。
  2. text/html: HTML 文档,用于网页内容。
  3. text/css: CSS 样式表,用于定义文档的样式和布局。
  4. text/javascript: JavaScript 脚本。
  5. application/json: JSON 格式,用于在客户端和服务器之间传输数据。
  6. application/xml: XML 格式,用于表示结构化的数据。
  7. image/jpeg、image/png、image/gif: 分别表示 JPEG、PNG、GIF 格式的图像。
  8. application/pdf: 表示 Adobe Portable Document Format(PDF)文件。
  9. application/octet-stream: 二进制流数据,通常用于传输不属于以上任何类型的二进制文件。
  10. 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">Submitbutton>
    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>
    
    <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('

Title: ' + data.title + '

Body: ' + data.body + '

'
); }, error: function(xhr, status, error) { // 请求失败时的处理 console.error('Error: ' + status + ', ' + error); } });
script> body> html>

使用 Postman

HTTPS

和 HTTP 的区别

  1. 安全性:
    • HTTP: HTTP 是一种不加密的协议,数据在传输过程中是以明文形式传送的。这意味着如果攻击者能够截获网络通信,他们可以轻松地读取或修改传输的数据。因此,HTTP 不适合传输敏感信息,如登录凭证、支付信息等。
    • HTTPS: HTTPS 使用了 TLS/SSL 协议进行加密,确保数据在传输过程中是加密的。这使得通过 HTTPS 传输的数据更难被窃取或篡改。HTTPS 是一种更安全的协议,适用于需要保护隐私和安全性的场景,如在线支付、登录等。
  2. 协议标识:
    • HTTP: 使用标准的 “http://” URL 标识,通常在浏览器中显示为 “http://”.
    • HTTPS: 使用标准的 “https://” URL 标识,通常在浏览器中显示为 “https://”. 还可能会显示一个锁图标,表示连接是安全的。
  3. 端口:
    • HTTP: 默认使用端口 80。
    • HTTPS: 默认使用端口 443。
  4. 证书:
    • HTTP: 不需要使用数字证书。
    • HTTPS: 需要使用由可信任的证书颁发机构(CA)颁发的数字证书。这个证书用于验证服务器的身份,确保连接是安全的。
  5. 性能:
    • HTTP: 由于不涉及加密解密过程,通常比 HTTPS 稍微快一些。
    • HTTPS: 由于数据加密和解密的额外开销,可能会稍微减缓数据传输速度。

对称密钥和非对称密钥

对称密钥和非对称密钥是两种常见的加密算法使用的密钥体制,它们分别用于加密和解密信息。

  1. 对称密钥(Symmetric Key):
    • 定义: 对称密钥是一种使用相同密钥进行加密和解密的加密算法。加密和解密都使用相同的密钥,这就是为什么它被称为“对称”的原因。
    • 特点: 算法简单、加解密速度快,但密钥的安全分发是一个挑战。因为发送方和接收方都必须事先共享相同的密钥,传输过程中,密钥也可能被黑客截获,存在安全风险。
    • 示例: 常见的对称加密算法包括 DES(Data Encryption Standard)、AES(Advanced Encryption Standard)等。
  2. 非对称密钥(Asymmetric Key):
    • 定义: 非对称密钥使用一对密钥,分别是公钥和私钥。公钥用于加密信息,而私钥用于解密信息。这使得公钥可以安全地发布,而私钥则必须保持机密。
    • 特点: 相对于对称密钥,非对称密钥更安全,但由于其复杂性,加解密速度较慢。非对称密钥通常用于安全通信的密钥交换阶段。
    • 示例: 常见的非对称加密算法包括 RSA(Rivest-Shamir-Adleman)、ECC(Elliptic Curve Cryptography)等。

数字证书

数字证书是一种用于验证网络通信中身份的安全工具,通常用于建立加密通信。数字证书是由称为证书颁发机构(Certificate Authority,简称CA)的可信任实体签发的一种电子文档,用于确认某个实体(通常是一个网络服务器)的身份。

数字证书包含了以下关键信息:

  1. 公钥: 数字证书包含一个公钥,用于加密和解密信息。公钥是一个用于加密数据的密码学密钥,同时也用于验证由该证书签发者签名的数据。
  2. 身份信息: 数字证书通常包含与公钥相关联的实体的身份信息,如域名(对于服务器证书)或个人信息(对于个人证书)。这个信息被称为主题(Subject)。
  3. 数字签名: 证书颁发机构使用其私钥对证书的内容进行数字签名,以确保证书的完整性和真实性。这个数字签名可以被其他人使用 CA 的公钥进行验证。

在网络通信中,数字证书的传输和验证通常涉及以下步骤:

  1. 握手阶段: 在建立安全连接(如HTTPS)时,通信的两端(客户端和服务器)会执行握手阶段。在这个阶段,双方协商加密算法、生成会话密钥等。
  2. 服务器发送证书: 在握手过程中,服务器将其数字证书发送给客户端。这个证书包含了服务器的公钥、服务器的身份信息(主题)、证书颁发机构的签名以及其他相关信息。
  3. 客户端验证证书: 客户端收到服务器的证书后,会验证证书的有效性。这个验证过程包括以下步骤:
    • 验证数字签名: 客户端使用证书颁发机构的公钥验证证书的数字签名,确保证书的完整性和真实性。
    • 验证有效期: 客户端检查证书的有效期,确保证书在当前时间内有效。
    • 验证域名: 对于服务器证书,客户端会检查证书中的域名信息与实际连接的域名是否匹配,以防止钓鱼攻击。
  4. 生成共享密钥: 如果证书验证通过,客户端使用服务器的公钥加密一个随机生成的会话密钥,并将其发送给服务器。
  5. 服务器解密会话密钥: 服务器使用其私钥解密客户端发送的会话密钥,从而两端都获得了相同的会话密钥。
  6. 加密通信: 之后的通信将使用共享的会话密钥进行加密和解密,保障通信的保密性和完整性。

Tomcat

Apache Tomcat 是一个轻量级的开源 Java 应用服务器,用于托管和运行 Java Web 应用。

官方网站:https://tomcat.apache.org/

下载:这里选择 Tomcat 8.5 的版本

HTTP协议、Java前后端交互、Servlet_第3张图片

解压好进入 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 项目的目录:

HTTP协议、Java前后端交互、Servlet_第4张图片

main 用于存放业务代码,test 用于存放测试代码

java 用于存放 java 代码,resources 存放项目中依赖的图片、音频、字体等资源文件

pom.xml 是 Maven 项目的核心文件,描述了这个项目的属性信息

引入依赖

去 Maven Repository 搜索 servlet,选择 3.1.0 版本

HTTP协议、Java前后端交互、Servlet_第5张图片

将方框内的代码复制粘贴到 pom.xml 里面,具体步骤是在 标签内创建 标签,将代码粘贴到 内。

复制进去,点击刷新按钮,就会自动开始下载了。

默认的下载位置是家目录下的 .m2 文件中

创建目录

为了我们的程序能被 Tomcat 识别,需要创建如下的目录结构

HTTP协议、Java前后端交互、Servlet_第6张图片

在 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 Applicationdisplay-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. 读取请求并解析
    2. 根据请求计算响应
    3. 将响应返回给客户端

    其中的1、3两步都是由 Tomcat 自动完成,第 2 步由我们自己重写的 doGet 方法完成

  • @WebServlet("/hello") 注解,用来约定 HTTP 请求的 url 是什么样的 path 才会调用到当前这个 Servlet 类

打包

点开右边的 m 图标,展开 Lifecycle,双击 package 或者右键 package 运行。

HTTP协议、Java前后端交互、Servlet_第7张图片

接下来它就会执行从 clean 到 package 的过程

在 target 目录下可以找到最终生成的 jar 包

HTTP协议、Java前后端交互、Servlet_第8张图片

然而 jar 包并不能被 Tomcat 识别,Tomcat 的可以识别的是 war 包,需要到 pom.xml 中修改配置,让它生成 war 包:

标签里添加 :

<packaging>warpackaging>
<build>
    <finalName>hello_servletfinalName>
build>

标签指定了生成 war 包, 中的 标签指定生成的包的名字

重新双击 package,打包操作完成。

部署和验证

把刚才得到的 war 包,拷贝到 Tomcat 的 webapps 目录中

然后启动 Tomcat,启动时会自动对我们拷贝的 war 包进行解压,生成文件夹。

打开浏览器,指定好路径,进行验证:

HTTP协议、Java前后端交互、Servlet_第9张图片

/hello_servlet/hello 路径:

  • 其中 /hello_servlet 是 webapps 里的目录,又叫上下文路径。
  • /hello 是 @WebServlet("/hello") 注解里的路径,又叫 Servlet 路径

更快地部署:Smart Tomcat 插件

Smart Tomcat 插件可以帮我们省略打包、部署和启动 Tomcat 的过程。

安装好 Smart Tomcat 插件后,进入 Edit Configurations…

HTTP协议、Java前后端交互、Servlet_第10张图片

点 + 号,选择 Smart Tomcat

HTTP协议、Java前后端交互、Servlet_第11张图片

配置 Tomcat 路径,其他保持默认

HTTP协议、Java前后端交互、Servlet_第12张图片

点击 OK 之后,右上角出现配置好的Smart Tomcat,点击绿色三角按钮即可自动启动 Tomcat 并完成部署。

使用 Smart Tomcat 插件,Context path 就是在这个页面里面设置的。

常见错误总结

  1. 404 大概率是 url 写错,也有可能是 webapp 没有加载正确(比如 web.xml 写错了)
  2. 405 可能收到 GET 请求,但是你没有实现 doGet。或者写了 doGet,但是没有把 super.doGet 这一行删掉
  3. 500 服务器内部错误,比如你的代码抛异常了
  4. 返回空白页面,可能是忘记填写 resp
  5. 无法访问此网站,可能是 Tomcat 没有正确启动

Servlet 详解

HttpServlet

方法 调用时机
init HttpServlet 实例化后被调用一次
destroy HttpServlet 实例不再使用的时候调用一次
service 收到 HTTP 请求时调用
doGet 收到 GET 请求时调用(由 service 方法调用)
doPost 收到 POST 请求时调用(由 service 方法调用)
doPut/doDelete/doOptions/... 收到其他请求时调用(由 service 方法调用)

HttpServlet 的生命周期

  1. 首次使用,先调用一次 init
  2. 每次收到请求,调用 service,在 service 内部通过方法来决定调用哪个 doXXX
  3. 销毁之前调用 destroy

HttpServletRequest

方法 说明
String getProtocol() 返回请求使用的协议的名称和版本
String getMethod() 返回请求的HTTP方法的名称
String getRequestURI() 返回此请求的URL的一部分
String getContextPath() 返回请求URI中指示请求上下文的部分
String getQueryString() 返回包含在请求URL的路径后的查询字符串
Enumeration getParameterNames() 返回包含参数名称的 String 对象的枚举
String getParameter(String name) 以字符串形式返回请求参数的值,如果该参数不存在,则返回null
String[] getParameterValues(String name) 返回一个字符串数组,包含给定请求参数的所有值,如果该参数不存在,则返回null
Enumeration 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("
"
); stringBuilder.append(req.getMethod()); stringBuilder.append("
"
); stringBuilder.append(req.getRequestURI()); stringBuilder.append("
"
); stringBuilder.append(req.getContextPath()); stringBuilder.append("
"
); stringBuilder.append(req.getQueryString()); stringBuilder.append("
"
); Enumeration<String> headerNames = req.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); stringBuilder.append(name).append(": ").append(req.getHeader(name)).append("
"
); } resp.setContentType("text/html"); resp.getWriter().write(stringBuilder.toString()); } }

结果:

HTTP协议、Java前后端交互、Servlet_第13张图片

回显 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);
    }
}

img

对 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 请求,获取返回结果

HTTP协议、Java前后端交互、Servlet_第14张图片

构造和解析 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,可以看到结果正确返回

HTTP协议、Java前后端交互、Servlet_第15张图片

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 返回给浏览器,就可以验证用户身份。

流程如图:

HTTP协议、Java前后端交互、Servlet_第16张图片

  • cookie 存在客户端,session 存在服务端,sessionId 在客户端和服务端都有

HttpServletRequest 类中的方法

方法 说明
HttpSession getSession() 返回与此请求关联的当前会话,如果请求没有会话,则创建一个会话
HttpSession getSession(boolean create) 返回与此请求关联的当前会话,如果没有当前会话并且 createtrue,则返回新会话,如果没有当前会话并且 createfalse,则返回 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 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>Documenttitle>
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 是边界,表示上传文件的起始和结束位置

你可能感兴趣的:(Java,http,java,交互)