HTTP檔案上傳原理

^_^果凍^_^發表於2019-07-31

前言

對於這塊知識點,我一直都是模糊的,不是非常清楚的。在平時的工作中,遇到上傳的問題,也沒有深入的去研究過,也都是直接用別人封裝好的類來完成自己的工作。某一天,看了本書,說到這個知識點,一臉茫然,覺的有必要去深入的學習一下,至少要讓自己明白HTTP檔案上傳的過程,這個原理,以便將來出現問題,也能通過原理進行深入的分析,而不是等問題來了,兩眼一抹黑,然後漫無目的的百度。哦,如果是那樣子,那該是多麼的痛苦,多麼的無助。

所以查缺補漏,以免讓將來的自己感到無助、痛苦,甚至難堪,走起!!!

HTTP上傳原理

我們在開發的時候,當要用到檔案上傳功能時,前端開發人員都會告訴你以下幾條金科律令:

  • 提交方式必須為post
  • 表單中有檔案上傳的表單項必須為<input type="file"/>
  • 必須指定表單型別enctype="multipart/form-data"

是的,我們必須按照上面這三條鐵令進行設定,否則就無法上傳檔案。比如我們一般會這麼寫:

<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<body>
<form action="<%=request.getContextPath() %>/UploadServletDemo" enctype="multipart/form-data" method="post">
    上傳使用者:<input type="text" name="username"><br/>
    上傳檔案1:<input type="file" name="file1"><br/>
    上傳檔案2:<input type="file" name="file2"><br/>
     
    <input type="submit" value="提交">
</form>
</body>
</html>

我這裡寫了一個簡單的Servlet:

@WebServlet("/UploadServletDemo")
public class UploadServletDemo extends HttpServlet{
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");

        // 獲取表單(POST)資料
        ServletInputStream in = request.getInputStream();//此方法得到所有的提交資訊,不僅僅只有內容

        // 轉換流
        InputStreamReader inReaser = new InputStreamReader(in);

        // 緩衝流
        BufferedReader reader = new BufferedReader(inReaser);
        String str = null;
        while ((str=reader.readLine()) != null){
            System.out.println(str);
        }
    }
}

我們把程式跑起來,然後通過Fiddler進行抓包,可以看到我們傳送的Post請求中,請求體中有以下這樣的資料:

POST http://localhost:8080/javawebservlet_war/UploadServletDemo HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 446
Cache-Control: max-age=0
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqj67FUBQUHXZj78G
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://localhost:8080/javawebservlet_war/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,und;q=0.7,zh-TW;q=0.6
Cookie: JSESSIONID=6BE280EF3CBE213F73430FFDF015DE97

------WebKitFormBoundaryqj67FUBQUHXZj78G
Content-Disposition: form-data; name="username"

abc
------WebKitFormBoundaryqj67FUBQUHXZj78G
Content-Disposition: form-data; name="file1"; filename="檔案1.txt"
Content-Type: text/plain

ABC檔案1
------WebKitFormBoundaryqj67FUBQUHXZj78G
Content-Disposition: form-data; name="file2"; filename="檔案2.txt"
Content-Type: text/plain

BDF檔案2
------WebKitFormBoundaryqj67FUBQUHXZj78G--

到這裡,我們就大概就知道了HTTP上傳檔案的原理了。HTTP把需要上傳的表單的所有資料按照一定的格式存放在請求體中,對於檔案也是同樣的。

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryqj67FUBQUHXZj78G表示要上傳附件,其中boundary表示分隔符,如果表單中有多項,就要使用boundary進行分隔,每個表單項由------FormBoundary開始,以------FormBoundary結尾。例如這樣:

------FormBoundary
Content-Disposition: form-data; name="param1"

value1
------FormBoundary

這個boundary的值是由瀏覽器生成的,由瀏覽器來保證與上傳內容不重複。在每個分隔項裡,需要我們去重點關注Content-Disposition訊息頭,其中第一個引數總是固定不變的form-data,name表示表單元素屬性名,回車換行符後面的內容就是元素的值。還有Content-Type表示我們上傳的檔案的MIME型別,我們在伺服器端需要根據這個進行檔案的區分。

HTTP就是按照這種格式,把表單中的資料封裝成一個請求一股腦的發給伺服器端,伺服器端根據這種規則對接收到的請求進行解析,從而完成檔案上傳功能。

最後一個boundary的結尾會多兩個--

FileUpload元件

通過上面的描述,我們可以知道完成檔案上傳功能,重點工作不是在於客戶端,而是在於伺服器端。伺服器端需要根據客戶端傳送過來的請求,根據上面說的規則對請求報文進行解析,從而提取出上傳的檔案內容。可以看到,雖然上面的規則比較簡單,但是用不同的開發語言來一次性寫出沒有Bug的解析程式,也不是那麼簡單的。而且這種實現過一遍,就可以大家共享的東西就非常適合開發成元件供大家一起使用,所以呢,開源社群就開發了這樣的一個元件,這個元件來給我們完成了上面規則的編碼,而我們需要做的就是去學會使用這個元件,就這麼簡單!

下面就通過一個簡單的FileUploadDemo程式來總結一下如何使用FileUpload元件。

FileUpload Demo

下面來一段簡單的使用Demo。

@WebServlet("/FileUpload")
public class FileUploadDemo extends HttpServlet {
    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        DiskFileItemFactory fac = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(fac);
        upload.setFileSizeMax(10 * 1024 * 1024);
        upload.setSizeMax(20 * 1024 * 1024);

        if (upload.isMultipartContent(request)) {
            try {
                List<FileItem> list = upload.parseRequest(request);
                for (FileItem item : list) {
                    if (item.isFormField()) {
                        String fileName = item.getFieldName();
                        String value = item.getString("UTF-8");
                        System.out.println(fileName + ":" + value);
                    } else {
                        String name = item.getName();
                        String id = UUID.randomUUID().toString();
                        name = id + name;

                        String realPath = getServletContext().getRealPath("/upload");
                        File file = new File(realPath, name);
                        item.write(file);
                        item.delete();

                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("不處理!");
        }
    }
}

總結

這篇文章對通過HTTP協議進行檔案上傳原理進行了比較詳細的分析和總結,希望對大家有幫助!

2019年7月31日 於內蒙古呼和浩特。

相關文章