我真沒想到這篇文章竟然寫了將近一個月,一方面我在寫這篇文章的時候陽了,所以將近有兩週沒幹活,另外一方面,我發現在寫基於Node的HTTP的demo的時候,我不會Node,所以我又要一邊學學Node,一邊百度,一邊看HTTP,最後百度的東西百分之九十不能用,所以某些點就卡的我特別難受。
比如最後的分段傳輸的例子,我以為是瀏覽器會解析分段資料,誰知道是拼接在body裡的。
其次,我還覺得是否這樣去詳細的逐字的寫例子是不是有點本末倒置,本來是講HTTP的,結果全是一些例子。但是我又覺得不這麼寫,你就知道點概念,沒有弄清楚具體某些欄位的互動和使用,跟沒學好像也沒多大區別。
我還是拿分段傳輸來舉例子,我不寫出來,你知道它是在body裡的麼?
所以,後續,反正我想咋寫就咋寫吧,不去糾結這些,啦啦啦啦~
以下是正文。
話說上一篇文章真的有些無聊,全是理論,一點意思都沒有,我寫的都要睡著了。不過這一篇我希望你可以跟我一起來玩一玩,並且這一篇文章所實現的一些例子還是有一定的實踐價值的。比如斷點續傳?比如不聽話的伺服器。
我們就按照上一篇理論篇的順序,來實現我們的具體的例子。
我們先來回顧一下之前寫過的一個最簡單的例子,html和js服務程式碼如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>content-type</title> </head> <body> 可以了 </body> </html>
然後是server.js:
const http = require("http"); const fs = require("fs"); const path = require("path"); const hostname = "127.0.0.1"; const port = 9000; const server = http.createServer((req, res) => { let sourceCode = fs.readFileSync( path.resolve(__dirname, "./index.html"), "utf8" ); res.end(sourceCode); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
我們的程式碼很簡單,就不解釋了哈,我們直接來看請求的結果:
這是我們開啟我們在hosts檔案中修改的域名,以及在node服務中設定監聽的埠號後,發出的請求及其報文內容,要強調的一點是,我們目前在程式碼層面沒有新增任何頭欄位的內容,無論是使用者端還是伺服器。
我相信這張圖你一定可以看懂至少四個欄位。我們發現其實瀏覽器和伺服器預設給我們進行了一些頭欄位的設定,比如請求頭中的Accept、Accept-Encoding和Accept-Language,響應頭中的Content-Length等等。這些預設設定其實是固定的,或者說是根據系統環境固定了一些預設設定,當然,這個我是猜的,因為它跟HTTP標準就沒啥關係了,這是瀏覽器或者Node的實現層面的事情了,我們就不過多的涉獵了。
然後,我們稍微修改一下媒體的型別,我在當前的程式碼下增加了一個media資料夾,裡面放了幾個型別的檔案,然後我們什麼都不用幹,直接修改路徑地址就好,試一試返回是什麼樣的。大家可以在當前的場景下自行嘗試。其中文字型別的檔案,都可以直接顯示在頁面上,但是媒體型別的就不行了,比如圖片,僅用當前的程式碼,瀏覽器是無法正確的解析的。這部分的程式碼我放在了content-type-01目錄下。
我們繼續噢,上面的簡單的小例子僅僅是使用了瀏覽器和Node伺服器的一些預設能力,現在我們嘗試在頁面中手動發起一個ajax請求,來獲取伺服器的返回,並在此基礎上,加以額外的嘗試。
server.js的程式碼是這樣的:
const http = require("http"); const fs = require("fs"); const path = require("path"); const { URL } = require("url"); const hostname = "127.0.0.1"; const port = 9000; const server = http.createServer((req, res) => { const parsedUrl = new URL(req.url, "http://www.zaking.com"); // 瀏覽器icon,瀏覽器會預設請求,如果是這個的話,直接返回個200好了。 // 或者你可以自己嘗試返回一個icon,啊哈哈 if (parsedUrl.pathname == "/favicon.ico") { res.writeHead(200); res.end(); return; } // 返回靜態html檔案 if (parsedUrl.pathname == "/home") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "./index.html"), "utf8" ); res.end(sourceCode); } // 返回靜態json資源 if (parsedUrl.pathname == "/api") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "../media/web.json"), "utf8" ); res.end(sourceCode); } }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
我們來看這段程式碼,和之前的例子稍稍有些區別,在這個例子裡,我並沒有分別建立靜態html和被請求介面的獨立的伺服器,而是把靜態html和被請求介面放在了同一個埠和服務下,為啥要這樣做呢?因為我不想解決跨域問題。
另外,其實這樣的寫法和實現在伺服器實踐中很常見,比如,你可以看看你現在自己的手中正在開發的專案,外網存取地址是https://www.example.com,而介面地址則是https://www.example.com/api/yourpath這樣。那麼基本上就是基於這樣的思路實現的,只不過或許是不同的語言,比如JAVA,或許用了某一個類庫,比如express。
好啦,我們解釋下上面的程式碼,很簡單,我覺得你大致肯定是可以看懂的。我們新增了一個url模組,這個模組從名字就知道是用來做url解析的。然後呢,我們通過解析request也就是請求的url來獲取到一些資料。
然後呢,如果請求的icon,那就直接返回個200就好了,這個不重要,就是稍微處理下。其實你不寫也是可以的。
再然後,如果請求的是/home這個path路徑,則會去讀取靜態的html檔案,如果是/api這個路徑,則會讀取一個靜態的json檔案並返回。當然,這個路徑的判斷你可以隨便寫~
那麼,我們來稍稍修改下html的程式碼,我希望可以點選一下按鈕,請求我們提供的介面的這個/api路徑。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>content-type</title> </head> <body> <button id="btn">點我試試</button> </body> <script> const btnDom = document.getElementById("btn"); function requestFn() { const xhr = new XMLHttpRequest(); const url = "http://www.zaking.com:9000/api"; xhr.open("GET", url); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { console.log(xhr); console.log(xhr.responseText); } }; xhr.send(); } btnDom.addEventListener("click", requestFn); </script> </html>
其實就是之前的例子,沒有區別,然後我們可以啟動服務node youfilepath,點選按鈕,你就可以看到請求結果了。一點問題沒有~。大家稍微注意觀察下頭欄位的變化,瞭解下就行了。
到目前為止我們講清楚了怎麼用Node搭建簡單的測試環境,都還沒怎麼涉及到HTTP的內容,別急,馬上就來了。
這一篇啊,我們就不傳JSON、HTML、TXT啥的這種檔案了,咱們來玩點複雜的,看看圖片和視訊、Excel要怎麼玩。
在實踐中,我們差不多有那麼幾種獲取和使用圖片的方式,嗯……大概可以分為兩種吧,一種是後端提供一個遠端的伺服器的圖片的地址,我們通過img標籤直接存取就好了,另外一種就是像請求介面那樣,獲取圖片的body,然後通過Blob或者其它類似手段生成原生的地址來存取。我們先來看看簡單的,存取一個遠端圖片地址的情況。
我們現在index.html中加上點這樣的程式碼:
<br /> <img src="http://www.zaking.com:9000/img" alt="" style="width: 100px; height: 100px" />
然後,伺服器的程式碼是這樣的:
if (parsedUrl.pathname == "/img") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "../media/puppy.jpeg") ); console.log(sourceCode, "sourceCode"); res.end(sourceCode); }
重新啟動服務後,你會發現,請求成功了:
你會發現,其實我們也沒做什麼複雜的事情,就是讀取後返回,去掉了讀取檔案時的utf8編碼,當然,如果你友善一點,可以加一點程式碼:
res.setHeader("Content-Type", "image/jpeg");
友好的告訴使用者端,我傳給你個圖片哦,你看著辦哦。
到這裡,我還有個問題,大家在工作中,遇沒遇到這種,比如圖片的地址是https://www.baidu.com/aaa.jpg,和我們這個例子中有什麼區別呢?其實本質來說都是一樣的,只不過,https://www.baidu.com/aaa.jpg這種,實際上存取的是伺服器上的靜態資源,沒有經過伺服器的程式碼處理,直接存取就好了。
而我們的例子,實際上你請求的是伺服器的介面,你需要通過伺服器讀取圖片後再返回給你,這是兩者細微的區別噢。下面我們就看看如何返回個圖片流(其實就是二進位制資料啦),然後通過前端程式碼解析成一個本地地址。我們先來看後端程式碼咋寫的:
if (parsedUrl.pathname == "/stream-img") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "../media/puppy.jpeg") ); const streamData = Buffer.from(sourceCode); // res.setHeader("Content-Type", "application/octet-stream"); res.end(streamData); }
我們看這段程式碼,只多了兩行,一行是通過Buffer.from方法把獲取到的圖片檔案轉換成二進位制,然後,註釋的部分,實際上是告訴瀏覽器你要按照二進位制來解析,不然的話,其實瀏覽器還是會按照圖片來解析,你拿到的就是圖片。當然,這麼說其實不太「準確」,因為無論是什麼形式,什麼資料型別,本質上來說,它都是一個「圖片」,只不過這個「圖片」的資料型別是什麼可能會有所區別,所以,哪怕你傳輸的是二進位制,但是你要是不告訴瀏覽器它的資料型別的話,還是會按照圖片來解析,也就是,返回的body看起來是這樣的:
當我們把響應頭中的Content-Type設定好,返回的body則會像下面這樣:
是不是很熟悉的亂碼,然後,我們就可以通過前端JS程式碼,來解析這段二進位制的資料了:
// html <button id="streamImgBtn">點我顯示流圖片</button> // js const streamImgBtnDom = document.getElementById("streamImgBtn"); streamImgBtnDom.addEventListener("click", requestStreamImgFn); function requestStreamImgFn() { const xhr = new XMLHttpRequest(); const url = "http://www.zaking.com:9000/stream-img"; xhr.responseType = "arraybuffer"; xhr.open("GET", url); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { const result = xhr.response; const blobData = new Blob([result]); const blobSrc = URL.createObjectURL(blobData); const img = document.createElement("img"); img.src = blobSrc; document.body.appendChild(img); } }; xhr.send(); }
整個程式碼並不複雜,點選一下按鈕就可以出現預料中的結果。但是尤其要注意加粗的那一塊程式碼,雖然你的伺服器返回和瀏覽器解析都是按照二進位制來的,但是xhr物件並不知道,否則會按照文字來處理,所以需要設定一下responseType。
好啦,關於圖片的部分,我們暫時告一段落咯。接下來我們簡單看看Excel檔案,其實本質上來說都是一樣的。不同的就是Content-Type的型別。我們稍微試一下,儘量少花點篇幅,把重頭戲留給視訊那部分。
伺服器端的程式碼是這樣的:
if (parsedUrl.pathname == "/excel") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "../media/test.xlsx") ); res.setHeader( "Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ); res.end(sourceCode); } if (parsedUrl.pathname == "/stream-excel") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "../media/test.xlsx") ); const streamData = Buffer.from(sourceCode); res.setHeader( "Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ); res.end(streamData); }
就其實很簡單,唯一注意的就是返回的Content-Type的型別,其他的跟圖片其實一模一樣。然後使用者端請求的程式碼也是一樣的,我就不貼了,當然,這裡沒法在瀏覽器檢視Excel,需要額外的外掛支援,這裡就不多說了,畢竟這不是重點。
簡單的傳輸方式其實對於視訊來說也是可以的,我在範例程式碼中也寫了這一部分,不再在這裡無意義的重複了。我們先來看看分塊傳輸是怎麼玩的。
廢話不多說,咱們直接上程式碼,哦對這是伺服器的程式碼:
if (parsedUrl.pathname == "/video-chunked") { let sourceCode = fs.readFileSync( path.resolve(__dirname, "../media/maomao.mp4") ); const bufSource = Buffer.from(sourceCode); res.setHeader("Content-Type", "video/mp4"); res.setHeader("Transfer-Encoding", "chunked"); const chunkSize = 1024; const chunks = []; for (let i = 0; i < bufSource.length; i += chunkSize) { chunks.push(Uint8Array.prototype.slice.call(bufSource, i, i + chunkSize)); } console.log(chunks, "chunks"); for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; res.write(chunk); } res.end(); }
我們來看這段程式碼,資訊量有點大,而且有點有趣(當然我並不知道為啥會這麼有趣,但是就是有趣)。
首先我要強調的一點是,Transfer-Encoding: chunked的設定不是預設開啟的,你要手動,而且還要匹配你的資料塊,否則就會發生有趣的事情。
然後,我們看程式碼,首先我們按照每一個塊是1024位元組來拆分,最後有多少塊我不管,我們來回圈整個chunks陣列,通過response.write寫到響應體裡,最後結束這次實驗。我們無法直接操作原始檔並slice,所以我們需要先把原始檔轉換成Buffer,再去通過Uint8Array原型上的slice方法來拆分。
OK,程式碼我們簡單的解釋完了,我們可以在index.html中新增一點程式碼:
<body> <video controls width="250"> <source src="http://www.zaking.com:9000/video" type="video/mp4" /> </video> <video controls width="250"> <source src="http://www.zaking.com:9000/video-chunked" type="video/mp4" /> </video> </body>
第二個就是我們新的地址。然後,我們啟動服務,開啟頁面:
注意看我們紅框的地方,當我們用Transfer-Encoding: chunked的時候前後兩個視訊載入的細微對比,並且,你可以點選開始按鈕,你會發現它的載入速度是不一樣的,第一個視訊,基本上一下子就滿了,而第二個則是一點一點一點一點的載入。
那這樣就算是chunked成功了麼?我們來看下:
理論上講,這樣確實是成功了,並且我們還從側面進一步驗證,但是,我不想從側面,我想正面驗證一下不行麼?好吧,滿足你的小小願望。但是為了滿足你的這個願望,我們需要額外的工具,也就是WireShark,或者你會使用其他的抓包工具也可以,我們現在在這裡, 就使用WireShark來抓包看下哦。
首先,進入介面後點選下面紅框的loopback:
就是迴環的意思,大概是說你的本地電腦即作為伺服器又作為使用者端,自己玩,就點這個就行了,然後進去後你會發現咔咔咔咔一頓跳各種請求,嗯,是你電腦裡各種軟體的請求資訊,那咋整呢?
在過濾欄裡輸入這樣的過濾條件,你會發現世界都安靜了,好舒服~然後呢,我們重新整理下剛剛的頁面,哦抱歉,你還不能這樣做,不過你可以先這樣試下。
好吧~接下來我們再寫一個小服務吧,檔名叫做video/client.js:
const http = require("http"); const options = { hostname: "www.zaking.com", port: 9000, path: "/video-chunked", method: "GET", }; const req = http.request(options, (res) => { // console.log(res, "res"); console.log(`STATUS: ${res.statusCode}`); console.log(`HEADERS: ${JSON.stringify(res.headers)}`); res.on("data", (chunk) => { console.log(`BODY: ${chunk}`); }); res.on("end", () => { console.log("No more data in response."); }); }); req.on("error", (e) => { console.error(`problem with request: ${e.message}`); }); req.end();
很簡單,這個例子咱們之前也用過,稍微的改造了下,我們在命令列工具中啟動一下即可:
node 06/video/client.js
然後,我們切回WireShark,內容很多,我們不管他都是啥,我們找到這個帶路徑的HTTP資訊:
然後點選一下,再把卷軸往後面拽,使勁拽,拽到底:
然後我們就可以看到這條,你發現這倆是一對,咋發現的呢?通過箭頭髮現的,一去一回~,然後我們點選它,可以看到它的詳細資訊:
好大啊,我看個毛?別急,把Hypertext Transfer Protocol開啟:
再開啟HTTP chunked response:
看到這,我們是不是就可以完全確定我們設定的chunked生效了?沒毛病吧~完美~~~但是呢~還沒完,我們再開啟其中一個塊:
注意哦,你現在可以手動自己去開啟每一個塊,你會發現,每一個塊都有這樣的編碼:
並且它在第一個塊就有一個這玩意,然後最後一個塊是這樣的:
好吧,恭喜你,發現了Transfer-encoding: chunked的核心內容,這裡稍微涉及點理論知識,下面我們根據我們的實際操作,來補全一下這部分理論。
分塊傳輸也是採用明文的方式,主要分為兩部分,長度頭和資料塊,長度頭呢是以CRLF(回車換行,即\r\n)結尾的一行明文,用16進位制數位表示塊的長度,資料塊緊跟在長度頭後,最後也用 CRLF 結尾,但資料不包含 CRLF;最後用一個長度為 0 的塊表示結束,即「0\r\n\r\n」。
誒?是不是跟我們剛才看到的對上了,那個400是16進位制的長度,我算算,400的16進位制轉成10進位制是不是1024:
好像,有點完美啊~~環環相扣,絲毫不漏。哈哈哈哈~
然後,我們可以再來個圖示:
沒問題吧,嗯……分塊傳輸就基本上完事了,大家可以試試這些實際的例子。
哦對了,我還忘了一個我在開始的時候說的有趣的事情,就是如果你把chunkSize設定的很大,比如1024*1024,抓包的時候會是什麼樣呢?你可以自己試下。你會發現它並沒有按照chunked形式傳遞。至於為啥,我猜是因為你的塊分的太大,實現的部分就不再視為chunked了,當然,這個是我猜的,我也不知道為啥。
哦對,我還在程式碼裡附上了wireshark的快照,用wireshark開啟就可以回溯上面例子了。
我們稍微回到用html來請求分塊傳輸的視訊的那個例子,假設你在跟著我玩這個遊戲,不知道你在那個例子的時候是否拖拽了一下進度條?那你是否發現怎麼拖好像都沒效果~,沒實現肯定沒效果。
再有,不知道你是否細心的看到了這個東東:
你看到,實際上在使用chunked的時候,請求頭中已經加了Range欄位,並且預設是獲取所有從0開始到最後,下面,我們就來看看如何實現這個範圍請求。
很簡單,我們來直接看程式碼咯,首先是發起請求的html按鈕,跟之前一樣:
<body> <button id="simpleRangeBtn">發起這個視訊的簡單的範圍請求</button> </body> <script> const simpleRangeBtn = document.getElementById("simpleRangeBtn"); simpleRangeBtn.addEventListener("click", simpleRangeRequestFn); function simpleRangeRequestFn() { const xhr = new XMLHttpRequest(); const url = "http://www.zaking.com:9000/simple-range"; xhr.open("GET", url); xhr.setRequestHeader("Range", "bytes=0-2048"); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { console.log(xhr); console.log(xhr.responseText); const result = xhr.responseText; console.log(result.name); } }; xhr.send(); } </script>
唯一的區別是我加了Range的請求頭,請求從0到2048位元組的視訊資料。然後,伺服器端是這樣的:
if (parsedUrl.pathname == "/simple-range") { const range = req.headers["range"]; console.log(range); res.setHeader("Accept-Ranges", "bytes"); res.end("zaking"); }
誒?你這寫的不對吧?你這怎麼就返回個字串?嗯……我強調過不止一遍,使用者端和伺服器使用HTTP通訊的作用是協商,協商的結果是伺服器給的,伺服器不一定會按照你使用者端期望的那樣返回給你預期的結果,所以,其實伺服器是不那麼聽話的。但是,HTTP是一份協定,協定的目的就是在約定的範圍內,你最好聽話,不然我玩什麼?好吧,上面僅僅是個小例子,為了進一步說明啥是協商。
其實接下來的事情就很簡單了,獲取視訊資料然後再擷取請求的範圍的長度即可,下面我們就按照協定的要求來完善這個簡單的例子,讓伺服器返回我們期望的範圍的視訊資料。
OK,我們先來看完整的伺服器端的程式碼:
if (parsedUrl.pathname == "/simple-range") { let videoSource = fs.readFileSync( path.resolve(__dirname, "../media/maomao.mp4") ); // 轉換 const bufSource = Buffer.from(videoSource); // 獲取長度 const bufSourceLen = bufSource.length; // 獲取請求的Range頭的長度範圍 const range = req.headers["range"]; const rangeVal = range.split("=")[1].split("-"); // 獲取開始和結束的長度 const start = parseInt(rangeVal[0], 10); const end = rangeVal[1] ? parseInt(rangeVal[1], 10) : start + bufSourceLen; console.log(start, end, bufSourceLen); // 判斷是否超出請求資源的最大長度,就返回416 if (start > bufSourceLen || end > bufSourceLen) { res.writeHead(416, { "Content-Range": `bytes */${bufSourceLen}` }); res.end(); } else { // 否則返回206即可 res.writeHead(206, { "Content-Range": `bytes ${start}-${end}/${bufSourceLen}`, "Accept-Ranges": "bytes", "Content-type": "video/mp4", }); res.write(Uint8Array.prototype.slice.call(bufSource, start, end)); res.end(); } }
這是目前最複雜的程式碼了,我們稍微來捋一下,首先,我們獲取伺服器上的原始檔,然後把它轉換成blob並且獲取到blob的長度,因為我們要校驗使用者端給你的Range範圍是否合法,這很重要。我們會按照HTTP的Range頭的格式來分割一下字串,獲取資料範圍的開始和結束資料,再然後,我們根據資料的長度判斷請求範圍是否合法。如果不合法,那就返回個416,結束。如果合法,那麼我們使用Uint8Array原型鏈上的方法去切分一下我們的資料並返回給使用者端即可。
然後,我們看下使用者端的程式碼:
// html <button id="simpleRangeBtn">發起這個視訊的簡單的範圍請求</button> // js const simpleRangeBtn = document.getElementById("simpleRangeBtn"); simpleRangeBtn.addEventListener("click", simpleRangeRequestFn); function simpleRangeRequestFn() { const xhr = new XMLHttpRequest(); const url = "http://www.zaking.com:9000/simple-range"; xhr.open("GET", url); xhr.responseType = "blob"; xhr.setRequestHeader("Range", "bytes=0-2048"); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { console.log(xhr); const result = xhr.response; // 我們需要把這段二進位制資料轉換成視訊 const blobData = new Blob([result]); const blobSrc = URL.createObjectURL(blobData); const video = document.createElement("video"); video.controls = true; video.width = "250"; video.src = blobSrc; document.body.appendChild(video); } }; xhr.send(); }
差不多這樣,這整體的程式碼沒啥好說的,我尤其要說一下的上面加粗的兩部分,嗯……稍後說,我們來看看效果。
誒?看起來好像不太對,請求沒問題,範圍也沒問題,OK的,但是為啥視訊沒播放呢?你猜猜呢?答案就在我加粗的兩行程式碼裡,首先,後端伺服器傳回的是blob檔案,前端的XMLHttpRequest物件也要設定responseType為blob,這個很重要。然後,最最重要的來了,你的視訊,注意,是視訊,所請求的視訊的範圍不能太小,你可以看到Content-Range的整個檔案的大小是195萬2139,所以你這給個零頭還不到的範圍,不行,我們把範圍調大一點,就100w吧,然後我們再看效果。
非常完美,但是我要強調兩個細節。首先,我們請求的是範圍,差不多是一半左右的視訊吧,所以當開始後,後面的資料就沒有了,視訊也就暫停了。其次,我們發現,其實這樣的前後端互動設計,就可以實現原生的進度條拖拽了。不信你可以在返回資料的範圍內拖拽一下進度條試試?
那麼簡單的範圍請求我們就搞定了~,其實也是我們最核心的部分。
上一個例子,我們完成了範圍請求並且確切的獲取到了一段視訊資料並渲染了,但是後面的部分沒渲染啊。這咋整?我們可以利用video物件的一些能力,來繼續後續的請求。我糾結了一下,例子我寫好了,在這裡,大家自己自行下載到本地玩一玩吧,因為沒有什麼新的HTTP的內容,其實更多是偏向於檔案編碼的處理的一些技術細節,所以就不再在這裡浪費篇幅了,這篇實踐文章比我預料的要長太多了。
當然,這個例子寫的只是個例子。翻譯過來就是僅供參考。
我們繼續把後續的一個知識點再實踐一下。
關於在一個HTTP請求中請求多段資料,其實並不十分複雜,它有兩個核心,一個是特殊的媒體型別multipart/byterange,另外就是分割多段資料的分隔符。我們不多廢話,直接來看下程式碼的實現。
// 因為我懶所以沒有去獲取請求頭拼接字串,也沒做一些判斷,就這樣吧。 if (parsedUrl.pathname === "/multipart-range") { const str = "1234567890"; const boundary = "split_bound"; const len = str.length; const data = [ { headers: { "Content-Range": `bytes 0-3/${len}`, "Content-Type": "text/plain", }, body: str.slice(0, 3), }, { headers: { "Content-Range": `bytes 4-6/${len}`, "Content-Type": "text/plain", }, body: str.slice(4, 6), }, ]; let body = data .map((item) => { let part = `\n--${boundary}\n`; for (const [key, value] of Object.entries(item.headers)) { part += `${key}: ${value}\n`; } part += "\n"; part += item.body; return part; }) .join(""); body += `\n--${boundary}--\n`; res.writeHead(206, { "Accept-Ranges": "bytes", "Content-type": `multipart/byteranges; boundary=${boundary}`, "Content-Length": Buffer.byteLength(body), }); res.write(body); res.end(); }
這塊程式碼有點長,我們需要來分析一下。嗯……稍後再分析,我們先看下測試的結果,哦對了,使用者端請求是這樣的:
// html <button id="multipleRangeBtn">點發我發起多段資料請求</button> // js const multipleRangeBtn = document.getElementById("multipleRangeBtn"); multipleRangeBtn.addEventListener("click", multipleRangeBtnRequestFn); function multipleRangeBtnRequestFn() { const xhr = new XMLHttpRequest(); xhr.open("GET", "http://www.zaking.com:9000/multipart-range"); xhr.setRequestHeader("Range", `bytes=0-3, 4-6`); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { // 因為我懶所以只console了一下 console.log(xhr); } }; xhr.send(); }
我們看下結果:
這裡有點小瑕疵,我們不管他,我懶得再切字串了。你發現一個問題沒有,分段傳輸實際上傳輸的是整個body,我們操作的是body的資料,是由前後端手動去分辨你分了哪些段,資訊都在body的資料裡,而不是通過伺服器或者瀏覽器幫你去解析分段資料返回給你。為什麼會這樣呢?
想象一下,瀏覽器怎麼知道這些「段」是整體資料的哪一部分?它沒法幫你做啊,所以那就都交給你們自己解決,自己商議了,那我們看這個資料結構。是HTTP協定要求這樣去做的。我們看這段資料就可以理解,首先,每一段資料的開始都要有一個「--」加上伺服器告訴你的分隔符是啥,在響應頭裡告訴你了,然後一塊資料就類似一個小的http段,頭部和body用\n分割,前端收到這段資料要自己通過邏輯程式碼去處理,最後,通過一個--加上分隔符--作為整體資料的結束。
那既然是body資料,我的理解,你可以隨意設定前端需要的,或者前後端約定的分段資料內的可能的、允許的、預設的資料形式和結構,也就是說,你不一定非要返回Content-Range和Content-Type,你還可以返回其他的,甚至不返回。
嗯……看起來就是這個樣子:
這就是分段資料在body中的結構,注意,我一再強調,這是約定的結構,你完全可以不按照這樣來。只要前後端商議好,並且不會造成未知的副作用。
那麼說了這麼多,我們回頭看下程式碼吧,其實程式碼很簡單,就是寫死了一塊資料,然後形成了一個陣列,最後遍歷這個資料拼接上協定約定的分隔符就完事了。當然,這裡我偷懶了,沒有去讀取請求頭中的資料作為依據,而是寫死的,額……這不是重點,我就偷點懶。
首先,本篇文章有兩件事沒有事無鉅細的去做,一個是我在文章開頭提到的斷點續傳,這個東西我覺得你學完了,學會了本篇的所有例子,你一定有思路去實現斷點續傳,一點都不復雜,我覺得我再寫的話這篇文章就太長了,本來就長的出乎我的預估,所以留作課後作業吧。
其次,還有一個沒實現的例子就是基於Stream的分塊傳輸,這個其實本質沒有區別,大家有興趣也可以自己去找一找資料,因為它其實更偏向於Node,和HTTP沒有太大關係了。
最後,我們稍微回顧一下本篇文章都做了啥。我們剛開始的時候用json、img、xlsx作為例子,看看前後端的互動處理是怎樣的,很簡單。
然後,我們著重學習了以視訊資料為例子的分塊傳輸和範圍請求。在文章的最後,我們用一個簡單的例子,來實現了分段傳輸。
我要強調的是,大家在學習這篇文章的時候,一定要結合例子,能清楚的分辨哪些是前後端程式碼要做的事情,哪些是我設定了頭欄位使用者端會處理的情況。
最後,終於結束了~
本文來自部落格園,作者:Zaking,轉載請註明原文連結:https://www.cnblogs.com/zaking/p/16966545.html