日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

一文了解文件上傳全過(guò)程(1.8w字深度解析,進(jìn)階必備)

 西北望msm66g9f 2020-05-06

前言

平常在寫業(yè)務(wù)的時(shí)候常常會(huì)用的到的是 GET, POST請(qǐng)求去請(qǐng)求接口,GET 相關(guān)的接口會(huì)比較容易基本不會(huì)出錯(cuò),而對(duì)于 POST中常用的 表單提交,JSON提交也比較容易,但是對(duì)于文件上傳呢?大家可能對(duì)這個(gè)步驟會(huì)比較害怕,因?yàn)榭赡艽蠹覍?duì)它并不是怎么熟悉,而瀏覽器Network對(duì)它也沒(méi)有詳細(xì)的進(jìn)行記錄,因此它成為了我們心中的一根刺,我們老是無(wú)法確定,關(guān)于文件上傳到底是我寫的有問(wèn)題呢?還是后端有問(wèn)題,當(dāng)然,我們一般都比較謙虛, 總是會(huì)在自己身上找原因,可是往往實(shí)事呢?可能就出在后端身上,可能是他接受寫的有問(wèn)題,導(dǎo)致你換了各種請(qǐng)求庫(kù)去嘗試,axios,request,fetch 等等。那么我們?nèi)绾伪苊膺@種情況呢?我們自身要對(duì)這一塊夠熟悉,才能不以猜的方式去寫代碼。如果你覺(jué)得我以上說(shuō)的你有同感,那么你閱讀完這篇文章你將收獲自信,你將不會(huì)質(zhì)疑自己,不會(huì)以猜的方式去寫代碼。

本文比較長(zhǎng)可能需要花點(diǎn)時(shí)間去看,需要有耐心,我采用自頂向下的方式,所有示例會(huì)先展現(xiàn)出你熟悉的方式,再一層層往下, 先從請(qǐng)求端是怎么發(fā)送文件的,再到接收端是怎么解析文件的。

前置知識(shí)

什么是 multipart/form-data?

multipart/form-data 最初由 《RFC 1867: Form-based File Upload in HTML》[1]文檔提出。

Since file-upload is a feature that will benefit many applications, this proposes an extension to HTML to allow information providers to express file upload requests uniformly, and a MIME compatible representation for file upload responses.

由于文件上傳功能將使許多應(yīng)用程序受益,因此建議對(duì)HTML進(jìn)行擴(kuò)展,以允許信息提供者統(tǒng)一表達(dá)文件上傳請(qǐng)求,并提供文件上傳響應(yīng)的MIME兼容表示。

總結(jié)就是原先的規(guī)范不滿足啦,我要擴(kuò)充規(guī)范了。

文件上傳為什么要用 multipart/form-data?

The encoding type application/x-www-form-urlencoded is inefficient for sending large quantities of binary data or text containing non-ASCII characters.  Thus, a new media type,multipart/form-data, is proposed as a way of efficiently sending the values associated with a filled-out form from client to server.

1867文檔中也寫了為什么要新增一個(gè)類型,而不使用舊有的application/x-www-form-urlencoded:因?yàn)榇祟愋筒贿m合用于傳輸大型二進(jìn)制數(shù)據(jù)或者包含非ASCII字符的數(shù)據(jù)。平常我們使用這個(gè)類型都是把表單數(shù)據(jù)使用url編碼后傳送給后端,二進(jìn)制文件當(dāng)然沒(méi)辦法一起編碼進(jìn)去了。所以multipart/form-data就誕生了,專門用于有效的傳輸文件。

也許你有疑問(wèn)?那可以用 application/json嗎?

