Comprensión profunda del flujo de archivos en el nodo

Por qué usar secuencias de archivos

Imagine tal escenario, quiero procesar un archivo 10G, pero el tamaño de mi memoria es solo 2G, ¿qué debo hacer?

Podemos leer el archivo 5 veces, y solo leer 2G de datos cada vez, para que este problema pueda resolverse, ¡entonces el proceso de lectura en segmentos es continuo!

En el nodo stream, el módulo encapsula las operaciones básicas de la secuencia, y la secuencia de archivos también depende directamente de este módulo. Aquí usamos la secuencia de archivos para comprender en profundidadstream

flujo de archivo legible

Lea el archivo y lea el contenido del archivo en la memoria poco a poco.

Cómo utilizar

Echemos un vistazo al uso básico primero.

const fs = require('fs')

const rs = fs.createReadStream('./w-test.js')

rs.on('data', (chunk) => {
  console.log(chunk)
})

rs.on('close', () => {
  console.log('close')
})

Como se muestra en el código anterior, hemos fs.createStream()creado una secuencia legible para leer el archivo w-test.js .

En ese on('data')momento , los datos del archivo se leerán automáticamente y el contenido de 64 kb se leerá de forma predeterminada cada vez, highWaterMarky el valor de umbral de cada flujo de contenido también se puede cambiar dinámicamente a través de parámetros.

closeEl evento se activa automáticamente cuando se lee el archivo .

El siguiente código es el parámetro que createReadStreamse puede configurar

const rs = fs.createReadStream('./w-test.js', {
  flags: 'r', // 文件系统表示,这里是指以可读的形式操作文件
  encoding: null, // 编码方式
  autoClose: false, // 读取完毕时是否自动触发 close 事件
  start: 0, // 开始读取的位置
  end: 2, // 结束读取的位置
  highWaterMark: 2 // 每次读取的内容大小
})

Nota: Se incluyen tanto el inicio como el final, es decir, [inicio, final].

De hecho, fs.crateReadStreamdevuelve una instancia de fs.ReadStreamla clase , por lo que el código anterior es equivalente a:

const rs = new fs.ReadStream('./w-test.js', {
  flags: 'r', // 文件系统表示,这里是指以可读的形式操作文件
  encoding: null, // 编码方式
  autoClose: false, // 读取完毕时是否自动触发 close 事件
  start: 0, // 开始读取的位置
  end: 2, // 结束读取的位置
  highWaterMark: 2 // 每次读取的内容大小
})

Implementación de transmisiones legibles por archivos

Después de comprender el uso, deberíamos intentar hacerlo en principio.Luego, escribimos una secuencia legible a mano.

fs.read / fs.abrir

La esencia de un flujo legible es leer los datos del archivo en lotes, y fs.read()el método puede controlar el tamaño del contenido del archivo leído.

fs.read(fd, buffer, offset, length, position, callback)
  • fd: el descriptor de archivo para leer

  • búfer: el búfer donde se escribirán los datos (escriba el contenido del archivo leído en este búfer)

  • offset: el desplazamiento para comenzar a escribir en el búfer (desde el índice del búfer para comenzar a escribir)

  • longitud: el número de bytes leídos (leer varios bytes del archivo)

  • posición: especifique la posición para comenzar a leer desde el archivo (comience a leer desde los primeros bytes del archivo)

  • devolución de llamada: función de devolución de llamada

    • errar

    • bytesRead: el número real de bytes leídos

Leer un archivo requiere un identificador de archivo, deberíamos usarlo fs.openpara obtener

  • ruta: ruta del archivo

  • banderas: banderas del sistema de archivos, por defecto: 'r'. Significa qué operación hacer en el archivo, las comunes son las siguientes:

    • r: abre el archivo para leer

    • w: abre el archivo para escribir

    • a: abra el archivo para anexar

  • modo: permiso de operación de archivo, valor predeterminado: 0o666 (legible y escribible).

  • devolución de llamada: función de devolución de llamada. Los parámetros llevados en la función son los siguientes:

    • err: si falla, el valor es el motivo del error

    • fd (número): descriptor de archivo, este valor se usa al leer y escribir archivos

inicialización

En primer lugar, ReadStreames una clase.Desde el punto de vista del rendimiento, esta clase puede escuchar eventos on('data'), por lo que debemos dejar que herede EventEmitterdel siguiente código:

class ReadStream extends EventEmitter {
  constructor() {
    super();
  }
}

Luego inicializamos los parámetros y abrimos el archivo, el siguiente código (el código clave se comentará en el código):

