Nodejs – Module (buffer / stream)

Buffer / Streaming

Streaming 이란 ?

서버가 동영상 파일을 보내주고, 사용자가 동영상 파일을 다 받을때까지 기다렸다가 동영상을 보게되면 너무 번거롭고 오랜시간이 걸린다.

서버에서 동영상 파일을 잘게잘게 나눠서 보내주고, 사용자가 전체 동영상을 다 받지 않아도 동영상을 볼 수 있게끔 해주는 것을 스트리밍이라고 한다. (Progressive download)

사용자가 동영상을 보는 속도보다 다운로드 받는 속도보다 더 빠르다면, ‘버퍼링’을 이용해서 버퍼를 더 채워둘 수 있다.
그래서 반대로 다운로드 받는 속도보다 사용자가 동영상을 보는 속도가 더 빠르면, 충분히 쌓여있는 버퍼가 없기때문에 ‘버퍼링에 걸렸다’ 라는 말을 쓴다.

트위치라는 실시간으로 게임하는 장면을 생중계하는 서비스를 생각해보자.
사용자가 게임을 하는 장면을 실시간으로 녹화하면서 조금조금씩 그 데이터를 스트리밍 하게되고, 서버에서는 스트리밍된 데이터를 서버에서 버퍼링을 했다가 작은 단위의 mp4 파일을 실시간으로 보고있는 시청자에게 보내준다.


Buffer

컴퓨터에서도 파일을 읽을때, 그 파일의 데이터를 메모리에 가져온다.
만약 그 파일의 사이즈가 정말 크다면, 당연히 메모리에 부하가 된다.

이럴때는, 작은 단위의 데이터(버퍼)를 스트리밍해서 조금씩 메모리로 가져가면 된다.

// Buffer : Fixed-size chunk of memory (버퍼란, 메모리에서 고정된 사이즈의 메모리 덩어리)
// array of integers, byte of data 

const buf1 = Buffer.from('Hi');
console.log(buf1); // <Buffer 48 69> (유니코드 형태)
console.log(buf1.length); // 2
console.log(buf1[0]); //72 (ASCII 코드 형태)
console.log(buf1[1]); //105
console.log(buf1.toString('utf-8')); // Hi (toString의 기본 포맷은 'utf-8' 생략 가능)

// create
const buf2 = Buffer.alloc(2); // 메모리 내에서 사이즈가 2개인 버퍼를 만든다. (만듦과 동시에 데이터 초기화 시킴)
const buf3 = Buffer.allocUnsafe(2); // 초기화 시키지 않음 (다른 데이터가 들어있을 수 있음) (빠르다) 

buf2[0] = 72; //H (ASCII)
buf2[1] = 105; //i (ASCII)
buf2.copy(buf3);
console.log(buf2.toString());
console.log(buf3.toString());

// concat (버퍼를 합침)
const newBuf = Buffer.concat([buf1,buf2,buf3]);
console.log(newBuf.toString());

버퍼라는 것은 문자열이 될 수도 있고, 숫자가 될 수도 있고, 데이터를 로우 형태로 바이트 단위로 처리할 수 있게 해준다.


const fs = require('fs');

const beforeMem = process.memoryUsage().rss;
fs.readFile('./file.txt',(_, data)=>{
    fs.writeFile('./file2.txt', data, ()=>{});
    // calculate
    const afterMem = process.memoryUsage().rss;
    const diff = afterMem - beforeMem;
    const consumed = diff / 1024 / 1024 ;
    console.log(diff);
    console.log(`Consumed Memory : ${consumed}MB`);
});

/*
5115904
Consumed Memory : 4.87890625MB
*/

만약 위 코드에서 ‘file.txt’ 파일이 내 컴퓨터가 가지고 있는 메모리보다 더 큰 사이즈라면 다 읽어올 수가 없게된다. 이렇게 readFile() 로 모두 읽어와서 writeFile()로 쓰는건 몹시 비효율 적이다.

stream을 이용하여 모두 읽는게 아닌, 조금씩 순차적으로 읽고 쓸 수 있다.

readStream

const fs = require('fs');

const data = [];
const readStream = fs.createReadStream('./file.txt',{
    highWaterMark : 128 , //버퍼가 한번에 처리하는 양 (default : 64 kbytes)
    encoding : 'utf-8',
});

readStream.on('data', chunk =>{
    //console.log(chunk);
    data.push(chunk); //덩어리를 배열에 저장
    console.count('data');
}); // 데이터가 처리되는 이벤트 발생되면 콜백함수 실행

readStream.on('end',()=>{
    console.log(data.join('')); // data 배열에 쌓인 덩어리들을 모두 합침
}); // 데이터 읽은 스트리밍이 끝나면 콜백함수 실행

readStream.on('error', error=>{
    console.log(error);
});


/* chaining 도 가능 */
const readStream = fs.createReadStream('./file.txt',{
    highWaterMark : 128 , //버퍼가 한번에 처리하는 양 (default : 64 kbytes)
    encoding : 'utf-8',
}).on('data', chunk =>{
    //console.log(chunk);
    data.push(chunk); //덩어리를 배열에 저장
    console.count('data');
}).on('end',()=>{
    console.log(data.join('')); // data 배열에 쌓인 덩어리들을 모두 합침
}).on('error', error=>{
    console.log(error);
});

writeStream

const fs = require('fs');

const writeStream = fs.createWriteStream('./file3.txt');
writeStream.on('finish',()=>{
    console.log('finished!');
});

writeStream.write('hello!');
writeStream.write('world!');
writeStream.end(); // 쓰기가 끝났을때 호출 > finished 이벤트 호출


pipe

const fs = require('fs');
const zlib = require('zlib'); //데이터 압축 모듈

const readStream = fs.createReadStream('./file.txt');
const zlibStream = zlib.createGzip(); // 압축 스트림
const writeStream = fs.createWriteStream('./file4.zip');

const piping = readStream.pipe(zlibStream).pipe(writeStream); // stream 을 물줄기가 흐르듯이 연결해줄 수 있음. 데이터 읽는 스트림 > 압축 스트림 > 데이트 쓰기 스트림

piping.on('finish',()=>{
   console.log('done!!') ;
});

pipe는 서버를 만들때도 많이 사용한다. fs 모듈로 전부를 읽어서 response를 던지기 보다는 stream 모듈을 이용하여 파이핑 처리하는 것이 훨씬 효율적이다.

const http = require('http');
const server = http.createServer((req,res)=>{
    /*
    fs.readFile('file.txt',(err,data)=>{
        res.end(data);
    }); // 파일을 다 읽은 다음에 메모리에 들어온 data를 res에 실어보낸다.
    */
   const stream = fs.createReadStream('./file.txt');
   stream.pipe(res); // 위와 같이 하는것보다 스트림을 이용하여 스트림 자체를 response에 파이핑해서 연결해주는 것이 훨씬 효율적이다.
})
server.listen(3000);