其實(shí)我認(rèn)為,無(wú)論你用什么都可以傳,只不過(guò)會(huì)要綜合考慮一些因素的話,multipart/form-data更好。例如我們知道了文件是以二進(jìn)制的形式存在,application/json 是以文本形式進(jìn)行傳輸,那么某種意義上我們確實(shí)可以將文件轉(zhuǎn)成例如文本形式的 Base64 形式。但是呢,你轉(zhuǎn)成這樣的形式,后端也需要按照你這樣傳輸?shù)男问剑鎏厥獾慕馕?。并且文本在傳輸過(guò)程中是相比二進(jìn)制效率低的,那么對(duì)于我們動(dòng)輒幾十M幾百M(fèi)的文件來(lái)說(shuō)是速度是更慢的。

以上為什么文件傳輸要用multipart/form-data 我還可以舉個(gè)例子,例如你在中國(guó),你想要去美洲,我們的multipart/form-data相當(dāng)于是選擇飛機(jī),而application/json相當(dāng)于高鐵,但是呢?中國(guó)和美洲之間沒(méi)有高鐵啊,你執(zhí)意要坐高鐵去,你可以花昂貴的代價(jià)(后端額外解析你的文本)造高鐵去美洲,但是你有更加廉價(jià)的方式坐飛機(jī)(使用multipart/form-data)去美洲(去傳輸文件)。你圖啥?(如果你有錢有時(shí)間,抱歉,打擾了,老子給你道歉)

multipart/form-data規(guī)范是什么?

摘自 《RFC 1867: Form-based File Upload in HTML》[2] 6.Example












Content-type: multipart/form-data, boundary=AaB03x
--AaB03xcontent-disposition: form-data; name='field1'Joe Blow--AaB03xcontent-disposition: form-data; name='pics'; filename='file1.txt'Content-Type: text/plain
... contents of file1.txt ...--AaB03x--

可以簡(jiǎn)單解釋一些,首先是請(qǐng)求類型,然后是一個(gè) boundary (分割符),這個(gè)東西是干啥的呢?其實(shí)看名字就知道,分隔符,當(dāng)時(shí)分割作用,因?yàn)榭赡苡卸辔募嘧侄?,每個(gè)字段文件之間,我們無(wú)法準(zhǔn)確地去判斷這個(gè)文件哪里到哪里為截止?fàn)顟B(tài)。因此需要有分隔符來(lái)進(jìn)行劃分。然后再接下來(lái)就是聲明內(nèi)容的描述是 form-data 類型,字段名字是啥,如果是文件的話,得知道文件名是啥,還有這個(gè)文件的類型是啥,這個(gè)也很好理解,我上傳一個(gè)文件,我總得告訴后端,我傳的是個(gè)啥,是圖片?還是一個(gè)txt文本?這些信息肯定得告訴人家,別人才好去進(jìn)行判斷,后面我們也會(huì)講到如果這些沒(méi)有聲明的時(shí)候,會(huì)發(fā)生什么?

好了講完了這些前置知識(shí),我們接下來(lái)要進(jìn)入我們的主題了。面對(duì)File, formData,Blob,Base64,ArrayBuffer,到底怎么做?還有文件上傳不僅僅是前端的事。服務(wù)端也可以文件上傳(例如我們利用某云,把靜態(tài)資源上傳到 OSS 對(duì)象存儲(chǔ))。服務(wù)端和客戶端也有各種類型,Buffer,Stream,Base64....頭禿,怎么搞?不急,就是因?yàn)樯蟼魑募粏螁问乔岸说氖?,所以我將以下上傳文件的一方稱為請(qǐng)求端,接受文件一方稱為接收方。我會(huì)以請(qǐng)求端各種上傳方式,接收端是怎么解析我們的文件以及我們最終的殺手锏調(diào)試工具-wireshark來(lái)進(jìn)行講解。以下是講解的大綱,我們先從瀏覽器端上傳文件,再到服務(wù)端上傳文件,然后我們?cè)賮?lái)解析文件是如何被解析的。


請(qǐng)求端

瀏覽端

File

首先我們先寫下最簡(jiǎn)單的一個(gè)表單提交方式。





<form action='http://localhost:7787/files' method='POST'>	<input name='file' type='file' id='file'>	<input type='submit' value='提交'></form>