class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super()

    // 解析参数
    this.path = path
    this.flags = options.flags ?? 'r'
    this.encoding = options.encoding ?? 'utf8'
    this.autoClose = options.autoClose ?? true
    this.start = options.start ?? 0
    this.end = options.end ?? undefined
    this.highWaterMark = options.highWaterMark ?? 16 * 1024

    // 文件的偏移量
    this.offset = this.start

    // 是否处于流动状态,调用 pause 或 resume 方法时会用到,下文会讲到
    this.flowing = false

    // 打开文件
    this.open()

    // 当绑定新事件时会触发 newListener
    // 这里当绑定 data 事件时,自动触发文件的读取
    this.on('newListener', (type) => {
      if (type === 'data') {
        // 标记为开始流动
        this.flowing = true
        // 开始读取文件
        this.read()
      }
    })
  }
}
  • Antes de leer el archivo, tenemos que abrir el archivo, es decir this.open(), .

  • on('newListener')Es un evento de EventEmitter, que se activará cada vez que vinculamos un nuevo evento newListener, por ejemplo: cuando on('data')nosotros , newListenerel evento se activará y el tipo es 'datos'.

  • Aquí, cuando escuchamos datael enlace del evento (es decir on('data')), comenzamos a leer el archivo this.read(), es this.read()decir, nuestro método principal.

abierto

openMétodos de la siguiente manera:

open() {
  fs.open(this.path, this.flags, (err, fd) => {
    if (err) {
      // 文件打开失败触发 error 事件
      this.emit('error', err)
      return
    }

    // 记录文件标识符
    this.fd = fd
    // 文件打开成功后触发 open 事件
    this.emit('open')
  })
}

Registre el identificador del archivo cuando se abre el archivo, es decirthis.fd

leer

readMétodos de la siguiente manera:

read() {
  // 由于 ```fs.open``` 是异步操作,
  // 所以当调用 read 方法时,文件可能还没有打开
  // 所以我们要等 open 事件触发之后,再次调用 read 方法
  if (typeof this.fd !== 'number') {
    this.once('open', () => this.read())
    return
  }

  // 申请一个 highWaterMark 字节的 buffer,
  // 用来存储从文件读取的内容
  const buf = Buffer.alloc(this.highWaterMark)

  // 开始读取文件
  // 每次读取时,都记录下文件的偏移量
  fs.read(this.fd, buf, 0, buf.length, this.offset, (err, bytesRead) => {
    this.offset += bytesRead

    // bytesRead 为实际读取的文件字节数
    // 如果 bytesRead 为 0,则代表没有读取到内容,即读取完毕
    if (bytesRead) {
      // 每次读取都触发 data 事件
      this.emit('data', buf.slice(0, bytesRead))
      // 如果处于流动状态,则继续读取
      // 这里当调用 pause 方法时,会将 this.flowing 置为 false
      this.flowing && this.read()
    } else {
      // 读取完毕后触发 end 事件
      this.emit('end')

      // 如果可以自动关闭,则关闭文件并触发 close 事件
      this.autoClose && fs.close(this.fd, () => this.emit('close'))
    }
  })
}

Cada línea de código anterior tiene comentarios, creo que no es difícil de entender, aquí hay algunos puntos clave a los que prestar atención

  • Debe esperar a que se abra el archivo antes de poder comenzar a leer el archivo, pero la apertura del archivo es una operación asincrónica y no sabemos el tiempo específico de finalización de la apertura, por lo que activaremos un on('open')evento openactive la rellamada nuevamenteread()

  • fs.read()El método se ha mencionado anteriormente, puede echar un vistazo al método principal de fs escrito a mano

  • this.flowingLos atributos se utilizan para determinar si es fluido y se controlarán mediante el pasue()método y resume()Echemos un vistazo a estos dos métodos.

pausa

pause() {
  this.flowing =false
}

reanudar

resume() {
  if (!this.flowing) {
    this.flowing = true
    this.read()
  }
}

código completo

const { EventEmitter } = require('events')
const fs = require('fs')

