當上傳檔案時,客戶端對伺服器說了些什麼?

,發表於2014-12-05

在一次寫檔案上傳程式時,突發奇想,想了解一下在通過Form表單上傳檔案過程中,客戶端給伺服器端傳送的資料格式是什麼樣的。 在大概瞭解了檔案上傳協議相關內容之後,寫了一個非常傻瓜的伺服器程式——它只會把客戶端傳送過來的訊息內容原封不動地列印出來。下面是Rebol程式碼:

Rebol [
    Title: "Parse file uploads data via Form"
]       

server: open tcp://:8080

server/awake: func [ event /local port ][
    if event/type = 'accept [
        port: first event/port
        port/awake: func [ event /local data ][
            switch event/type [
                read [
                    print [ to string! event/port/data ]

                    clear event/port/data
                    write event/port to-binary {Some data ^/}
                ]
                wrote [
                    read event/port
                ]
                close [
                    close event/port
                    return true
                ]
            ]
            false
        ]
        read port
    ]
    false
]

wait [ server 20 ] ;20秒後超時
close server

可以看到,這個伺服器程式開啟了8080埠,直接使用TCP協議讀取資料。

客戶端使用curl來模擬:

curl --form upload1=@D:/a.txt --form upload2=@D:/b.txt --form press=OK http://localhost:8080

這條命令相當於:

<form action="http://127.0.0.1:8080/" method="post" enctype="multipart/form-data">
    <input type="file" name="upload1" /><br />
    <input type="file" name="upload2" /><br />
    <input type="text" name="press" value="OK">
    <input type="submit" />
</form>

輸出結果如下:

POST / HTTP/1.1
User-Agent: curl/7.39.0
Host: localhost:8080
Accept: */*
Content-Length: 450
Expect: 100-continue
Content-Type: multipart/form-data; boundary=------------------------9de42a043d77defe


--------------------------9de42a043d77defe
Content-Disposition: form-data; name="upload"; filename="a.txt"
Content-Type: text/plain

Hello world!
--------------------------9de42a043d77defe
Content-Disposition: form-data; name="upload2"; filename="b.txt"
Content-Type: text/plain

Hello world, too!
--------------------------9de42a043d77defe
Content-Disposition: form-data; name="press"

OK
--------------------------9de42a043d77defe--

仔細觀察會發現,curl在傳送資料的時候是先傳送請求頭,然後傳送上傳訊息流(包括其中的請求頭和檔案的內容),並且中間有一定的時滯。原因在於curl採用的是HTTP1.1協議。在HTTP1.1中,為了提高效率,設定了一個Expect: 100-continue 訊息頭。它的作用是,在客戶端要傳送大量的資料之前,可以先對伺服器進行詢問,是否接受這個請求。如果伺服器願意接受,則回應一個HTTP/1.1 100 Continue狀態,否則回應HTTP/1.1 417 Expectation Failed。另外,對於不能識別Expect: 100-continue訊息頭的舊伺服器或者像上面展示的很傻很天真的伺服器,客戶端不應該無限等待伺服器的回應(詳見rfc2616規範)。因此,客戶端在等待我們的很傻很天真的伺服器一段時間後,還是默默地把上傳檔案資料的請求傳送了出來。;-) 當然了,我們可以使我們的伺服器“聰明”一些。那就是對Expect: 100-continue的請求做出反應:

...
read [
    print [ to string! event/port/data ]
    data: event/port/data
    if find data to binary! {Expect: 100-continue} [
        write event/port to-binary {HTTP/1.1 100 Continue ^M^/^M^/}
    ]

    clear event/port/data
    write event/port to-binary {Some data ^/}
]
...

瞬間客戶端的響應速度就快了很多!:-D

接下來做點什麼呢?(ˇˍˇ)......當然是將上傳的檔案儲存起來啦!:-P上傳的資料流都已經拿到了,接下來只要解析資料流就可以了。程式碼如下:

Rebol [
    Title: "Parse file uploads data via Form"
]       

server: open tcp://:8080

boundary: ""
segment: to-binary reduce [ crlf crlf ]
flag: to-binary {Content-Type: multipart/form-data; boundary=}
flag2: to-binary {filename="}

server/awake: func [ event /local port ][
    if event/type = 'accept [
        port: first event/port
        port/awake: func [ event /local data ][
            switch event/type [
                read [
                    print [ to-string event/port/data ]
                    data: event/port/data

                     if find data to-binary "Expect: 100-continue" [
                        write event/port to-binary {HTTP/1.1 100 Continue ^M^/^M^/}
                    ]

                    either find data flag [
                        ;print "================Header data"
                        parse data [
                            ;找到資料的“分割邊界”
                            thru flag copy boundary to segment (print to-string boundary)
                        ]
                    ][
                        ;print "================file data"
                        parse data [
                            any [
                                thru boundary thru flag2 copy file-name to {"} ;獲取原檔名
                                thru segment ;越過上傳訊息流的請求頭
                                copy file-data to boundary ( write to-file file-name file-data)    ;將檔案資料拷貝到file-data中並寫入檔案
                            ]
                        ]
                    ]

                    clear event/port/data
                    write event/port to-binary {Some data ^/}
                ]
                wrote [
                    read event/port
                ]
                close [
                    print "Close"
                    close event/port
                    return true
                ]
            ]
            false
        ]
        read port
    ]
    false
]

wait [ server 20 ]
close server

當然可以上傳各種格式的檔案了:

curl --form upload=@D:/a.txt --form upload=@D:/a.jpg --form press=OK http://localhost:8080

如何執行我們的伺服器?

  1. 下載Rebol3.0的直譯器;
  2. 將指令碼另存為server.reb檔案;
  3. 執行檔案:do %server.reb

參考資料:

  1. http://www.faqs.org/rfcs/rfc1867.html
  2. http://www.rebol.net/wiki/Port_Examples
  3. http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
  4. http://blog.zhaojie.me/2011/03/html-form-file-uploading-programming.html
  5. http://www.snooda.com/read/322

相關文章