Làm thế nào để tải lên tập tin HTTP hoạt động?


528

Khi tôi gửi một biểu mẫu đơn giản như thế này với một tệp đính kèm:

<form enctype="multipart/form-data" action="http://localhost:3000/upload?upload_progress_id=12344" method="POST">
<input type="hidden" name="MAX_FILE_SIZE" value="100000" />
Choose a file to upload: <input name="uploadedfile" type="file" /><br />
<input type="submit" value="Upload File" />
</form>

Làm thế nào để nó gửi các tập tin nội bộ? Là tệp được gửi như một phần của cơ thể HTTP dưới dạng dữ liệu? Trong các tiêu đề của yêu cầu này, tôi không thấy bất cứ điều gì liên quan đến tên của tệp.

Tôi chỉ muốn biết các hoạt động nội bộ của HTTP khi gửi tệp.


Tôi đã không sử dụng trình thám thính trong một thời gian nhưng nếu bạn muốn xem những gì đang được gửi trong yêu cầu của bạn (vì nó là máy chủ thì đó là một yêu cầu) đánh hơi nó. Câu hỏi này quá rộng. SO là nhiều hơn cho các câu hỏi lập trình cụ thể.
paparazzo

... khi người đánh hơi đi, fiddler là vũ khí lựa chọn của tôi. Bạn thậm chí có thể xây dựng các yêu cầu kiểm tra của riêng bạn để xem cách họ đăng.
Phil Cooper

Đối với những người quan tâm, cũng xem " MAX_FILE_SIZEtrong PHP - điểm gì" trên stackoverflow.com/q/1381364/632951
Pacerier 24/07/2015

Tôi thấy MAX_FILE_SIZE kỳ lạ. vì tôi có thể sửa đổi html của mình trong chrome thành 100000000 trước khi đăng nó để nó có giá trị tốt hơn. Hoặc là 1. có nó trong một cookie với hàm băm an toàn thông qua muối để cookie nếu được sửa đổi, máy chủ có thể xác thực và ném ngoại lệ (như webpack hoặc playframework cả hai) hoặc một số loại xác thực mẫu mà mọi thứ không thay đổi. @ 0xSina
Dean Hiller

Câu trả lời:


320

Chúng ta hãy xem điều gì xảy ra khi bạn chọn một tệp và gửi biểu mẫu của bạn (Tôi đã cắt bớt các tiêu đề cho ngắn gọn):

POST /upload?upload_progress_id=12344 HTTP/1.1
Host: localhost:3000
Content-Length: 1325
Origin: http://localhost:3000
... other headers ...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryePkpFF7tjBAqx29L

------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="MAX_FILE_SIZE"

100000
------WebKitFormBoundaryePkpFF7tjBAqx29L
Content-Disposition: form-data; name="uploadedfile"; filename="hello.o"
Content-Type: application/x-object

... contents of file goes here ...
------WebKitFormBoundaryePkpFF7tjBAqx29L--

LƯU Ý: mỗi chuỗi ranh giới phải được thêm tiền tố --, giống như ở cuối chuỗi ranh giới cuối cùng. Ví dụ trên đã bao gồm điều này, nhưng nó có thể dễ bỏ lỡ. Xem bình luận của @Andreas bên dưới.

Thay vì URL mã hóa các tham số biểu mẫu, các tham số biểu mẫu (bao gồm dữ liệu tệp) được gửi dưới dạng các phần trong tài liệu nhiều phần trong phần thân của yêu cầu.

Trong ví dụ trên, bạn có thể thấy đầu vào MAX_FILE_SIZEvới giá trị được đặt trong biểu mẫu, cũng như một phần có chứa dữ liệu tệp. Tên tệp là một phần của Content-Dispositiontiêu đề.

Các chi tiết đầy đủ có ở đây .