我們選擇文件后上傳,發(fā)現(xiàn)后端返回了文件不存在。


不用著急,熟悉的同學(xué)可能立馬知道是啥原因了。噓,知道了也聽(tīng)我慢慢叨叨。

我們打開(kāi)控制臺(tái),由于表單提交會(huì)進(jìn)行網(wǎng)頁(yè)跳轉(zhuǎn),因此我們勾選preserve log 來(lái)進(jìn)行日志追蹤。



我們可以發(fā)現(xiàn)其實(shí) FormDatafile 字段顯示的是文件名,并沒(méi)有將真正的內(nèi)容進(jìn)行傳輸。再看請(qǐng)求頭。


發(fā)現(xiàn)是請(qǐng)求頭和預(yù)期不符,也印證了 application/x-www-form-urlencoded 無(wú)法進(jìn)行文件上傳。

我們加上請(qǐng)求頭,再次請(qǐng)求。





<form action='http://localhost:7787/files' enctype='multipart/form-data' method='POST'> <input name='file' type='file' id='file'> <input type='submit' value='提交'></form>

發(fā)現(xiàn)文件上傳成功,簡(jiǎn)單的表單上傳就是像以上一樣簡(jiǎn)單。但是你得熟記文件上傳的格式以及類型。

FormData

formData 的方式我隨便寫了以下幾種方式。




























<input type='file' id='file'><button id='submit'>上傳</button><script src='https://cdn./axios/0.19.2/axios.min.js'></script><script>submit.onclick = () => {    const file = document.getElementById('file').files[0];    var form = new FormData();    form.append('file', file);    	// type 1    axios.post('http://localhost:7787/files', form).then(res => {        console.log(res.data);    })  	// type 2  	fetch('http://localhost:7787/files', {        method: 'POST',        body: form    }).then(res => res.json()).tehn(res => {console.log(res)});  	// type3  	var xhr = new XMLHttpRequest();    xhr.open('POST', 'http://localhost:7787/files', true);    xhr.onload = function () {    	console.log(xhr.responseText);    };    xhr.send(form);}</script>

以上幾種方式都是可以的。但是呢,請(qǐng)求庫(kù)這么多,我隨便在 npm 上一搜就有幾百個(gè)請(qǐng)求相關(guān)的庫(kù)。


因此,掌握請(qǐng)求庫(kù)的寫法并不是我們的目標(biāo),目標(biāo)只有一個(gè)還是掌握文件上傳的請(qǐng)求頭和請(qǐng)求內(nèi)容。


Blob

Blob 對(duì)象表示一個(gè)不可變、原始數(shù)據(jù)的類文件對(duì)象。Blob 表示的不一定是JavaScript原生格式的數(shù)據(jù)。`File`[3] 接口基于Blob,繼承了 blob 的功能并將其擴(kuò)展使其支持用戶系統(tǒng)上的文件。

因此如果我們遇到 Blob 方式的文件上方式不用害怕,可以用以下兩種方式:

1.直接使用 blob 上傳







const json = { hello: 'world' };const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const form = new FormData();form.append('file', blob, '1.json');axios.post('http://localhost:7787/files', form);

2.使用 File 對(duì)象,再進(jìn)行一次包裝(File 兼容性可能會(huì)差一些  https:///#search=File)







const json = { hello: 'world' };const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });    const file = new File([blob], '1.json');form.append('file', file);axios.post('http://localhost:7787/files', form)

ArrayBuffer

ArrayBuffer 對(duì)象用來(lái)表示通用的、固定長(zhǎng)度的原始二進(jìn)制數(shù)據(jù)緩沖區(qū)。

雖然它用的比較少,但是他是最貼近文件流的方式了。

在瀏覽器中,他每個(gè)字節(jié)以十進(jìn)制的方式存在。我提前準(zhǔn)備了一張圖片。







