在工作中碰到了這樣的需求,需要用nodejs 來上傳文件,之前也只是知道怎么通過瀏覽器來上傳文件, 用nodejs的話, 相當於模擬瀏覽器的行為。 google 了一番之后, 明白了瀏覽器無非就是利用http協議來給服務器傳輸數據, 具體協議就是《RFC 1867 - Form-based File Upload in HTML》, 在瀏覽器上通過form 表單來上傳文件就是通過這個協議,我們可以先看看瀏覽器給服務端發送了什么數據, 就可以依葫蘆畫瓢的把上傳功能實現出來。說起form 表單上傳文件的話, 大家應該很熟悉:
<form action="http://www.qq.com/" method="post"> <input type="text" name="text1" /><br /> <input type="text" name="text2" /><br /> <input type="submit" /> </form>
提交時, 用fiddler 抓包可以看到向服務端發出這樣的數據:
POST http://www.qq.com/ HTTP/1.1 Host: www.qq.com Content-Length: 23 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 text1=hello&text2=world
值得注意的是Content-Type默認為application/x-www-form-urlencoded,所以消息會經過URL編碼。比如“你好”會編碼為 %E4%BD%A0%E5%A5%BD。
接下來我們看一下通過form 表單是怎么上傳的。大家應該也不陌生:
<form action="http://www.qq.com" method="post" enctype="multipart/form-data"> <input type="file" name="myfile" /> <input type="submit" value="submit" /> </form>
然后新建一個只有hello world字樣的upload.txt文本文件上傳上去,我們再吃用fiddler 來抓下包, 可以發現發送過去的數據稍微復雜了一些(已經去掉了很多的其它沒關系的請求行,比如緩存控制和cookie之類的):
POST http://www.qq.com/ HTTP/1.1 Host: www.qq.com Content-Length: 199 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywr3X7sXBYQQ4ZF5G ------WebKitFormBoundarywr3X7sXBYQQ4ZF5G Content-Disposition: form-data; name="myfile"; filename="upload.txt" Content-Type: text/plain hello world ------WebKitFormBoundarywr3X7sXBYQQ4ZF5G--
根據RFC 1867的定義,我們需要生成一段邊界數據,這個數據不能在內容的其它地方出現,這個可以自己定義, 在每個瀏覽器的生成算法可能都不一樣, 上面的boundary就是分隔數據,生成了分隔數據之后, 就可以把分隔數據放在頭部的Content-Type里面傳送給服務端, 也就是上文的 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywr3X7sXBYQQ4ZF5G, 另外,上傳的內容,需要用分隔數據來分隔成若干個段,然后每段數據里面都有文件的文件名,還有上傳時候的name,服務端就是用這個name來接收文件,還有文件的類型Content-Type,在這個例子里是 text/plain,如果上傳的是png圖片就是image/png。文件類型的一個空行后就是所上傳的文件的內容,在這個例子里也是為了容易理解所以上傳的是文本文件所以內容直接就能夠顯示出來,如果上傳的是圖片文件, 因為是二進制文件,fiddler 就顯示的是亂碼。 文件的內容結束之后就是一個空行再加上邊界數據。
了解了發送格式的細節之后, 下一步就是使用nodejs來編程實現,簡單來講, 就是按照格式把數據發送給服務端就行了。
const http = require('http'); const fs = require('fs'); //post地址為本地服務的一個php,用於測試上傳是否成功 var options = { hostname: 'localhost', port: 80, path: '/get.php', method: 'POST' } //生成分隔數據 var boundaryKey = '----WebKitFormBoundaryjLVkbqXtIi0YGpaB'; //讀取需要上傳的文件內容 fs.readFile('./upload.txt', function (err, data) { //拼裝分隔數據段 var payload = '--' + boundaryKey + '\r\n' + 'Content-Disposition:form-data; name="myfile"; filename="upload.txt"\r\n' + 'Content-Type:text/plain\r\n\r\n'; payload += data; payload += '\r\n--' + boundaryKey + '--'; //發送請求 var req = http.request(options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('body:' + chunk); }); }); req.on('error', function(e) { console.error("error:"+e); }); //把boundary、要發送的數據大小以及數據本身寫進請求 req.setHeader('Content-Type', 'multipart/form-data; boundary='+boundaryKey+''); req.setHeader('Content-Length', Buffer.byteLength(payload, 'utf8')); req.write(payload); req.end(); });
本文重點在於了解協議並且用代碼實現出來, 代碼組織上面還有很多優化的地方。
最后在本地apache,簡單寫一個php來保存上傳的文件來用作測試:
<?php $filePath = './upload.txt'; move_uploaded_file($_FILES['myfile']['tmp_name'] , $filePath); echo "ok"; ?>
另外,根據RFC 1867 還可以實現一次上傳多個文件的功能, 這個在這里就不詳述, 需要的話可以詳細參考RFC 1867來實現。