7
@ source.rar: Không. Máy chủ web luôn (gần như?) luôn được xâu chuỗi để chúng có thể xử lý các kết nối đồng thời. Về cơ bản, quy trình trình nền đang nghe trên cổng 80 ngay lập tức thực hiện nhiệm vụ phục vụ cho một luồng / quy trình khác để nó có thể quay lại để nghe kết nối khác; ngay cả khi hai kết nối đến đến cùng một thời điểm, chúng sẽ chỉ ngồi trong bộ đệm mạng cho đến khi trình nền sẵn sàng đọc chúng.
eggyal

10
Giải thích luồng là một chút không chính xác vì có các máy chủ hiệu suất cao được thiết kế dưới dạng luồng đơn và sử dụng máy trạng thái để nhanh chóng thay phiên tải xuống các gói dữ liệu từ các kết nối. Thay vào đó, trong TCP / IP, cổng 80 là cổng nghe, không phải cổng được truyền dữ liệu.
slebetman

9
Khi ổ cắm nghe IP (cổng 80) nhận được kết nối, một ổ cắm khác được tạo trên một cổng khác, thường có số ngẫu nhiên trên 1000. Ổ cắm này sau đó được kết nối với ổ cắm từ xa để cổng 80 miễn phí để nghe các kết nối mới.
slebetman

11
@slebetman Trước hết, đây là về HTTP. Chế độ hoạt động FTP không áp dụng ở đây. Thứ hai, ổ cắm nghe không bị chặn trên mọi kết nối. Bạn có thể có nhiều kết nối đến một cổng, vì các bên khác có các cổng để liên kết kết thúc của riêng họ.
Slotos

33
Lưu ý rằng chuỗi ranh giới được truyền qua như một phần của trường tiêu đề Kiểu nội dung ngắn hơn 2 ký tự so với chuỗi ranh giới cho các phần riêng lẻ bên dưới. Tôi đã dành một giờ cố gắng để tìm hiểu lý do tại sao trình tải lên của tôi không hoạt động vì khá khó để nhận ra rằng thực sự chỉ có 4 dấu gạch ngang trong chuỗi ranh giới đầu tiên nhưng 6 dấu gạch ngang trong các chuỗi ranh giới khác. Nói cách khác: Khi sử dụng chuỗi ranh giới để phân tách dữ liệu biểu mẫu riêng lẻ, nó phải được thêm tiền tố bởi hai dấu gạch ngang: - Tất nhiên, nó được mô tả trong RFC1867 nhưng tôi nghĩ nó cũng nên được chỉ ra ở đây
Andreas

279

Làm thế nào để nó gửi các tập tin nội bộ?

Định dạng được gọi multipart/form-data, như được hỏi tại: enctype = 'Multipart / form-data' nghĩa là gì?

Tôi sẽ:

  • thêm một số tài liệu tham khảo HTML5
  • giải thích lý do tại sao anh ta đúng với một ví dụ gửi mẫu

Tài liệu tham khảo HTML5

ba khả năng cho enctype:

  • x-www-urlencoded
  • multipart/form-data(thông số kỹ thuật trỏ đến RFC2388 )
  • text-plain. Đây là "không đáng tin cậy bằng máy tính", vì vậy nó không bao giờ nên được sử dụng trong sản xuất và chúng tôi sẽ không nhìn sâu hơn vào nó.

Cách tạo các ví dụ

Khi bạn thấy một ví dụ về mỗi phương thức, nó sẽ trở nên rõ ràng về cách chúng hoạt động và khi nào bạn nên sử dụng từng phương thức.

Bạn có thể tạo các ví dụ bằng cách sử dụng:

Lưu biểu mẫu vào một .htmltệp tối thiểu :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>upload</title>
</head>
<body>
  <form action="http://localhost:8000" method="post" enctype="multipart/form-data">
  <p><input type="text" name="text1" value="text default">
  <p><input type="text" name="text2" value="a&#x03C9;b">
  <p><input type="file" name="file1">
  <p><input type="file" name="file2">
  <p><input type="file" name="file3">
  <p><button type="submit">Submit</button>
</form>
</body>
</html>

Chúng tôi thiết lập các giá trị văn bản mặc định a&#x03C9;b, mà phương tiện aωbωU+03C9, đó là các byte 61 CF 89 62trong UTF-8.

Tạo tập tin để tải lên:

