## HTTP 协议
* HTTP 即 Hyper Text Transfer Protocol,超文本传输协议,该协议定义了客户端和服务器端通信时发送**数据的格式**。
* HTTP是基于 TCP/IP 的高级协议,基于**请求/响应**模型即一次请求对应一次响应,而且是无状态的,也就是指每次请求之间相互独立,不能交互数据,HTTP 协议默认端口号为 80。
* HTTP 1.0 版本中,每一次请求响应都会建立新的连接,1.1 版本中使用复用连接机制
## HTTP 请求
HTTP 请求消息包括四部分:请求行、请求头、请求空行、请求体
### 请求行
* Request Method:请求方式,HTTP 协议有 7 种请求方式,常用的有 2 种即 `GET` 和 `POST`
* `GET` 方式特点是请求参数在**请求行**中,拼接在 url 后,参数会在浏览器地址栏显示,不太安全,而且请求的 url 长度有限制,所以无法传输大文件
* `POST` 方式特点是请求参数在**请求体**中,浏览器地址栏不会显示参数,相对安全,而且请求的 url 长度没有限制
* Request URL:请求 url,`/ServletDemo/demo1`
* 请求协议和版本:例如 `HTTP/1.1`
### 请求头
请求头中封装着客户端浏览器传递给服务器的一些信息,使用键值对存储,格式为 `请求头名称:请求头值`,常见的请求头如下
|请求头名称|作用|
|:---:|---|
|Host|请求目标(服务器端)的 IP 地址和端口号,和请求行中的URL可以组成完整的所请求资源的地址|
|Connection|是否持久连接,该值为 `keep-alive` 表示持久连接(复用)|
|User-Agent|告知服务器,访问使用的浏览器版本,可以用于解决兼容性问题|
|Referer|告知浏览器,此次请求是从哪个网页 URL 获得点击当前请求中的网址,可以用于防盗链和统计工作|
|Accept|告知服务器,浏览器接受什么样的介质,例如 `text/html,application/xhtml+xml,application/xml`|
|Accept-Language|告知服务器,浏览器接受什么样的语言,例如 `zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2`|
### 请求空行
就是一个空行,用于分隔请求头和请求体
### 请求体
`POST` 请求才会有的部分,用于封装 `POST` 请求消息的请求参数
## ServletRequest
```java
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}
```
可以看到,要实现的 `servic()` 方法有两个参数,即 `ServletRequest` 对象和 `ServletResponse` 对象,`ServletRequest` 对象用于获取请求消息,`ServletResponse` 对象用于设置相应消息,这两个类的对象实例都由服务器进行创建,程序员使用即可。
### ServletRequest 的继承体系