class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super()

    this.path = path
    this.flags = options.flags ?? 'r'
    this.encoding = options.encoding ?? 'utf8'
    this.autoClose = options.autoClose ?? true
    this.start = options.start ?? 0
    this.end = options.end ?? undefined
    this.highWaterMark = options.highWaterMark ?? 16 * 1024
    this.offset = this.start
    this.flowing = false

    this.open()

    this.on('newListener', (type) => {
      if (type === 'data') {
        this.flowing = true
        this.read()
      }
    })
  }

  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error', err)
        return
      }

      this.fd = fd
      this.emit('open')
    })
  }

  pause() {
    this.flowing =false
  }

  resume() {
    if (!this.flowing) {
      this.flowing = true
      this.read()
    }
  }

  read() {
    if (typeof this.fd !== 'number') {
      this.once('open', () => this.read())
      return
    }

    const buf = Buffer.alloc(this.highWaterMark)
    fs.read(this.fd, buf, 0, buf.length, this.offset, (err, bytesRead) => {
      this.offset += bytesRead
      if (bytesRead) {
        this.emit('data', buf.slice(0, bytesRead))
        this.flowing && this.read()
      } else {
        this.emit('end')
        this.autoClose && fs.close(this.fd, () => this.emit('close'))
      }
    })
  }
}

secuencia de archivos grabable

Como sugiere el nombre, escriba el contenido en el archivo poco a poco.

fs.escribir

  • fd: el descriptor del archivo que se va a escribir

  • búfer: escribe el contenido del búfer especificado en el archivo

  • offset: especifique la posición de escritura del búfer (escriba el contenido leído desde el índice de compensación del búfer en el archivo)

  • longitud: especifica el número de bytes a escribir

  • posición: el desplazamiento del archivo (escribir desde el byte de posición del archivo)

Cómo utilizar

// 使用方式 1:
const ws = fs.createWriteStream('./w-test.js')

// 使用方式 2:
const ws = new WriteStream('./w-test.js', {
  flags: 'w',
  encoding: 'utf8',
  autoClose: true,
  highWaterMark: 2
})

// 写入文件
const flag = ws.write('2')

ws.on('drain', () => console.log('drain'))
  • ws.write()escribir en el archivo. Aquí hay un valor de retorno, que representa si se ha alcanzado el caché máximo. Cuando llamamos varias write()veces sincrónicamente , el archivo no se escribe inmediatamente para cada llamada, pero solo se puede realizar una operación de escritura al mismo tiempo, por lo que el resto se escribirá en el caché hasta que se complete la última escritura. Luego, búsquelos y ejecútelos. secuencialmente desde el caché. Por lo tanto, habrá un tamaño máximo de caché en este momento, que es de 64 kb por defecto. El valor devuelto aquí representa si puede continuar escribiendo, es decir, si se ha alcanzado el caché máximo. verdadero significa que la escritura puede continuar.

  • ws.on('drain'), si la llamada ws.write()devuelve false, el evento 'drain' se activa cuando se pueden seguir escribiendo datos en la secuencia.

Implementación de un flujo de escritura de archivos

inicialización

Primero define WriteStreamla clase y heredala EventEmitter, luego inicializa los parámetros. _Presta atención a los comentarios del código_

const { EventEmitter } = require('events')
const fs = require('fs')

class WriteStream extends EventEmitter {
  constructor(path, options = {}) {
    super()

    // 初始化参数
    this.path = path
    this.flags = options.flags ?? 'w'
    this.encoding = options.encoding ?? 'utf8'
    this.autoClose = options.autoClose ?? true
    this.highWaterMark = options.highWaterMark ?? 16 * 1024

    this.offset = 0 // 文件读取偏移量
    this.cache = [] // 缓存的要被写入的内容

    // 将要被写入的总长度,包括缓存中的内容长度
    this.writtenLen = 0

    // 是否正在执行写入操作,
    // 如果正在写入,那以后的操作需放入 this.cache
    this.writing = false

    // 是否应该触发 drain 事件
    this.needDrain = false

    // 打开文件
    this.open()
  }
}

abierto()

Mismo código que ReadStream.

open() {
  fs.open(this.path, this.flags, (err, fd) => {
    if (err) {
      this.emit('error', err)
      return
    }

    this.fd = fd
    this.emit('open')
  })
}

escribir()

realizar la operación de escritura

write(chunk, encoding, cb = () => {}) {
  // 初始化被写入的内容
  // 如果时字符串,则转为 buffer
  chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
  // 计算要被写入的长度
  this.writtenLen += chunk.length
  // 判断是否已经超过 highWaterMark
  const hasLimit = this.writtenLen >= this.highWaterMark

  // 是否需要触发 drain
  // 如果超过 highWaterMark,则代表需要触发
  this.needDrain = hasLimit

  // 如果没有正在写入的内容,则调用 _write 直接开始写入
  // 否则放入 cache 中
  // 写入完成后,调用 clearBuffer,从缓存中拿取最近一次内容开始写入
  if (!this.writing) {
    this.writing = true
    this._write(chunk, () => {
      cb()
      this.clearBuffer()
    })
  } else {
    this.cache.push({
      chunk: chunk,
      cb
    })
  }

  return !hasLimit
}