echo 'Content of a.txt.' > a.txt

echo '<!DOCTYPE html><title>Content of a.html.</title>' > a.html

# Binary file containing 4 bytes: 'a', 1, 2 and 'b'.
printf 'a\xCF\x89b' > binary

Chạy máy chủ echo nhỏ của chúng tôi:

while true; do printf '' | nc -l 8000 localhost; done

Mở HTML trên trình duyệt của bạn, chọn các tệp và nhấp vào gửi và kiểm tra thiết bị đầu cuối.

nc in yêu cầu nhận được.

Đã thử nghiệm trên: Ubuntu 14.04.3, ncBSD 1.105, Firefox 40.

nhiều dữ liệu / biểu mẫu

Firefox đã gửi:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150
Content-Length: 834

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text1"

text default
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="text2"

aωb
-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain

Content of a.txt.

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html

<!DOCTYPE html><title>Content of a.html.</title>

-----------------------------735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream

aωb
-----------------------------735323031399963166993862150--

Đối với tệp nhị phân và trường văn bản, các byte 61 CF 89 62( aωbtrong UTF-8) được gửi theo nghĩa đen. Bạn có thể xác minh rằng với nc -l localhost 8000 | hd, nói rằng các byte:

61 CF 89 62

đã được gửi ( 61== 'a' và 62== 'b').

Vì vậy, rõ ràng rằng:

  • Content-Type: multipart/form-data; boundary=---------------------------735323031399963166993862150đặt loại nội dung thành multipart/form-datavà nói rằng các trường được phân tách bằng boundarychuỗi đã cho .

    Nhưng lưu ý rằng:

    boundary=---------------------------735323031399963166993862150
    

    có hai cha ít --hơn rào cản thực tế

    -----------------------------735323031399963166993862150
    

    Điều này là do tiêu chuẩn yêu cầu ranh giới bắt đầu bằng hai dấu gạch ngang --. Các dấu gạch ngang khác dường như là cách Firefox chọn để thực hiện ranh giới tùy ý. RFC 7578 đề cập rõ ràng rằng hai dấu gạch ngang hàng đầu đó --là bắt buộc:

    4.1. "Ranh giới" Tham số của nhiều dữ liệu / biểu mẫu

    Cũng như các loại nhiều phần khác, các phần được phân định bằng dấu phân cách ranh giới, được xây dựng bằng CRLF, "-" và giá trị của tham số "ranh giới".

  • mỗi trường có một số tiêu đề phụ trước dữ liệu của nó : Content-Disposition: form-data;, trường name, the filename, theo sau là dữ liệu.

    Máy chủ đọc dữ liệu cho đến chuỗi ranh giới tiếp theo. Trình duyệt phải chọn một ranh giới sẽ không xuất hiện trong bất kỳ trường nào, vì vậy đây là lý do tại sao ranh giới có thể khác nhau giữa các yêu cầu.

    Bởi vì chúng tôi có ranh giới duy nhất, không cần mã hóa dữ liệu: dữ liệu nhị phân được gửi như hiện có.

    TODO: kích thước ranh giới tối ưu ( log(N)tôi đặt cược) và tên / thời gian chạy của thuật toán tìm thấy nó là gì? Đã hỏi tại: /cs/39487/find-the-shortest- resultence-that-is-not-a-sub-resultence-of-a-set-of- result

  • Content-Type được tự động xác định bởi trình duyệt.

    Làm thế nào nó được xác định chính xác đã được hỏi tại: Làm thế nào loại mime của một tệp tải lên được xác định bởi trình duyệt?

application / x-www-form-urlencoding

Bây giờ thay đổi enctypethành application/x-www-form-urlencoded, tải lại trình duyệt và gửi lại.

Firefox đã gửi:

POST / HTTP/1.1
[[ Less interesting headers ... ]]
Content-Type: application/x-www-form-urlencoded
Content-Length: 51

text1=text+default&text2=a%CF%89b&file1=a.txt&file2=a.html&file3=binary

Rõ ràng dữ liệu tệp không được gửi, chỉ có các tên cơ sở. Vì vậy, điều này không thể được sử dụng cho các tập tin.

