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);