// 写入操作
_write(chunk, cb) {
  if (typeof this.fd !== 'number') {
    this.once('open', () => this._write(chunk, cb))
    return
  }

  // 写入文件
  fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, bytesWritten) => {
    if (err) {
      this.emit('error', err)
      return
    }

    // 计算偏移量
    this.offset += bytesWritten
    // 写入完毕,则减去当前写入的长度
    this.writtenLen -= bytesWritten
    cb()
  })
}
  1. Primero inicialice el contenido que se va a escribir, solo admita el búfer y la cadena; si es una cadena, se convertirá directamente en búfer.

  2. Calcular la longitud total a escribir, es decirthis.writtenLen += chunk.length

  3. Determinar si se ha excedido la marca de agua alta

  4. Determinar si activar el drenaje

  5. Determine si ya hay contenido en escritura, si no, llame _write()directamente para escribir, si lo hay, colóquelo en el caché. Cuando termine _write()de escribir , llame clearBuffer()al método para recuperar el primer contenido almacenado this.cacheen caché y escribir. El método clearBuffer se ve así

borrar búfer ()

clearBuffer() {
  // 取出缓存
  const data = this.cache.shift()
  if (data) {
    const { chunk, cb } = data
    // 继续进行写入操作
    this._write(chunk, () => {
      cb()
      this.clearBuffer()
    })
    return
  }

  // 触发 drain
  this.needDrain && this.emit('drain')
  // 写入完毕,将writing置为false
  this.writing = false
  // needDrain 置为 false
  this.needDrain = false
}

código completo

const { EventEmitter } = require('events')
const fs = require('fs')

class WriteStream extends EventEmitter {
  constructor(path, options = {}) {
    super()

    this.path = path
    this.flags = options.flags ?? 'w'
    this.encoding = options.encoding ?? 'utf8'
    this.autoClose = options.autoClose ?? true
    this.highWaterMark = options.highWaterMark ?? 16 * 1024

    this.offset = 0
    this.cache = []
    this.writtenLen = 0
    this.writing = false
    this.needDrain = false

    this.open()
  }

  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error', err)
        return
      }

      this.fd = fd
      this.emit('open')
    })
  }

  clearBuffer() {
    const data = this.cache.shift()
    if (data) {
      const { chunk, cb } = data
      this._write(chunk, () => {
        cb()
        this.clearBuffer()
      })
      return
    }

    this.needDrain && this.emit('drain')
    this.writing = false
    this.needDrain = false
  }

  write(chunk, encoding, cb = () => {}) {
    chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
    this.writtenLen += chunk.length
    const hasLimit = this.writtenLen >= this.highWaterMark
    this.needDrain = hasLimit

    if (!this.writing) {
      this.writing = true
      this._write(chunk, () => {
        cb()
        this.clearBuffer()
      })
    } else {
      this.cache.push({
        chunk: chunk,
        cb
      })
    }

    return !hasLimit
  }

  _write(chunk, cb) {
    if (typeof this.fd !== 'number') {
      this.once('open', () => this._write(chunk, cb))
      return
    }

    fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, bytesWritten) => {
      if (err) {
        this.emit('error', err)
        return
      }

      this.offset += bytesWritten
      this.writtenLen -= bytesWritten
      cb()
    })
  }
}

- FIN -

Acerca de Qi Wu Troupe

Qi Wu Troupe es el equipo front-end más grande de 360 ​​Group y participa en el trabajo de los miembros de W3C y ECMA (TC39) en nombre del grupo. Qi Wu Troupe otorga gran importancia a la capacitación de talentos. Hay varias direcciones de desarrollo, como ingenieros, profesores, traductores, personas de interfaz comercial y líderes de equipo para que los empleados elijan, y se complementan con la capacitación técnica, profesional, general y de liderazgo correspondiente. curso Qi Dance Troupe da la bienvenida a todo tipo de talentos destacados para que presten atención y se unan a Qi Dance Troupe con una actitud abierta y de búsqueda de talentos.

2203fa2bd17263d2290216558a8b9f50.png

Supongo que te gusta

Origin blog.csdn.net/qiwoo_weekly/article/details/130537865
Recomendado
Clasificación