Đối với trường văn bản, chúng tôi thấy rằng các ký tự có thể in thông thường như abđược gửi trong một byte, trong khi các ký tự không in được như thế 0xCF0x89chiếm 3 byte mỗi ký tự : %CF%89!

So sánh

Tải lên tệp thường chứa nhiều ký tự không in được (ví dụ hình ảnh), trong khi các hình thức văn bản hầu như không bao giờ thực hiện.

Từ các ví dụ chúng ta đã thấy rằng:

  • multipart/form-data: thêm một vài byte chi phí biên cho thông báo và phải dành thời gian để tính toán nó, nhưng gửi mỗi byte trong một byte.

  • application/x-www-form-urlencoded: có một ranh giới byte đơn trên mỗi trường ( &), nhưng thêm hệ số chi phí tuyến tính3x cho mỗi ký tự không in được.

Do đó, ngay cả khi chúng tôi có thể gửi tệp cùng application/x-www-form-urlencoded, chúng tôi sẽ không muốn, vì nó rất kém hiệu quả.

Nhưng đối với các ký tự có thể in được tìm thấy trong các trường văn bản, nó không quan trọng và tạo ra ít chi phí hơn, vì vậy chúng tôi chỉ sử dụng nó.


1
Làm thế nào bạn sẽ thêm một tập tin đính kèm nhị phân? (tức là một hình ảnh nhỏ) - Tôi có thể thấy việc thay đổi các giá trị cho các thuộc tính Content-DispositionContent-Typethuộc tính nhưng làm thế nào để xử lý 'nội dung'?
Blurfus 5/2/2015

3
@ianbeks Trình duyệt tự động thực hiện trước khi gửi yêu cầu. Tôi không biết nó sử dụng phương pháp phỏng đoán nào, nhưng rất có thể phần mở rộng tập tin nằm trong số đó. Điều này có thể trả lời câu hỏi: stackoverflow.com/questions/1201945/
Kiếm

3
@CiroSantilli 事件 法轮功 纳米比亚 威 Tôi nghĩ câu trả lời này tốt hơn nhiều so với câu hỏi được chọn. Nhưng vui lòng xóa nội dung không liên quan khỏi hồ sơ của bạn. Nó chống lại tinh thần của SO.
smwikipedia

2
@smwikipedia cảm ơn bạn đã trích dẫn rfc và thích câu trả lời này! Về tên người dùng: với tôi, tinh thần của SO là mọi người nên có thông tin tốt nhất mọi lúc. ~ ~ Hãy tiếp tục thảo luận này để twitter hoặc meta. Sự thanh bình.
Ciro Santilli 郝海东 冠状 病 事件

1
@KumarHarsh không đủ chi tiết để trả lời tôi nghĩ. Hãy mở một câu hỏi siêu chi tiết mới.
Ciro Santilli 郝海东 冠状 病 事件

62

Gửi tệp dưới dạng nội dung nhị phân (tải lên mà không cần biểu mẫu hoặc FormData)

Trong các câu trả lời / ví dụ đã cho, tệp (rất có thể) được tải lên bằng biểu mẫu HTML hoặc sử dụng API FormData . Tệp chỉ là một phần của dữ liệu được gửi trong yêu cầu, do đó là multipart/form-data Content-Typetiêu đề.

Nếu bạn muốn gửi tệp dưới dạng nội dung duy nhất thì bạn có thể trực tiếp thêm tệp đó làm nội dung yêu cầu và bạn đặt Content-Typetiêu đề thành loại MIME của tệp bạn đang gửi. Tên tập tin có thể được thêm vào trong Content-Dispositiontiêu đề. Bạn có thể tải lên như thế này:

var xmlHttpRequest = new XMLHttpRequest();

var file = ...file handle...
var fileName = ...file name...
var target = ...target...
var mimeType = ...mime type...

xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.send(file);

Nếu bạn không (muốn) sử dụng biểu mẫu và bạn chỉ muốn tải lên một tệp duy nhất thì đây là cách dễ nhất để đưa tệp của bạn vào yêu cầu.


