流
在 HTTP 模塊章節中的靜態文件服務器中,我們已經看過了兩個可寫流的例子,即服務器可以向 response 對象中寫入數據和向 request 返回的請求對象中寫入數據。
可寫流是在 node 接口中廣泛使用的概念,所有可寫流都有一個 write 方法,用來傳遞字符串或者 buffer 對象; 還有一個 end 方法用於關閉流,如果給定一個參數,end 會在流關閉前輸出指定的一段數據。這兩個方法都可以接受一個回調函數作爲參數。
但是在我們搭建的靜態文件服務器中有一個問題,就是文件夾中的文件我們是直接用 readfire 方法讀取的,無論文件的大小,對於體積小一點的文件來說還好說,但是如果是文件是一個幾個 G 大小的電影的話,佔用的內存空間就需要幾個 G 的大小,而且在這個文件完全寫入網絡另一端之前這些內存是無法釋放掉的,這對我們的電腦來說是無法承受的
事實上,對於文件來說我們也沒有必要非得一次性的將文件傳輸完成,我們完全可以讀一點寫一點,一點一點地完成文件的傳輸。當然也不能是讀一點馬上就寫一點,傳輸速度可能不同,比如文件讀取的快、寫入的慢,電腦完全可以讀出了一定量的數據在內存裏用來寫入後去運行其他任務,這就需要有一個緩衝區來提高運行的效率。
流的目的:
- 可以控制內存佔用(控制內存佔用不要超過一個水位線)
- 可以將大的文件分解爲小的片段,再一點一點的傳輸,以減輕內存的壓力
- 協調不同階段處理速度之間的差異
流的分類:
- 可讀流
- 可寫流
- 雙工流
- 轉換流
例如:我們想傳輸一個電影
在以往的方式,可能我們會這麼寫:
const fs = require('fs')
var file = 'movie.mp4'
fs.readFile(file, (err, data) => {
if (err) {
console.log(err)
} else {
fs.writeFile('movie-1', data , () => {
console.log('done')
})
}
})
寫法十分簡單,就是讀取然後寫入,這個過程是一次性完成的,也就意味着如果電影的大小是1 G 的話,內存的佔用也至少爲一個 G
如果用流的方式寫呢:
const fs = require('fs')
var file = 'D:/Users/movie.mp4'
var rs = fs.createReadStream(file)
var ws = fs.createWriteStream('D:/Users/movie-1.mp4', {highWaterMark: 65536})
//這裏的 highWaterMark 可以指定緩衝內存的大小,默認大小是65536個字節,即 64k
rs.on('data', data => {
if(ws.write(data) === false) { //這裏的write函數會返回一個布爾值,false 表示緩衝內存已滿,不可繼續寫入了
rs.pause() //如果內存已滿,rs 暫停
}
})
ws.on('drain', () => { // ws 內存耗盡
rs.resume(
) // rs 回覆執行
})
rs.on('end', () => {
ws.end()
})
如果讀取的速度快於寫入的速度緩衝內存滿了之後,rs 就會在中途暫停讀取,等緩衝內存裏的數據寫入完了之後再繼續讀取,這就是一個簡單的流
注意,雖然 write 函數返回 false 時就告訴內存不可寫入了,但是如果依舊寫入的話,內存還是會接收數據的,而不會扔掉,當然這會導致內存佔用過高
現在有更簡單,更常用的書寫方式,就是 pipe
const fs = require('fs')
var file = 'D:/Users/movie.mp4'
var rs = fs.c reateReadStream(file)
var ws = fs.createWriteStream('D:/Users/movie-1.mp4', {highWaterMark: 65536})
rs.pipe(ws)
// pipe 以一種管道式的方式書寫,並依次運行,如果有有的話完全可以寫成:
// rs.pipe(gzip).pipe(ws).pipe(conn)
當然了,pipe 的內部實現是要比上面的代碼複雜的多的,但是主要想做的事情就是剛纔我們做的,當每個環節流速不同時,通過調節流速來控制內存佔用
練習
用一個可讀流讀取某一路徑的文件
const { Readable } = require('stream')
const fs = require('fs')
exports.createReadStream = function createReadStream(path) {
var fd = fs.openSync(path,'r') //打開 path 路徑文件
var fileStat = fs.statSync(path)
var fileSize = fileStat.size
var position = 0
return new Readable({
read(size) {
var buf = Buffer.alloc(1024) // buf 上有1024個字節的空間可用
if (position >= fileSize) {
this.push(null)
fs.close(fd, (err) => {
console.log(err)
})
} else {
fs.read(fd, buf, 0, 1024, position, (err, bytesRead) => {
// 從 fd 文件的 position 位置,讀取1024個字節放在buf的第0位
if (err) {
console.log(err)
}
if (bytesRead < 1024) {
this.push(buf.slice(0, bytesRead))
} else {
this.push(buf)
}
})
position += 1024
}
}
})
}
在 node 中運行代碼
$ node
> cfrs = require('./file-read-stream.js')
> rs = cfrs('./http-server.js')
> rs.on('data', d => console.log(d.toString()))
這樣就能讀取出上節 HTTP 模塊案例的代碼了,如果我們在添加一個 write 的函數,就可以實現一個複製文件的功能了
exports.createWriteStream = function createWriteStream(path) {
var fd = fs.openSync(path, 'a+')
var position = 0
return new Writable({
write(chunk, encoding, done) {
fs.write(fd, chunk, 0, chunk.length, position, () => {
done()
})
position += chunk.length
}
})
}
$ node
> const {createReadStream, createWriteStream} = require('./file-read-stream.js')
> createReadStream('./http-server.js').pipe(createWriteStream('./http-server222.js'))
運行這段代碼,就會有一個 http-server 的複製文件 http-server222 出現在文件夾中
回到昨天的靜態文件服務器,我們需要將 readFile 改成流的方式
const http = require('http')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')
const mime = require('mime')
const port = 8090
const baseDir = __dirname
const server = http.createServer(async (req, res) => {
var targetPath = decodeURIComponent(path.join(baseDir, req.url))
console.log(req.method, req.url, baseDir, targetPath)
try {
var stat = await fsp.stat(targetPath)
if (stat.isFile()) {
try {
var type = mime.getType(targetPath)
if (type) {// 如果文件類型在 mimeMap 對象中,就使用相應的解碼方式
res.writeHead(200, {'Content-Type': `${type}; charset=UTF-8`})
} else { //如果不在,就以流的方式解碼
res.writeHead(200, {'Content-Type': `application/octet-stream`})
}
fs.createReadStream(targetPath).pipe(res)
} catch(e) {
res.writeHead(502)
res.end('502 Internal Server Error')
}
} else if (stat.isDirectory()) {
var indexPath = path.join(targetPath, 'index.html')
try {
await fsp.stat(indexPath)
var type = mime.getType(indexPath)
if (type) {
res.writeHead(200, {'Content-Type': `${type}; charset=UTF-8`})
} else {
res.writeHead(200, {'Content-Type': `application/octet-stream`})
}
fs.createReadStream(indexPath).pipe(res)
} catch(e) {
if (!req.url.endsWith('/')) {
res.writeHead(301, {
'Location': req.url + '/'
})
res.end()
return
}
var entries = await fsp.readdir(targetPath, {withFileTypes: true})
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8'
})
res.end(`
${
entries.map(entry => {
var slash = entry.isDirectory() ? '/' : ''
return `
<div>
<a href='${entry.name}${slash}'>${entry.name}${slash}</a>
</div>
`
}).join('')
}
`)
}
}
} catch(e) {
res.writeHead(404)
res.end('404 Not Found')
}
})
server.listen(port, () => {
console.log(port)
})