实际开发中,我们自己编写的 Servlet 类一般继承自 `HttpServlet` 类,此时需要实现 `doPost()` 和 `doGet()` 方法,此时的参数为 `HttpServletRequest` 接口的对象,利用多态,真正执行的 `HttpServletRequest` 对象的方法实际上是 `org.apache.catalina.connector.RequestFacade` 类的方法。
### ServletRequest 获取请求行信息
1. 获取请求方式(`GET`、`POST`)
```java
String getMethod()
```
2. 获取虚拟目录
```java
String getContextPath()
```
3. 获取 Servlet 路径
```java
String getServletPath()
```
4. 获取请求参数(`GET` 方式才有值,`POST` 方式返回 `null`)
```java
String getQueryString()
```
5. 获取请求 URI,可以用于权限控制
URI:统一资源标识符,例如 `/ServletRequest/demo1`
URL:统一资源定位符,例如 `http://localhost/ServletRequest/demo1`
```java
String getRequestURI()
StringBuffer getRequestURL()
```
6. 获取协议及版本
```java
String getProtocol()
```
7. 获取客户机的 IP 地址
```java
String getRemoteAddr()
```
**测试**
```java
@WebServlet(urlPatterns = {"/demo1"})
public class RequestDemo1 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(request.getMethod());
System.out.println(request.getContextPath());
System.out.println(request.getServletPath());
System.out.println(request.getQueryString());
System.out.println(request.getRequestURI());
System.out.println(request.getRequestURL());
System.out.println(request.getProtocol());
System.out.println(request.getRemoteAddr());
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
```
以 `ServletRequest` 为虚拟目录启动项目,在浏览器地址栏输入 `http://localhost:8080/ServletRequest/demo1?username=cool&password=yzt`,直接访问该 Servlet 资源,控制台打印如下内容
```
GET
/ServletRequest
/demo1
username=cool&password=yzt
/ServletRequest/demo1
http://localhost:8080/ServletRequest/demo1
HTTP/1.1
0:0:0:0:0:0:0:1
```
### ServletRequest 获取请求行信息
1. 通过请求头的名称获取请求头的值
```java
String getHeader(String name)
```
2. 获取所有的请求头**名称**
```java
Enumeration getHeaderNames()
```
虽然 `Enumeration` 翻译为枚举,但是其作用类似集合框架中的 `Iterator` 迭代器,它有两个方法
```java
boolean hasMoreElements(); // 是否还有下一元素
E nextElement(); // 获取下一元素,并将指针下移
```
**测试**
获取所有的请求头名称
```java
Enumeration enumeration = request.getHeaderNames();
while(enumeration.hasMoreElements()) {
System.out.println(enumeration.nextElement());
}
```
通过请求头的名称获取一些重要的请求头的值
```java
System.out.println(request.getHeader("referer")); // 获取客户机从哪来,如果是直接输入url跳转,则值为 null
System.out.println(request.getHeader("user-agent")); // 获取客户机的浏览器版本
System.out.println(request.getHeader("host")); // 获取请求的资源的ip和域名
```
控制台输出
```
null
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
localhost:8080
```
### ServletRequest 获取请求体信息
只有 `POST` 请求方式才有请求体,请求体中封装了 `POST` 请求的请求参数,请求参数可以很大,比如上传文件。获取请求体信息,需要先从 `ServletRequest` 对象获取流对象,流对象可以是字符输入流(操作字符数据)或者字节输入流(针对所有类型的数据),然后再从流对象中获取数据,API 如下
```java
BufferedReader getReader(); //获取字符输入流
ServletInputStream getInputStream(); //获取字节输入流
```
**测试**
为了完成获取请求体的测试,先编写一个简单的表单页面,将表单的提交地址设置为目标 Servlet,注意表单的 `action` 值必须带着项目的虚拟目录,如果项目的虚拟目录是 `/`,则可以直接填写 Servlet 的路径
```html
form
```
编写相应的 Servlet
```java
package cool.yzt.web;
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.BufferedReader;
import java.io.IOException;
/**
* @Author: yzt
* @Description: 获取 POST 请求的请求体
*/
@WebServlet(urlPatterns = {"/demo2"})
public class RequestDemo2 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
BufferedReader br = request.getReader();
String str = null;
// BufferedReader 有 readLine() 方法,可以一次读取一行
while((str=br.readLine())!=null) {
System.out.println(str); // 打印读取到的 POST 参数
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
```
启动项目,在表单页面,填入用户名和密码为 `yzt.cool`,`123`,提交表单,首先,浏览器页面跳转至 `http://localhost:8080/ServletRequest/demo2`,其次,控制台打印如下内容
```
username=yzt.cool&password=123&button=%E6%8F%90%E4%BA%A4
```
### 获取请求参数通用方式
可以看到,用以上方法获取参数,较为麻烦,需要分为 `GET` 请求和 `POST` 请求两种情况。`ServletRequest` 提供了一种通用的获取参数的方式,无论是哪种请求方式,都可以通过以下方法获取请求参数。
1. 根据参数名称获取参数值
```java
String getParameter(String name)
```
2. 根据参数名称获取参数值的数组,例如表单的复选框,一个参数名称可能包含数个参数值
```java
String[] getParameterValues(String name)
```
3. 获取所有请求的参数名称
```java
Enumeration getParameterNames()
```
4. 取所有参数的 Map 集合,参数名与参数值作为键和值
```java
Map getParameterMap()
```
**注意** 中文乱码问题
* `GET` 方式:Tomcat8 已经将 `GET` 方式乱码问题解决了
* `POST` 方式:会出现乱码,需要在在获取参数前手动设置 `ServletRequest` 的编码,例如
```java
request.setCharacterEncoding("utf-8"); // 设置 request 的编码为 utrf-8
```
**测试**
依旧是上面的表单案例,这次使用通用方式获取 `POST` 参数
```java
package cool.yzt.web;
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.BufferedReader;
import java.io.IOException;
import java.util.Map;
/**
* @Author: yzt
* @Description: 获取 POST 请求的请求体
*/
@WebServlet(urlPatterns = {"/demo2"})
public class RequestDemo2 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 设置编码方式,防止中文参数值乱码
request.setCharacterEncoding("utf-8");
// 参数中的字符串对应页面表单中表单项的 name 属性值
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println(username);
System.out.println(password);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
```
### 请求转发与数据共享
**请求转发**
请求转发是一种在服务器内部的资源跳转方式,当发生请求转发时,客户端浏览器的地址栏中路径不会发生变化,但是请求只能转发至服务器的内部资源,而且转发的请求是同一次请求
1. 通过 `ServletRequest` 对象获取请求转发器对象 `RequestDispatcher`
```java
getRequestDispatcher(String path) // path 为转发目标资源的路径
```
2. 使用 `RequestDispatcher` 对象完成转发
```java
forward(ServletRequest request, ServletResponse response)
```
**域对象**
域对象是指一个有作用范围的对象,在该范围内可以使用域对象共享数据
**Request 域**
Request 域是指一次请求的范围,可以用于请求转发中的多个资源进行共享数据,主要 API 如下
```java
void setAttribute(String name,Object obj) // 以键值对的形式存储数据,name 为键,obj 为在 Request 域中共享的对象
Object getAttitude(String name) // 通过键获取值(共享的对象)
void removeAttribute(String name) // 通过键移除键值对
```

**测试**
编写两个 Servlet,省去了 `package` 和 `import` 语句
```java
@WebServlet(urlPatterns = {"/demo3"})
public class RequestDemo3 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 处理部分请求
System.out.println("demo3 被访问了");
// 存储数据到 Request 域中,用于共享
String msg = "demo3";
request.setAttribute("msg",msg);
// 请求转发到 /demo4 继续处理
request.getRequestDispatcher("/demo4").forward(request,response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
@WebServlet(urlPatterns = {"/demo4"})
public class RequestDemo4 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 处理请求
System.out.println("demo4 被访问了");
// 获取 Request 域中的共享对象数据
System.out.println("收到来自 " + request.getAttribute("msg") +" 的信息");
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}
```
启动项目,访问 `http://localhost:8080/ServletRequest/demo3`,控制台打印以下消息,但是浏览器地址栏并没有发生变化
```
demo3 被访问了
demo4 被访问了
收到来自 demo3 的信息
```
## 参考
* [黑马 JavaWeb](https://www.bilibili.com/video/BV1J4411877m)

Http 请求和 ServletRequest 对象