const bufferArrary = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,1,3,0,0,0,37,219,86,202,0,0,0,6,80,76,84,69,0,0,255,128,128,128,76,108,191,213,0,0,0,9,112,72,89,115,0,0,14,196,0,0,14,196,1,149,43,14,27,0,0,0,10,73,68,65,84,8,153,99,96,0,0,0,2,0,1,244,113,100,166,0,0,0,0,73,69,78,68,174,66,96,130];const array = Uint8Array.from(bufferArrary);const blob = new Blob([array], {type: 'image/png'});const form = new FormData();form.append('file', blob, '1.png');axios.post('http://localhost:7787/files', form)

這里需要注意的是 new Blob([typedArray.buffer], {type: 'xxx'}),第一個(gè)參數(shù)是由一個(gè)數(shù)組包裹。里面是 typedArray 類型的 buffer。

Base64












const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEUAAP+AgIBMbL/VAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==';const byteCharacters = atob(base64);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {	byteNumbers[i] = byteCharacters.charCodeAt(i);}const array = Uint8Array.from(byteNumbers);const blob = new Blob([array], {type: 'image/png'});const form = new FormData();form.append('file', blob, '1.png');axios.post('http://localhost:7787/files', form);

關(guān)于 base64 的轉(zhuǎn)化和原理可以看這兩篇 base64 原理[4]

原來(lái)瀏覽器原生支持JS Base64編碼解碼[5]

小結(jié)

對(duì)于瀏覽器端的文件上傳,可以歸結(jié)出一個(gè)套路,所有東西核心思路就是構(gòu)造出 File 對(duì)象。然后觀察請(qǐng)求 Content-Type,再看請(qǐng)求體是否有信息缺失。而以上這些二進(jìn)制數(shù)據(jù)類型的轉(zhuǎn)化可以看以下表。


圖片來(lái)源 (https:///post/binary-in-frontend/#%E6%95%B0%E6%8D%AE%E8%BE%93%E5%85%A5[6])

服務(wù)端

講完了瀏覽器端,現(xiàn)在我們來(lái)講服務(wù)器端,和瀏覽器不同的是,服務(wù)端上傳有兩個(gè)難點(diǎn)。

1.瀏覽器沒(méi)有原生 formData,也不會(huì)想瀏覽器一樣幫我們轉(zhuǎn)成二進(jìn)制形式。

2.服務(wù)端沒(méi)有可視化的 Network 調(diào)試器。

Buffer

Request

首先我們通過(guò)最簡(jiǎn)單的示例來(lái)進(jìn)行演示,然后一步一步深入。相信文檔可以查看 https://github.com/request/request#multipartform-data-multipart-form-uploads














// request-error.jsconst fs = require('fs');const path = require('path');const request = require('request');const stream = fs.readFileSync(path.join(__dirname, '../1.png'));request.post({ url: 'http://localhost:7787/files', formData: { file: stream, }}, (err, res, body) => { console.log(body);})

發(fā)現(xiàn)報(bào)了一個(gè)錯(cuò)誤,正像上面所說(shuō),瀏覽器端報(bào)錯(cuò),可以用NetWork。那么服務(wù)端怎么辦?這個(gè)時(shí)候我們拿出我們的利器 -- wireshark

我們打開(kāi) wireshark (如果沒(méi)有或者不會(huì)的可以查看教程 https://blog.csdn.net/u013613428/article/details/53156957)

設(shè)置配置 tcp.port == 7787,這個(gè)是我們后端的端口。


運(yùn)行上述文件 node request-error.js


我們來(lái)找到我們發(fā)送的這條http的請(qǐng)求報(bào)文。中間那堆亂七八糟的就是我們的文件內(nèi)容。

















POST /files HTTP/1.1host: localhost:7787content-type: multipart/form-data; boundary=--------------------------437240798074408070374415content-length: 305Connection: close
----------------------------437240798074408070374415Content-Disposition: form-data; name='file'Content-Type: application/octet-stream
.PNG....IHDR.............%.V.....PLTE......Ll..... pHYs..........+.....IDAT..c`.......qd.....IEND.B`.----------------------------437240798074408070374415--

可以看到上述報(bào)文。發(fā)現(xiàn)我們的內(nèi)容請(qǐng)求頭 Content-Type: application/octet-stream有錯(cuò)誤,我們上傳的是圖片請(qǐng)求頭應(yīng)該是image/png,并且也少了 filename='1.png'

我們來(lái)思考一下,我們剛才用的是fs.readFileSync(path.join(__dirname, '../1.png')) 這個(gè)函數(shù)返回的是 Buffer,Buffer是什么樣的呢?就是下面的形式,不會(huì)包含任何文件相關(guān)的信息,只有二進(jìn)制流。


<Buffer 01 02>

所以我想到的是,需要指定文件名以及文件格式,幸好 request 也給我們提供了這個(gè)選項(xiàng)。








key: {    value:  fs.createReadStream('/dev/urandom'),    options: {      filename: 'topsecret.jpg',      contentType: 'image/jpeg'    }}

可以指定options,因此正確的代碼應(yīng)該如下(省略不重要的代碼)













...request.post({ url: 'http://localhost:7787/files', formData: { file: { value: stream, options: { filename: '1.png' } }, }});

我們通過(guò)抓包可以進(jìn)行分析到,文件上傳的要點(diǎn)還是規(guī)范,大部分的問(wèn)題,都可以通過(guò)規(guī)范模板來(lái)進(jìn)行排查,是否構(gòu)造出了規(guī)范的樣子。

Form-data

我們?cè)偕钊胍恍瑏?lái)看看 request 的源碼, 他是怎么實(shí)現(xiàn)Node端的數(shù)據(jù)傳輸?shù)摹?/p>

