---
title: 'Http Header - Request、Response'
disqus: kyleAlien
---
Http Header - Request、Response
===
## Overview of Content
如有引用參考請詳註出處,感謝 :cat:
[TOC]
## Web 容器 - 判斷請求類型
Servlet 是如何處理使用者的 API 請求的呢?其實 **並不是 Servlet 先接觸到請求,而是 Web 容器會先接觸到請求**,**由容器判斷請求類型**(Get、Post... 等等)
### Web 容器 UML
* 首先 HttpServlet 進入
1. HttpServlet 其實只是 **`Servlet` 的一個實做**,其實它表達出了它不只可以處理 Http 資料,也 **期望 `Servlet` 界面可以拓展出其他實做**
> 這裡只列出部份常見方法
```mermaid
classDiagram
Servlet <-- GenericServlet
Servlet: +init(ServletConfig)
Servlet: +service(ServletRequest, ServletResponse)
Servlet: +getServletConfig() ServletConfig
Servlet: +getServletInfo() String
Servlet: +destroy()
ServletConfig <-- GenericServlet
ServletConfig: +getServletName() String
ServletConfig: +getServletContext() ServletContext
ServletConfig: +getInitParameter() String
GenericServlet <|-- HttpServlet
HttpServlet: +doGet(HttpServletRequest, HttpServletResponse)
HttpServlet: +doPost(HttpServletRequest, HttpServletResponse)
HttpServlet: +doPut(HttpServletRequest, HttpServletResponse)
```
2. 在來看 Servlet#service 方法所關聯的兩個界面 `ServletRequest`、`ServletResponse`,**這兩個界面有基礎處理 Meta Data,跟與資料交流的方法**
> 這裡只列出部份常見方法
* 使用者請求的相關訊息
| ServletRequest 方法 | 功能 | 補充 |
| - | - | - |
| `getCharacterEncoding()` | 使用者這次請求的字串格式 | 像是 `UTF8`/`16`、`Big5`.. |
| `getContentLength()` | 以 Byte 為單位,返回這次資料的長度 | `-1` 代表未知長度 |
| `getContentType()` | 表示 Body 的 MIME 類型 | 像是 `text/html`、`charset=UTF-8`、`null` |
* 服務器端回應使用者
| ServletResponse 方法 | 功能 | 補充 |
| - | - | - |
| `getCharacterEncoding()` | 這次回應的字串格式 | 像是 `UTF8`/`16`、`Big5`.. |
| `getContentType()` | 表示 Body 的 MIME 類型 | 像是 `text/html`、`charset=UTF-8`、`null` |
| `getWriter()` | 返回一個可以傳送字符串給使用者的物件 | 字符類型可透過 `getCharacterEncoding()` 確認 |
> MIME 會在下面小節再回顧並說明
```mermaid
classDiagram
class Servlet {
+init(ServletConfig)
+service(ServletRequest, ServletResponse)
+getServletConfig() ServletConfig
+getServletInfo() String
+destroy()
}
class ServletResponse {
+getCharacterEncoding() String
+getContentType() String
+getWriter() PrintWriter
}
class ServletRequest {
+getCharacterEncoding() String
+getContentLength() int
+getContentType() String
}
ServletResponse <-- Servlet
ServletRequest <-- Servlet
```
3. 接著看繼承上述界面的 **`HttpServletRequest`、`HttpServletResponse` 界面,這兩個界面有與 Http 協議更有相關的方法**(較細緻的抽象)
> 這裡只列出部份常見方法
* 使用者請求的相關訊息
| HttpServletRequest 方法 | 功能 | 補充 |
| - | - | - |
| `getAuthType()` | 傳回用於保護 servlet 的身份驗證方案的名稱 | 像是 `BASIC_AUTH`, `FORM_AUTH`, `CLIENT_CERT_AUTH`, `DIGEST_AUTH` |
| `getCookies()` | 取得使用者端的所有 Cookie | - |
| `getDateHeader(String)` | 傳回指定請求 Header 的值作為表示 Date 物件的長整數值 | 將此方法用於包含日期的標頭,例如 `If-Modified-Since` |
* 服務器端回應使用者
| HttpServletResponse 方法 | 功能 | 補充 |
| - | - | - |
| `addCookie(Cookie)` | 將服務器端的 Cookie 資訊傳給使用者 | 可存多個 Cookie |
| `containsHeader(String)` | 判斷要返回的資料中是否已經有指定 Header | - |
| `encodeURL(String)` | 對指定 URL 編碼 | 包含 Session |
```mermaid
classDiagram
class ServletResponse {
+getCharacterEncoding() String
+getContentType() String
+getWriter() PrintWriter
}
class HttpServletResponse {
+addCookie(Cookie)
+containsHeader(String) boolean
+encodeURL(String) String
}
ServletResponse <|-- HttpServletResponse
class ServletRequest {
+getCharacterEncoding() String
+getContentLength() int
+getContentType() String
}
class HttpServletRequest {
+getAuthType() String
+getCookies() Cookie[]
+getDateHeader(String) long
}
ServletRequest <|-- HttpServletRequest
```
### Web 容器判斷請求 - doXXX
* 我們在繼承 `HttpServlet` 類後,會覆寫像是 `doGet()`、`doPost()`、`doPut()`... 等等不同的函數,它們分別對應了 Http 協議的方法
它的判斷方式其實是 ^1.^ 使用 HttpServletRequest# getMethod 方法,^2.^ 判斷請求的類型,並 ^3.^ 呼叫對應的 protected 方法(就是上面寫的 doXXX)
```java=
public abstract class HttpServlet extends GenericServlet {
// 處理 Http 請求
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
// 判斷請求、回應是否是 Http
if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) {
// 不是則拋出
// 這裡的拋出通常是協助 Web 容器開發者,一般使用者是不會接收到這種異常的
throw new ServletException("non-HTTP request or response");
}
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
// 繼續往下分析
service(request, response);
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 取得已經分析好的 MetaData(上層已經封裝為 HttpServletRequest)
String method = req.getMethod();
// 判斷 Http 是哪個方法,並呼叫對應的
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// -1 代表不支援 `if-modified-since 標頭
doGet(req, resp);
} else {
// Get 會判斷時間是否超時
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// 時間未超過
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
// 超過時間則 返回錯誤
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req, resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req, resp);
} else {
// ... 省略部份
}
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//... 省略部份
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//... 省略部份
}
}
```
## HttpServletRequest 請求者資訊
當 HTTP 轉發 MetaData 給 Web 容器後,Web 容器會收集相關資訊,並產生 `HttpServletRequest` 物件給開發者使用(傳遞到指定的 Servlet),**透過它就可以獲取使用者的求請資訊**
### 伺服器處理 - 請求 Header
* HTTP 協議的請求、響應資料中 **「必定」包含 HTTP 標頭**;它可以提供客戶端、服務器端所需的重要訊息(不管是請求、響應都有 Header),並且 **可以有多個 Header**
```shell=
# 多個 Header 是使用逗號分開 `,`
Accept-Charset: ISO-8859-1, UTF-8:
```
> 這邊要說的是客戶端請求服務器端的「請求 Header」(並由伺服器端來處理)
```mermaid
graph LR;
subgraph HTTP請求_Header
請求行
請求_Header
通用_Header
實體_Header
其他
end
```
* **請求 Header 字段**(`Request Header Fields`)
從客戶端向服務器端發送請求的 Header;也就是 **流「入」伺服器的資料,由請求者設定**(站在伺服器的角度)
| Header Data | 概述 |
| - | - |
| `Accept` | 用戶代理可處理的 **媒體類型** |
| `Accept-Charset` | 偏好(優先)的字符集 |
| `Accept-Encoding` | 偏好(優先)的內容編碼 |
| `Accept-Language` | 偏好(優先)的語言 |
| `Authoriztion` | Web 應用的驗證訊息 |
| `Expect` | 期待服務器的特定行為 |
| `From` | 用戶來源 |
| `Host` | 標示服務器的相關訊息 |
| `If-Match` | 比較實體標記 |
| `If-Modfified-Since` | 比較資源的更新時間 |
| `If-None-Match` | 比較實體的記(與 `If-Match` 相反) |
| `If-Range` | 資源未更新時發送實體 Byte 的範圍請由 |
| `If-Unmodified-Since` | |
| `Max-Forwards` | 最大傳輸 |
| `Proxy-Authoriztion` | 給代理服務器的驗證 |
| `Range` | 實體的字節請求範圍 |
| `Referer` | 請求中的 URI 的原始獲取方 |
| `TE` | 傳輸編碼的優先級別 |
| `User-Agent` | HTTP 客戶端的程序訊息(如果是用遊覽器,那就是遊覽器訊息) |
* 可以透過 JavaEE Servlet 提供的 `HttpServletRequest` 界面方法獲取請求(使用)者的 Header 參數,範例如下
> 這裡就不特別解說 API 了
```java=
@WebServlet(
urlPatterns = { "/show.header" }
)
public class ShowHeaderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
String headerName = headers.nextElement();
out.println("<br> name: " + headerName +
", value: " + req.getHeader(headerName) +
"</br>");
}
out.println("</body>");
out.close();
}
}
```
> 
### 伺服器處理 - Parameter 參數
* 可以透過 `HttpServletRequest` 的幾個方法獲取 `Get` 參數
1. **參數可以重複,並帶不同的 Value**
```shell=
## Http 請求並帶有參數
https://hello.world/view?param=10¶m=30¶m=60
## 其中參數為 param,並有多個不同數值
param=10¶m=30¶m=60
```
如果想取出 URL 中所有的「**param**」數值,那可以使用 `getParameterValues(...)` 取出指定參數的數值
```shell=
String[] values = request.getParameterValues("param");
```
2. **也可以取出所有參數名**
```shell=
## Http 請求並帶有參數
https://hello.world/view?apple=Red&banana=Yellow&car=Green
## 其中參數為 apple, banana, car
apple=Red&banana=Yellow&car=Green
```
如果想取出 URL 中所有的參數名稱,那可以使用 `getParameterNames()` 取出所有參數的名稱
```shell=
Enumeration[] names = request.getParameterNames();
```
* **使用 JavaEE Servlet 取得 URI 的參數,範例如下**
```java=
@WebServlet(
urlPatterns = { "/show.params" }
)
public class GetParamsServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
Enumeration<String> parameterNames = req.getParameterNames();
while (parameterNames.hasMoreElements()) {
String parameterName = parameterNames.nextElement();
for (String value : req.getParameterValues(parameterName)) {
out.println("<br> name: " + parameterName + ", value: " + value + "</br>");
}
}
out.println("</body>");
out.close();
}
}
```
> 訪問網址:`http://localhost:8080/JavaEEHelloWorld-1.0-SNAPSHOT/show.params?param=10¶m=30¶m=60&apple=Red&banana=Yellow&car=Green`
>
> 
### 伺服器處理 - POST/GET 編碼問題
* Web 容器域會使用 `ISO-8859-1`(`Latin-1`),它是一個「**西歐語言**」的編碼,並且與 ASCII 相容;
如果使用者傳遞參數時使用 **非西歐語言** 會導致 Web 容器用錯誤的方式分析 Http 的 MetaData,導致 Servlet 處理資料時,會接收錯誤的資料
> 
* **Servlet 對於 Http 不同的請求方式會有不同的編碼處理方案**
* **POST 請求**:
**Server 端在取得客戶端 POST 的數值「之前」**,可使用 `setCharacterEncodeing(...)` 來設定對於數據的解碼方式
```java=
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
```
:::info
* 源碼註解上有清楚寫,這個方法(`setCharacterEncoding`)設定取得「`Body`」的解碼方式
> 也就該方法 **只對 POST 數值有效**!
:::
> 
* **GET 請求**:
當請求使用 GET 發送時,使用 `setCharacterEncoding` 則不一定有效;建議直接「手動處理編碼」,**^1.^ 將字串轉為 Byte array,^2.^ 再指定編碼方案**
```java=
String name = req.getParameter("namePost");
// ISO_8859_1 轉為 Big5
name = new String(name.getBytes(StandardCharsets.ISO_8859_1), "Big5");
```
:::warning
* 轉換後,在開發者工具上仍無法出現正確的文字?
因為我們還沒告訴遊覽器要如何解讀這段文字,這個部分可以透過 HTML 設定,也可以透過 Http 的 Header 設置
```html=
<meta http-equiv="Content-Type" content="text/html; charset=Big5">
```
:::
* **Get、Post 範例**
1. **間單 HTML 文檔**:並寫兩個 form 表單
* 設定 Header 的 content 為 `text/html`、`charset=Big5`
* 設定 form method 為 `get`、`post`
* 設定 form action 為 `encoding`(使用同一個 URI 處理不同請求方法),對應到的是 Servlet URL 的處理
```xml=
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content = "text/html; charset=BIG5">
<title>Title</title>
</head>
<body>
<form method="get" action="encoding">
<br>Get input:
<label>
<input type="text" name="nameGet"/>
</label>
<button>GET Request</button>
</form>
<form method="post" action="encoding">
<br>Put input:
<label>
<input type="text" name="namePost"/>
</label>
<button>POST Request</button>
</form>
</body>
</html>
```
2. **處理對應的 Servlet**
設定 form 表單的 action 的 `encoding` Servlet 處理
```java=
@WebServlet(
"/encoding"
)
public class EncodingServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
req.setCharacterEncoding("Big5");
String name = req.getParameter("namePost");
System.out.println("POST: " + name);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
String name = req.getParameter("nameGet");
name = new String(name.getBytes(StandardCharsets.ISO_8859_1), "Big5");
System.out.println("GET: " + name);
}
}
```
### 伺服器處理 - 讀取 POST 內容
* 我們這邊直接使用 JavaEE 的 Servlet 讀取 POST 的內容(就是 Body)
1. **間單 HTML 文檔**:並寫一個 form 表單
* 設定 Header 的 content 為 `text/html`、**`charset=UTF-8`**
* 設定 form method 為 `post`
* 設定 form action 為 `body.view`,對應到的是 Servlet URL 的處理
```xml=
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content = "text/html; charset=UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="body.view">
<br>Name:
<label>
<input type="text" name="userName"/>
</label>
<br>Password:
<label>
<input type="password" name="userPasswd"/>
</label>
<button>Submit</button>
</form>
</body>
</html>
```
2. **處理對應的 Servlet**
設定 form 表單的 action 的 `body.view` Servlet 處理
```java=
@WebServlet(
"/body.view"
)
public class BodyServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 取得 Post 資訊
BufferedReader reader = req.getReader();
String input;
StringBuilder requestBody = new StringBuilder();
while ((input = reader.readLine()) != null) {
requestBody.append(input).append("<br>");
}
String body = requestBody.toString();
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
out.println(body);
out.println("</body>");
}
}
```
* 輸入 ASCII 解碼的內容
> 
* 輸入 ASCII 不能正常解碼的內容(輸入中文)
> 
### 伺服器處理 - 使用 Http Inputstream 處理多媒體 MetaData
:::warning
以下範例不一定可以在你的專案中運作,因為這是直接對 Http 協議來傳送、分析 MetaData(或是說`Raw Data` 分析)
`Raw Data` 的分析往往需要雙方都了解細節協定才可以運作,所以以下我們先定義一個多媒體傳送的資料格式,接下來出現的 IMGIC Number 都是為了解析這個格式(format)
```shell=
--*****
Content-Disposition: form-data; Hello.pic filename="檔案名"
多媒體數據
--*****--
```
:::
* 以下範例是使用 JavaEE 的 Servlet 處理 Http 請求(要處理的是 Body 的二進制資料),處理的主要步驟如下
1. **讀取 Http 請求的 Body**
```java=
private byte[] readBody(HttpServletRequest req) throws IOException {
// 使用 io 流,先取得資料的輸入流
// 再使用 io 輸出流收集 byte[]
try (ServletInputStream inputStream = req.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
return baos.toByteArray();
}
}
```
2. 透過傳送 Http 結構的特性,取 Http Body 的 `filename` 取得傳送資料的名稱
> 以下的 `metaData` 是指 Http Body 中的資料
```java=
private String getFileName(String metaData) {
// 利用 indexOf 找到 filename 的位置
int fileNameIndex = metaData.indexOf("filename=\"");
if (fileNameIndex != -1) {
// 截取 filename 開始到下一個雙引號的部分
int startQuote = fileNameIndex + "filename=\"".length();
int endQuote = metaData.indexOf("\"", startQuote);
if (endQuote != -1) {
// 提取文件名
return metaData.substring(startQuote, endQuote);
}
}
// 如果未找到文件名,返回空字符串或者 null,視情況而定
return "";
}
```
3. **取得傳輸書的二進位資料**:
重點就是擷取 `多媒體數據`(當要要除去 `Content-Disposition` 這行,)
```shell=
--*****
Content-Disposition: form-data; Hello.pic filename="檔案名"
多媒體數據
--*****--
```
:::info
* `boundary` 是只用來區分 Http Header 跟真正資料的間隔
在HTTP協議中,使用 `--*****\r\n` 的形式通常是模擬 `multipart/form-data` 格式的一部分,這種格式常用於上傳文件或二進制數據
> 這樣的形式通常就稱為 `boundary`,它在 `multipart/form-data` 中用於分隔不同的數據部分
**boundary 的選擇是開發者定義的**,**只要它是唯一的並且不會與實際數據中的內容衝突**
> 所以 `--*****\r\n` 只是一個示例,**你可以選擇任何你認為合適的字串作為boundary**
:::
```java=
private Position getFilePosition(HttpServletRequest req, String bodyMetaData) {
String contentType = req.getContentType();
String boundaryText = contentType.substring(contentType.lastIndexOf("=") + 1);
int startBoundaryPos = bodyMetaData.indexOf("filename=\"");
startBoundaryPos = bodyMetaData.indexOf("\r\n", startBoundaryPos) + 2;
int endBoundaryPos = bodyMetaData.indexOf(boundaryText, startBoundaryPos) - 2;
int startQuote = bodyMetaData.substring(0, startBoundaryPos).trim().getBytes(StandardCharsets.ISO_8859_1).length + 2;
int endQuote = bodyMetaData.substring(0, endBoundaryPos).trim().getBytes(StandardCharsets.ISO_8859_1).length;
return new Position(startQuote, endQuote);
}
private void writeTo(String fileName, byte[] body, Position position) throws IOException {
try (FileOutputStream fos = new FileOutputStream("/Users/Hy-KylePan/IdeaProjects/JavaEE-Web/" + fileName)) {
int length = position.end - position.begin;
fos.write(body, position.begin, length);
fos.flush();
}
}
static class Position {
final int begin;
final int end;
Position(int begin, int end) {
this.begin = begin;
this.end = end;
}
}
```
:::spoiler 完整程式
```java=
@WebServlet(
"/upload.handle"
)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// -----------------------------------------------------------------------
PrintWriter out = resp.getWriter();
byte[] body = readBody(req);
String textBody = new String(body, StandardCharsets.ISO_8859_1);
String fileName = getFileName(textBody);
Position position = getFilePosition(req, textBody);
// out.println("<pre>: " + fileName + "\n" +
// new String(body, StandardCharsets.UTF_8) + "\n" +
// "Position: " + position.begin + ", " + position.end + "</pre>");
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
out.println("<h3> fileName: " + fileName + "</h3>");
out.println("<h3> position start: " + position.begin + "</h3>");
out.println("<h3> position end: " + position.end + "</h3>");
out.println("</body>");
writeTo(fileName, body, position);
}
private byte[] readBody(HttpServletRequest req) throws IOException {
try (ServletInputStream inputStream = req.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
baos.write(buffer, 0, bytesRead);
}
return baos.toByteArray();
}
}
private String getFileName(String metaData) {
// 利用 indexOf 找到 filename 的位置
int fileNameIndex = metaData.indexOf("filename=\"");
if (fileNameIndex != -1) {
// 截取 filename 開始到下一個雙引號的部分
int startQuote = fileNameIndex + "filename=\"".length();
int endQuote = metaData.indexOf("\"", startQuote);
if (endQuote != -1) {
// 提取文件名
return metaData.substring(startQuote, endQuote);
}
}
// 如果未找到文件名,返回空字符串或者 null,視情況而定
return "";
}
private Position getFilePosition(HttpServletRequest req, String bodyMetaData) {
String contentType = req.getContentType();
String boundaryText = contentType.substring(contentType.lastIndexOf("=") + 1);
int startBoundaryPos = bodyMetaData.indexOf("filename=\"");
startBoundaryPos = bodyMetaData.indexOf("\r\n", startBoundaryPos) + 2;
int endBoundaryPos = bodyMetaData.indexOf(boundaryText, startBoundaryPos) - 2;
int startQuote = bodyMetaData.substring(0, startBoundaryPos).trim().getBytes(StandardCharsets.ISO_8859_1).length + 2;
int endQuote = bodyMetaData.substring(0, endBoundaryPos).trim().getBytes(StandardCharsets.ISO_8859_1).length;
return new Position(startQuote, endQuote);
}
private void writeTo(String fileName, byte[] body, Position position) throws IOException {
try (FileOutputStream fos = new FileOutputStream("/Users/Hy-KylePan/IdeaProjects/JavaEE-Web/" + fileName)) {
int length = position.end - position.begin;
fos.write(body, position.begin, length);
fos.flush();
}
}
static class Position {
final int begin;
final int end;
Position(int begin, int end) {
this.begin = begin;
this.end = end;
}
}
}
```
:::
* 客戶端訪問該 API 的測試程式如下(使用 Kotlin)
```java=
fun uploadByHttpFormat(url: String,
fileName: String,
filePath: String,
boundary: String = "*****") {
val url = URL(url)
val connection = url.openConnection().apply {
doInput = true
doOutput = true
useCaches = false
// Set Http request header
setRequestProperty("Connection", "Keep-Alive")
setRequestProperty("Charset", "UTF-8")
setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
}
// Write data to http
val outputStream = DataOutputStream(connection.getOutputStream()).apply {
writeBytes("--$boundary\r\n")
writeBytes("Content-Disposition: form-data; $fileName filename=\"$fileName\"\r\n")
val inputStream = FileInputStream(filePath).let {
val totalBytes = it.readBytes()
this.write(totalBytes, 0, totalBytes.size)
}
writeBytes("\r\n--$boundary--\r\n")
flush()
}
// Get http response
val inputStream = connection.getInputStream().apply {
val outPutStream = ByteArrayOutputStream()
val buf = ByteArray(1024)
while ((read(buf)) != -1) {
outPutStream.write(buf)
}
println("Http response: \n${String(outPutStream.toByteArray(), Charset.defaultCharset())}")
}
}
```
使用單元測試運行客戶端程式
```java=
@Test
fun uploadFile() { UploadData().uploadByHttpFormat("http://127.0.0.1:8080/JavaEEHelloWorld-1.0-SNAPSHOT/upload.handle",
"Hello.png",
"/Users/Hy-KylePan/Desktop/2024-01-04.png")
}
```
> 
### 取得上傳檔案 - getPart、getParts
* 在 `Serlets 3.0` 中有新增 Part 界面,這個界面可以方便的進行檔案上傳處理;
> 
* **`getPart` 範例如下**:
1. 從註解中我們可以看到,它接收的是一個 post 請求、`multipart/form-data`
2. 必須在設置的 Servlet 中,使用 `@MultipartConfig` 否則會拋出異常;其中可以設置的屬性如下
:::warning
* 缺少 `@MultipartConfig` 則會在運行時拋出異常
> 
:::
| 屬性 | 概述 | 預設值 |
| - | - | - |
| `fileSizeThreadshold` | 設定上傳檔案的大小;當超過設定門檻,就會寫入 **暫存檔案** | 0 |
| `location` | 設定寫入檔案時的 **目錄**;可以搭配 Part#write 方法使用 | 預設為空字串 |
| `maxFileSize` | 限制上傳檔案大小 | `-1`,代表不限制大小 |
| `maxRequestSize` | 限制請求的數量 | `-1`,代表不限制次數 |
```java=
@MultipartConfig
@WebServlet(
"/upload.part"
)
public class UploadServletPart extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
// 既然是使用 POST!那就可以透過設定 `setCharacterEncoding`
// 來處理中文檔名
req.setCharacterEncoding("UTF-8");
Part part = req.getPart("usePart");
String header = part.getHeader("Content-Disposition");
String fileName = header.substring(header.indexOf("filename=\"") + "filename=\"".length(),
header.lastIndexOf("\""));
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
out.println("<h3> fileName: " + fileName + "</h3>");
out.println("</body>");
writeTo(fileName, part);
}
private void writeTo(String fileName, Part part) throws IOException {
try(
InputStream inputStream = part.getInputStream();
OutputStream outputStream = new FileOutputStream("/home/alien/IdeaProjects/JavaEEHelloWorld/" + fileName);
) {
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
}
}
}
```
> 
* 如果要抓取上傳的 **多個檔案**,可以 **使用 `getParts`,範例如下**:
這裡透過設定 `@MultipartConfig`#location 來指定接下來處理的檔案要放置在哪裡
:::info
* 如果不想這樣設置,也可以透過 `web.xml` 設置
```xml=
<servlet>
<servlet-name>multiUploadServletParts</servlet-name>
<servlet-class>com.example.javaeehelloworld.MultiUploadServletParts</servlet-class>
<multipart-config>
<location>/home/alien/IdeaProjects/JavaEEHelloWorld/</location>
</multipart-config>
</servlet>
````
:::
```java=
@MultipartConfig(
location = "/home/alien/IdeaProjects/JavaEEHelloWorld/"
)
@WebServlet(
"/upload.parts"
)
public class MultiUploadServletParts extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
req.setCharacterEncoding("UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
// 取得所有 Parts
for (Part part: req.getParts()) {
String name = part.getName();
// 判斷目標 name
if (!name.startsWith("usePart_")) {
out.println("<h3> Not target part's name: " + name + "</h3>");
continue;
}
String fileName = getFileName(part);
out.println("<h3> fileName_1: " + name + "</h3>");
part.write(fileName);
}
out.println("</body>");
}
private String getFileName(Part part) {
String header = part.getHeader("Content-Disposition");
return header.substring(header.indexOf("filename=\"") + "filename=\"".length(),
header.lastIndexOf("\""));
}
}
```
> 
上傳的對應的 Part name
> 
上傳結果
> 
### Dispatcher 轉發請求 - include/forward
* 我們可以透過處理需求的轉發,讓一個請求經過多個 Servlet 處理;接下來直接上 Servlet Dispatcher 轉發請求的範例
```mermaid
graph LR;
請求 --> 入口_Servlet
入口_Servlet -.-> |轉發| Servlet_1
入口_Servlet -.-> |轉發| Servlet_2
入口_Servlet -.-> |轉發| Servlet_3
```
1. **協作 `include` 方法**
透過 `HttpServletRequest`#getRequestDispatcher(...) 來指定要**包含**的地址;**`include` 可以將令一個 Servlet 的執行流程「包含進」這一次的流程**
```java=
@WebServlet(
"/dispatch.entry"
)
public class EntryServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Entry... start");
// 取得轉發的目標
// 其中也可以帶入 GET 請求的參數
// dispatch.include?weather=sunny
// 之後可以透過 getParameter("weather") 取得參數
RequestDispatcher requestDispatcher = request.getRequestDispatcher("dispatch.include");
// 使用 include 轉發
requestDispatcher.include(request, response);
writer.println("Entry... done");
writer.close();
}
}
// ----------------------------------------------------------
@WebServlet(
"/dispatch.include"
)
public class IncludeServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
writer.println("Include... handling");
}
}
```
從結果可以看到,是 **兩個 Servlet 共同處理** 的這次的請求
> 
2. **`forward` 方法**(較常被使用到)
同樣透過 `HttpServletRequest`#getRequestDispatcher(...) 來指定要**轉發**的地址;**`forward` 可以將令一個 Servlet 的執行流程「轉發給」另一個 Servlet**
```java=
@WebServlet(
"/dispatch.entry2"
)
public class EntryServlet2 extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException, ServletException {
PrintWriter writer = response.getWriter();
writer.println("Entry... start");
RequestDispatcher requestDispatcher = request.getRequestDispatcher("dispatch.forward");
requestDispatcher.forward(request, response);
writer.println("Entry... done");
writer.close();
}
}
// ----------------------------------------------------------
@WebServlet(
"/dispatch.include"
)
public class IncludeServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
writer.println("Include... handling");
}
}
```
從結果可以看到,是 **被轉發的 Servlet 單獨處理** 的這次的請求
> 
## 服務器響應 HttpServletResponse
### 響應者 - 響應 Header
* 這邊要說的是服務端,如何透過 Servlet API 回應使用者請求的 Header
```mermaid
graph LR;
subgraph HTTP響應_Header
狀態行
響應_Header
通用_Header
實體_Header
其他
end
```
* **響應 Header 字段**(`Request Header Fields`)
服務器端要回應給客戶端的 Header;也就是 **流「出」伺服器的資料,由伺服器設定**(站在伺服器的角度)
| Header Data | 概述 |
| - | - |
| `Accept-Ranges` | 是否接受字節範圍請求 |
| `Age` | 可以用來推算資源創建後經歷的時間 |
| `Etag` | 資源的匹配信息 |
| `Location` | 令客戶端 **重新定向**,指定的 URI |
| `Proxy-Authoriztion` | 給代理服務器的驗證 |
| `Retry-After` | 如果要再次訪問伺服器,那要嘗試的時機 |
| `Server` | Http 服務器的相關訊息 |
| `Vary` | 代理服務器 **緩存的管理信息** |
| `WWW-Authenticate` | 服務器對客戶端的驗證信息 |
* JavaEE Servlet 可以透過 HttpServletResponse 的幾個方法來設定回應給使用者的標頭(Header)
* **設定 `setHeader` / 添加`addHeader`**
* **設定整數 `setIntHeader` / 添加整數 `addIntHeader`**
* **設定日期 `setDateHeader` / 添加日期 `setDateHeader`**
### 回應緩衝區
* **容器可以對回應給使用的數據進行緩衝**(並非必須,通常容器會對回應進行緩衝),Servlet 有提供幾個緩衝相關的方法
* **緩衝大小控制**
取得緩衝大小 `getBufferSize()` / 設定緩衝大小 `setBufferSize()`
:::warning
* 要在呼叫 `getWriter`、`getOutStream` 方法之前呼叫 `setBufferSize()` 設置緩存才行,否則會拋出異常
:::
* `isCommited()`:**查看緩衝區是否已經被提交**
在緩衝區未滿之前,設定的回應內容不會立刻傳到客戶端(可以透過 `isCommited()` 確認)
* `resetBuffer()`:**重設緩衝**
:::danger
* 重置回應內容,但不會清除已經設定的標頭(Header)內容
:::
* `reset()`:**不只重置內容,連同標頭一起重置**
* `flushBuffer()`:**強制刷新緩衝到客戶端**
### 請求結束的時機
* HttpServletResponse 物件若被容器關閉,則會清除的回應內容;而 Serlvet 被關閉的時機有
* `service()` 方法結束
* 呼叫 `sendRedirect()` 方法
* 呼叫 `sendError()` 方法
* 呼叫 AsyncContext#`complete()` 方法
### 正確輸出字元 - Local、CharacterEncoding、ContentType MIME
* **「亂碼」問題**:
透過 HttpServletResponse#`getWriter()` 可以,在沒有任何內容型態、編碼之前,**預設是使用 `IOS-8859-1` 編碼**
所以如果在回應時回應中文則會 **導致「亂碼」問題**(因為使用者遊覽器不清楚你使用了非 `IOS-8859-1` 編碼,導致解碼錯誤),而在 JavaEE Servlet 解決問題的方法有三種
1. **Local 物件設定**(需要有設定 `Accept-Language`)
2. **CharacterEncoding 設定**
3. **ContentType 設定**(設定 MIME, `Multipuerpose Internet Mail Extensions`)
* **Local 物件**:
1. 如果遊覽器端的 Header 有設置 `Accept-Language` 標頭,那就可以透過 HttpServletResponse#`getLocale` 方法取得物件
```java=
Locale locale = response.getLocale();
```
2. 可以透過 HttpServletResponse#`setLocale` 方法設定回應的標頭
```java=
response.setLocale(Locale.TAIWAN);
```
:::info
* 瀏覽器端收到時會顯示在響應的 Header 的 `Content-Length` 中
:::
透過這個設定,可以讓遊覽器知道要如何去判斷傳來的文字的編碼,這讓遊覽器可以正常輸出中文
```java=
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setLocale(Locale.TAIWAN);
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1> After set, 你好!! </h1>");
out.println("<h3> response locale: " + response.getLocale() + "</h3>");
for (String headerName : response.getHeaderNames()) {
for (String value : response.getHeaders(headerName)) {
out.println("<h3> header: " + headerName + ", value" + value + "</h3>");
}
}
out.println("</body>");
out.close();
}
```
> 中文可以 正常顯示
>
> 
* Character 設定,**使用 `setCharacterEncoding`、`setContentType` 方法來設定**
:::warning
* 如果使用 `setCharacterEncoding`、`setContentType` 設定,那 `setLocale` 就會失效!(也就是說這兩者的優先權更高)
:::
* **`setCharacterEncoding` 設定**:透過這個方法可以內容編碼的方式
```java=
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
// 設定回覆時的編碼
response.setCharacterEncoding("Big5");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
// 設定 CharacterEncoding
out.println("<h1> After setCharacterEncoding, 你好!! </h1>");
out.println("<h3> response locale: " + response.getLocale() + "</h3>");
for (String headerName : response.getHeaderNames()) {
for (String value : response.getHeaders(headerName)) {
out.println("<h3> header: " + headerName + ", value" + value + "</h3>");
}
}
out.println("</body>");
out.close();
}
```
> 
* **`setContentType` 設定**:
設定的是 **MIME**(`Multipurpose Internet Mail Extensions`) 的種類,用來告訴使用者返回的資料的種類
> 知道種類後,瀏覽器才能正確的去處理資料
常見的 **MIME** 如下…
* `text/html` 可以順帶設定 `charset`
* `application/pdf`
* `application/x-zip`
* `image/jpeg`
> [**完整的 MIME 類型**](https://www.iana.org/assignments/media-types/media-types.xhtml) 請點連結
```java=
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setContentType("text/html; charset=big5");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
out.println("<body>");
// 設定 ContentType
out.println("<h1> After setContentType, 你好!! </h1>");
out.println("<h3> response locale: " + response.getLocale() + "</h3>");
for (String headerName : response.getHeaderNames()) {
for (String value : response.getHeaders(headerName)) {
out.println("<h3> header: " + headerName + ", value" + value + "</h3>");
}
}
out.println("</body>");
out.close();
}
```
:::warning
* 其它非官方提供的 MIME 類型也是可以使用的,像是 `MessagePack` 傳輸方式,它常見的做法是使用 `x` 做延伸
它可能會帶出的 Header 為 `Content-Type: application/x-msgpack`
:::
> 
### Stream 串流輸出 - pdf / image
* 我們也可以將 Stream 直接輸到瀏覽器中(這部份 Writer 做不到)
Servlet 可以透過 HttpServletResponse 取得 `getOutputStream` 來進行 Stream 流輸出;範例如下
```java=
@WebServlet(
urlPatterns = { "/download.pdf" }
)
public class DownloadServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response) throws IOException {
// MIME 要設定正確唷!
response.setContentType("application/pdf");
try(InputStream in = getServletContext().getResourceAsStream("/WEB-INF/test.pdf");
OutputStream out = response.getOutputStream()) {
byte[] tmp = new byte[1024];
int length;
while ((length = in.read(tmp)) != -1) {
out.write(tmp, 0, length);
}
}
}
}
```
> 
* 範例二:
客戶端使用流(Stream)讀取服務器端的圖片
```java=
@WebServlet("/image.get")
public class GetImage extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("image/jpeg");
try (InputStream inputStream = new BufferedInputStream(
new FileInputStream("/home/alien/IdeaProjects/JavaEEHelloWorld/test_pic.png"));
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
}
}
```
> 
### 重新導向 3xx 、傳送錯誤 4xx
* 向客戶端發出 **重新導向**
* Servlet 的 `forward` 轉發行為並不會讓客戶端遊覽器感知到,也就是轉發過程仍算在「同一個請求」,不會觸發 3xx 狀態
```mermaid
graph RL;
客戶端 --> |請求| 伺服器
伺服器 -.-> |200| 客戶端
subgraph 伺服器
subgraph Web_容器
Servlet_A --> |forward| Servlet_B
end
end
```
* 如果要真正觸發則需要使用 HttpServletResponse#`sendRedirect` 方法
```java=
response.sendRedirect("https://...")
```
1. 首先在發出重新導向時,客戶端會收到 3xx 的狀態碼
```mermaid
graph RL;
客戶端 --> |1. 請求| 伺服器_A
伺服器_A -.-> |1. 300| 客戶端
subgraph 伺服器_A
subgraph Web_容器_A
Servlet_A
end
end
```
2. 客戶端收到重新導向後,會在重新發出請求
```mermaid
graph RL;
客戶端 --> |2. 請求| 伺服器_B
伺服器_B -.-> |2. 200| 客戶端
subgraph 伺服器_B
subgraph Web_容器_B
Servlet_B
end
end
```
> 
* 向客戶端發出 **傳送錯誤**
如果在客戶端處理請求時,發生一些錯誤(找不到資源、或是參數不合法);可以透過 HttpServletResponse#`sendError(...)` 方法來像客戶端發出 4xx 錯誤
```java=
// 發出 404 錯誤
response.sendError(HttpServletResponse.SC_NOT_FOUND);
```
> 除了 404 之外,以下還有其他 4xx 錯誤
>
> 
## HTTP 更多知識點
### 通用、實體 Header
通用、實體 Header 是 「請求」、「響應」兩方都可以使用的 MetaData;
* **通用 Header 字段**(`General Header Fields`)
「請求」(客戶端)、「響應」(伺服器端)兩方都會使用的 Header,可用來描述雙方的狀態
| Header Data | 概述 |
| - | - |
| `Cache-Control` | 控制緩存的行為 |
| `Connection` | 連接管理、逐跳管理 |
| `Date` | 創建資料的時間 |
| `Pragma` | 請求、響應的指令 |
| `Trailer` | 資料末端的首部訊息 |
| `Transfer-Encoding` | 指定資料主體的 **編碼方式** |
| `Upgrade` | 升級為其他協議 |
| `Via` | 代理服務器的相關訊息 |
| `Warning` | 警告通知 |
* **實體 Header 字段**(`Entity Header Fields`)
針對「請求」、「響應」資料的 **實體部份**;用來補充資源內容更新時間、實體訊息
| Header Data | 概述 |
| - | - |
| `Allow` | 資源可以支持的 HTTP 方式(`Get`、`Post`... 等等) |
| `Content-Encoding` | 內容主體通用的編碼方式 |
| `Content-Language` | 內容語言(人們用得自然語言) |
| `Content-Length` | 實體主體的大小,但為`Byte` |
| `Content-Location` | 重新導向 URI 的位置 |
| `Content-MD5` | 用於驗證訊息主體的完整性(通常 128 bit) |
| `Content-Range` | 指定實體資料的範圍 |
| `Content-Type` | 內容主體的媒體類型 |
| `Expires` | 內容實體的過期時間 |
| `Last-Modified` | 資源最後修改時間 |
### 非正式 Header
* 在 Http 通訊協議交互時,也會使用到非協議中的 Header(這裡是指 `Http/1.1`,基於 [**RFC 2616**](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)),常見的像是
* `Cookie`
* `Set-Cookie`
* `Content-Disposition`:它通常用於指示應該如何處理收到的消息主體,特別是應該如何處理附件(`attachment`)
> 主要使用在HTTP協議中,特別是在處理文件上傳或下載時
```shell=
Content-Disposition: attachment; filename="example.txt"
```
:::success
* 這些非正式的 Header 都規範在 [**RFC 4229**](https://www.rfc-editor.org/rfc/rfc4229.html#section-2.1.96) 的 HTTP Header Field Registration 中
> 
:::
### HTTP Header 行為 - 端到端、逐跳
* HTTP 首部字段將定義有分為大致的兩個種類
1. **端到端的 Header**(`End-to-end Header`)
分別在此類別中的 Header **會將 Header 轉發給請求、響應端**,也就是服務器、客戶端都會收到資訊,**不會被中途劫走**
>
2. **逐跳的 Header**(`Hop-by-hop Header`)
分別在此類別中的 Header,**只會對單次轉發有效**!如果中途有緩存、代理,就會被移除,不會再往下傳遞
> 如果要使用 `Hop-by-hop Header`,那就要設置 `Connection` 字段到 Header 中
### HttpServletRequest 監聽器
* 我們可以對 HttpServletRequest 做監聽(請求、屬性操作),範例如下
1. 監聽 HttpServletRequest 請求
```java=
@WebListener
public class ServletRequestListenerImpl implements ServletRequestListener {
private static final String REQ_CATCHER_KEY = "RequestCatcher";
@Override
public void requestInitialized(ServletRequestEvent sre) {
sre.getServletRequest().setAttribute(REQ_CATCHER_KEY, "Hello, I am request catcher");
}
@Override
public void requestDestroyed(ServletRequestEvent sre) {
sre.getServletRequest().removeAttribute(REQ_CATCHER_KEY);
}
}
```
2. 監聽 HttpServletRequest 屬性操作(包括添加、移除、替代)
```java=
@WebListener
public class ServletRequestAttrsListenerImpl implements ServletRequestAttributeListener {
public static final String REQ_ATTR_CATCHER_KEY = "RequestAttrsCatcher";
@Override
public void attributeAdded(ServletRequestAttributeEvent srae) {
srae.getServletContext().setAttribute(REQ_ATTR_CATCHER_KEY, "Hello, I am request attrs catcher");
}
@Override
public void attributeRemoved(ServletRequestAttributeEvent srae) {
ServletRequestAttributeListener.super.attributeRemoved(srae);
}
@Override
public void attributeReplaced(ServletRequestAttributeEvent srae) {
ServletRequestAttributeListener.super.attributeReplaced(srae);
}
}
```
* 測試程式如下
```java=
@WebServlet("/show.req.attrs")
public class ShowContextAttributeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Hello World</title>");
out.println("</head>");
Enumeration<String> names = req.getAttributeNames();
while (names.hasMoreElements()) {
String attrName = names.nextElement();
out.println("<h3> attrName : " + attrName + ", value: " + req.getAttribute(attrName) + "</h3>");
}
out.println("<h3> context attrName : " + ServletRequestAttrsListenerImpl.REQ_ATTR_CATCHER_KEY +
", value: " + req.getServletContext().getAttribute(ServletRequestAttrsListenerImpl.REQ_ATTR_CATCHER_KEY) + "</h3>");
out.println("<body>");
out.println("</body>");
out.close();
}
}
```
> 
## Appendix & FAQ
:::info
:::
###### tags: `Web`