Làm cách nào để định cấu hình dịch vụ phía máy chủ cho việc này với Asp.Net 4.0? Nó cũng sẽ xử lý nhiều tham số đầu vào, chẳng hạn như userId, path, captionText, v.v.?
Asle G

1
@AsleG Không, chỉ để gửi một tệp như nội dung yêu cầu của bạn. Tôi không phải là một chuyên gia Asp.Net, nhưng bạn chỉ cần rút nội dung (một blob) ra khỏi yêu cầu và lưu nó vào một tệp bằng cách sử dụng Content-Typetừ tiêu đề.
Héo

@AsleG Có lẽ liên kết này có thể giúp đỡ
Héo

@wilt Nếu tôi không sử dụng biểu mẫu, nhưng tôi muốn sử dụng API formdata, tôi có thể làm theo cách đó không?
kiwi tức giận

1
@AnkitKhettry Âm thanh như được tải lên bằng một biểu mẫu hoặc bằng cách sử dụng API biểu mẫu. Những "chuỗi lạ" mà bạn đề cập đến là các ranh giới biểu mẫu thường được sử dụng để phân tách dữ liệu biểu mẫu thành các phần trên máy chủ.
Héo

9

Tôi có Mã Java mẫu này:

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;

public class TestClass {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(8081);
        Socket accept = socket.accept();
        InputStream inputStream = accept.getInputStream();

        InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
        char readChar;
        while ((readChar = (char) inputStreamReader.read()) != -1) {
            System.out.print(readChar);
        }

        inputStream.close();
        accept.close();
        System.exit(1);
    }
}

và tôi có tệp test.html này:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File Upload!</title>
</head>
<body>
<form method="post" action="http://localhost:8081" enctype="multipart/form-data">
    <input type="file" name="file" id="file">
    <input type="submit">
</form>
</body>
</html>

và cuối cùng là tệp tôi sẽ sử dụng cho mục đích thử nghiệm, có tên a.dat có nội dung như sau:

0x39 0x69 0x65

nếu bạn diễn giải các byte ở trên dưới dạng các ký tự ASCII hoặc UTF-8, chúng thực sự sẽ đại diện cho:

9ie

Vì vậy, hãy chạy Mã Java của chúng tôi, mở test.html trong trình duyệt yêu thích của chúng tôi, tải lên a.datvà gửi biểu mẫu và xem những gì máy chủ của chúng tôi nhận được:

POST / HTTP/1.1
Host: localhost:8081
Connection: keep-alive
Content-Length: 196
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Origin: null
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.97 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary06f6g54NVbSieT6y
DNT: 1
Accept-Encoding: gzip, deflate
Accept-Language: en,en-US;q=0.8,tr;q=0.6
Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF

------WebKitFormBoundary06f6g54NVbSieT6y
Content-Disposition: form-data; name="file"; filename="a.dat"
Content-Type: application/octet-stream

9ie
------WebKitFormBoundary06f6g54NVbSieT6y--

Chà, tôi không ngạc nhiên khi thấy các ký tự 9ie vì chúng tôi đã bảo Java in chúng coi chúng là các ký tự UTF-8. Bạn cũng có thể chọn đọc chúng dưới dạng byte thô ..

Cookie: JSESSIONID=27D0A0637A0449CF65B3CB20F40048AF 

thực sự là tiêu đề HTTP cuối cùng ở đây. Sau đó là Phần thân HTTP, nơi có thể nhìn thấy meta và nội dung của tệp chúng tôi đã tải lên.


6

Một thông điệp HTTP có thể có một phần dữ liệu được gửi sau các dòng tiêu đề. Trong phản hồi, đây là nơi tài nguyên được yêu cầu được trả về máy khách (việc sử dụng phổ biến nhất của nội dung thư) hoặc có lẽ là văn bản giải thích nếu có lỗi. Trong một yêu cầu, đây là nơi dữ liệu do người dùng nhập hoặc các tệp đã tải lên được gửi đến máy chủ.

http://www.tutorialspoint.com/http/http_messages.htmlm

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.