打開(kāi)源碼我們很容易地就可以找到關(guān)于 formData 這塊相關(guān)的內(nèi)容 https://github.com/request/request/blob/3.0/request.js#L21


就是利用form-data,我們先來(lái)看看 formData 的方式。





















const path = require('path');const FormData = require('form-data');const fs = require('fs');const http = require('http');const form = new FormData();form.append('file', fs.readFileSync(path.join(__dirname, '../1.png')), {    filename: '1.png',    contentType: 'image/jpeg',});const request = http.request({    method: 'post',    host: 'localhost',    port: '7787',    path: '/files',    headers: form.getHeaders()});form.pipe(request);request.on('response', function(res) {    console.log(res.statusCode);});
原生 Node

看完formData,可能感覺(jué)這個(gè)封裝還是太高層了,于是我打算對(duì)照規(guī)范手動(dòng)來(lái)構(gòu)造multipart/form-data請(qǐng)求方式來(lái)進(jìn)行講解。我們?cè)賮?lái)回顧一下規(guī)范。












Content-type: multipart/form-data, boundary=AaB03x
--AaB03xcontent-disposition: form-data; name='field1'Joe Blow--AaB03xcontent-disposition: form-data; name='pics'; filename='file1.txt'Content-Type: text/plain
... contents of file1.txt ...--AaB03x--

我模擬上方,我用原生 Node 寫出了一個(gè)multipart/form-data 請(qǐng)求的方式。

主要分為4個(gè)部分
  • 構(gòu)造請(qǐng)求header

  • 構(gòu)造內(nèi)容header

  • 寫入內(nèi)容

  • 寫入結(jié)束分隔符






























const path = require('path');const fs = require('fs');const http = require('http');// 定義一個(gè)分隔符,要確保唯一性const boundaryKey = '-------------------------461591080941622511336662';const request = http.request({    method: 'post',    host: 'localhost',    port: '7787',    path: '/files',    headers: {        'Content-Type': 'multipart/form-data; boundary=' + boundaryKey, // 在請(qǐng)求頭上加上分隔符        'Connection': 'keep-alive'    }});// 寫入內(nèi)容頭部request.write(    `--${boundaryKey}\r\nContent-Disposition: form-data; name='file'; filename='1.png'\r\nContent-Type: image/jpeg\r\n\r\n`);// 寫入內(nèi)容const fileStream = fs.createReadStream(path.join(__dirname, '../1.png'));fileStream.pipe(request, { end: false });fileStream.on('end', function () {    // 寫入尾部    request.end('\r\n--' + boundaryKey + '--' + '\r\n');});request.on('response', function(res) {    console.log(res.statusCode);});

至此,已經(jīng)實(shí)現(xiàn)服務(wù)端上傳文件的方式。

Stream、Base64

由于這兩塊就是和Buffer的轉(zhuǎn)化,比較簡(jiǎn)單,我就不再重復(fù)描述了??梢宰鳛榱艚o大家的作業(yè),感興趣的可以給我這個(gè)示例代碼倉(cāng)庫(kù)貢獻(xiàn)這兩個(gè)示例。




// base64 to bufferconst b64string = /* whatever */;const buf = Buffer.from(b64string, 'base64');









// stream to bufferfunction streamToBuffer(stream) {    return new Promise((resolve, reject) => {    const buffers = [];    stream.on('error', reject);    stream.on('data', (data) => buffers.push(data))    stream.on('end', () => resolve(Buffer.concat(buffers))  });}

小結(jié)

由于服務(wù)端沒(méi)有像瀏覽器那樣 formData 的原生對(duì)象,因此服務(wù)端核心思路為構(gòu)造出文件上傳的格式(header,filename等),然后寫入 buffer 。然后千萬(wàn)別忘了用 wireshark進(jìn)行驗(yàn)證。

接收端

這一部分是針對(duì) Node 端進(jìn)行講解,對(duì)于那些 koa-body 等用慣了的同學(xué),可能一樣不太清楚整個(gè)過(guò)程發(fā)生了什么?可能唯一比較清楚的是 ctx.request.files ??? 如果ctx.request.files 不存在,就會(huì)懵逼了,可能也不太清楚它到底做了什么,文件流又是怎么解析的。

我還是要說(shuō)到規(guī)范...請(qǐng)求端是按照規(guī)范來(lái)構(gòu)造請(qǐng)求..那么我們接收端自然是按照規(guī)范來(lái)解析請(qǐng)求了。

Koa-body




const koaBody = require('koa-body');
app.use(koaBody({ multipart: true }));

我們來(lái)看看最常用的 koa-body,它的使用方式非常簡(jiǎn)單,短短幾行,就能讓我們享受到文件上傳的簡(jiǎn)單與快樂(lè)(其他源碼庫(kù)一樣的思路去尋找問(wèn)題的本源) 可以帶著一個(gè)問(wèn)題去閱讀,為什么用了它就能解析出文件?

尋求問(wèn)題的本源,我們當(dāng)然要打開(kāi) koa-body的源碼,koa-body 源碼很少只有211行,https://github.com/dlau/koa-body/blob/v4.1.1/index.js#L125 很容易地發(fā)現(xiàn)它其實(shí)是用了一個(gè)叫做formidable的庫(kù)來(lái)解析files 的。并且把解析好的files 對(duì)象賦值到了 ctx.req.files。(所以說(shuō)大家不要一味死記 ctx.request.files, 注意查看文檔,因?yàn)榻裉煊?koa-bodyctx.request.files 明天換個(gè)庫(kù)可能就是 ctx.request.body 了)

因此看完koa-body我們得出的結(jié)論是,koa-body的核心方法是formidable

Formidable

那么讓我們繼續(xù)深入,來(lái)看看formidable做了什么,我們首先來(lái)看它的目錄結(jié)構(gòu)。










.├── lib│   ├── file.js│   ├── incoming_form.js│   ├── index.js│   ├── json_parser.js│   ├── multipart_parser.js│   ├── octet_parser.js│   └── querystring_parser.js

看到這個(gè)目錄,我們大致可以梳理出這樣的關(guān)系。












index.js|incoming_form.js|type?|1.json_parser2.multipart_parser3.octet_parser4.querystring_parser

由于源碼分析比較枯燥。因此我只摘錄比較重要的片段。由于我們是分析文件上傳,所以我們只需要關(guān)心multipart_parser 這個(gè)文件。

https://github.com/node-formidable/formidable/blob/v1.2.1/lib/multipart_parser.js#L72











...MultipartParser.prototype.write = function(buffer) {	console.log(buffer);  var self = this,      i = 0,      len = buffer.length,      prevIndex = this.index,      index = this.index,      state = this.state,...

我們將它的 buffer 打印看看.






<Buffer 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 34 36 31 35 39 31 30 38 30 39 34 31 36 32 32 35 31 31 33 33 36 36 36 ... >144<Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 00 01 00 00 00 01 01 03 00 00 00 25 db 56 ca 00 00 00 06 50 4c 54 45 00 00 ff 80 80 80 4c 6c bf ... >106<Buffer 0d 0a 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 34 36 31 35 39 31 30 38 30 39 34 31 36 32 32 35 31 31 33 33 36 ... >

我們來(lái)看wireshark 抓到的包


我用紅色進(jìn)行了分割標(biāo)記,對(duì)應(yīng)的就是formidable所分割的片段 ,所以說(shuō)這個(gè)包主要是將大段的 buffer 進(jìn)行分割,然后循環(huán)處理。

這里我還可以補(bǔ)充一下,可能你對(duì)以上表非常陌生。左側(cè)是二進(jìn)制流,每1個(gè)代表1個(gè)字節(jié),1字節(jié)=8位,上面的 2d 其實(shí)就是16進(jìn)制的表示形式,用二進(jìn)制表示就是 0010 1101,右側(cè)是ascii 碼用來(lái)可視化,但是 assii 分可顯和非可顯示。有部分是無(wú)法可視的。比如你所看到文件中有需要小點(diǎn),就是不可見(jiàn)字符。

你可以對(duì)照,ascii表對(duì)照表[7]來(lái)看。

我來(lái)總結(jié)一下formidable對(duì)于文件的處理流程。


原生 Node

好了,我們已經(jīng)知道了文件處理的流程,那么我們自己來(lái)寫一個(gè)吧。



























































const fs = require('fs');const http = require('http');const querystring = require('querystring');const server = http.createServer((req, res) => {  if (req.url === '/files' && req.method.toLowerCase() === 'post') {    parseFile(req, res)  }})function parseFile(req, res) {  req.setEncoding('binary');  let body = '';  let fileName = '';  // 邊界字符  let boundary = req.headers['content-type']    .split('; ')[1]    .replace('boundary=', '')    req.on('data', function(chunk) {    body += chunk;  });  req.on('end', function() {    // 按照分解符切分    const list = body.split(boundary);    let contentType = '';    let fileName = '';    for (let i = 0; i < list.length; i++) {      if (list[i].includes('Content-Disposition')) {        const data = list[i].split('\r\n');        for (let j = 0; j < data.length; j++) {          // 從頭部拆分出名字和類型          if (data[j].includes('Content-Disposition')) {            const info = data[j].split(':')[1].split(';');            fileName = info[info.length - 1].split('=')[1].replace(/'/g, '');            console.log(fileName);          }          if (data[j].includes('Content-Type')) {            contentType = data[j];            console.log(data[j].split(':')[1]);          }        }      }    }    // 去除前面的請(qǐng)求頭    const start = body.toString().indexOf(contentType) + contentType.length + 4; // 有多\r\n\r\n    const startBinary = body.toString().substring(start);    const end = startBinary.indexOf('--' + boundary + '--') - 2; // 前面有多\r\n     // 去除后面的分隔符    const binary = startBinary.substring(0, end);    const bufferData = Buffer.from(binary, 'binary');    fs.writeFile(fileName, bufferData, function(err) {      res.end('sucess');    });    ;  })}
server.listen(7787)

總結(jié)

相信有了以上的介紹,你不再對(duì)文件上傳有所懼怕, 對(duì)文件上傳整個(gè)過(guò)程都會(huì)比較清晰了,還不懂。。。。找我。

再次回顧下我們的重點(diǎn):

請(qǐng)求端出問(wèn)題,瀏覽器端打開(kāi) network 查看格式是否正確(請(qǐng)求頭,請(qǐng)求體), 如果數(shù)據(jù)不夠詳細(xì),打開(kāi)wireshark,對(duì)照我們的規(guī)范標(biāo)準(zhǔn),看下格式(請(qǐng)求頭,請(qǐng)求體)。

接收端出問(wèn)題,情況一就是請(qǐng)求端缺少信息,參考上面請(qǐng)求端出問(wèn)題的情況,情況二請(qǐng)求體內(nèi)容錯(cuò)誤,如果說(shuō)請(qǐng)求體內(nèi)容是請(qǐng)求端自己構(gòu)造的,那么需要檢查請(qǐng)求體是否是正確的二進(jìn)制流(例如上面的blob構(gòu)造的時(shí)候,我一開(kāi)始少了一個(gè)[],導(dǎo)致內(nèi)容主體錯(cuò)誤)。

其實(shí)講這么多就兩個(gè)字: **規(guī)范**[8],所有的生態(tài)都是圍繞它而展開(kāi)的。更多請(qǐng)看我的博客[9]。

相關(guān)閱讀

Node  Napi 實(shí)現(xiàn) C++  擴(kuò)展 - LRU 淘汰算法開(kāi)發(fā)一個(gè)Node命令行小玩具全過(guò)程--高顏統(tǒng)計(jì)工具

開(kāi)發(fā)一個(gè)Node命令行小玩具全過(guò)程--高顏統(tǒng)計(jì)工具

關(guān)注

大家好我是秋風(fēng),是開(kāi)源項(xiàng)目 webchat[13](1528)、shark-cleaner[14](19)、google-translate-open-api[15] (46)等 的作者,如果你對(duì)我所總結(jié)或者是我寫的開(kāi)源項(xiàng)目感興趣,歡迎加我微信(下方二維碼)進(jìn)群一起探討。


參考

https:///post/5c9f4885f265da308868dad1

https://my.oschina.net/bing309/blog/3132260

https://segmentfault.com/a/1190000020654277

參考資料

[1]

《RFC 1867: Form-based File Upload in HTML》: https://www./rfc/rfc1867.txt

[2]

《RFC 1867: Form-based File Upload in HTML》: https://www./rfc/rfc1867.txt

[3]

File: https://developer.mozilla.org/zh-CN/docs/Web/API/File

[4]

base64 原理: https://blog.csdn.net/wo541075754/article/details/81734770

[5]

原來(lái)瀏覽器原生支持JS Base64編碼解碼: https://www./wordpress/2018/08/js-base64-atob-btoa-encode-decode/

[6]

https:///post/binary-in-frontend/#%E6%95%B0%E6%8D%AE%E8%BE%93%E5%85%A5: https:///post/binary-in-frontend/#數(shù)據(jù)輸入

[7]

ascii表對(duì)照表: http://ascii.911cha.com/

[8]

規(guī)范: https://www./rfc/rfc1867.txt

[9]

我的博客: https://qiufeng.blue/

[10]

shark-cleaner: 一個(gè)Node Cli 實(shí)現(xiàn)的垃圾清理工具(深層清理開(kāi)發(fā)垃圾): https:///post/5e78c0785188255e1a15b8a3

[11]

Node + NAPI 實(shí)現(xiàn) C++ 擴(kuò)展 - LRU 淘汰算法: https:///post/5e0171ab5188251221769c3c

[12]

開(kāi)發(fā)一個(gè)Node命令行小玩具全過(guò)程--高顏統(tǒng)計(jì)工具: https:///post/5b7296796fb9a0098165610d

[13]

webchat: https://github.com/hua1995116/webchat

[14]

shark-cleaner: https://github.com/hua1995116/shark-cleaner

[15]

google-translate-open-api: https://github.com/hua1995116/google-translate-open-api

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多