시작하면서….


스트리밍 서비스의 대상은 주로 동영상이나 음악 정도가 될 수 있겠죠. 하드같은 스토리지에서 직접 동영상 파일을 직접 스트리밍 시킬 수도 있지만, 이번에는 데이터베이스에서 직접 파일을 꺼내서 바로 스트리밍 하는 방법을 소개해 볼까 합니다.

이 전에 제가 mongoDB의 GridFS에 관한 글을 작성했었는데요, 이번에는 지난 글에 소개한 GridFS의 응용판이라고 보시면 될 것 같습니다.

GridFS통해서 저장된 파일들은 mongodb 드라이버를 통해서 검색이나 수정등이 가능합니다. 또한 npm에는 GridFS와 관련된 모듈도 많이 있습니다.

또는 직접 GridFS API를 사용할 수 도 있습니다.

이번 시간에는 GridFS관련 모듈 중 gridfs-steam을 사용하여 간단한 스트리밍 서버를 만들어 보겠습니다.

 

설치하기


> npm install gridfs-stream --save

위과 같은 명령어로 간단하게 설치가 가능합니다.

물론 gridfs관련 모듈을 설치하기 전에 mongoDB는 당연히 설치되어 있어야 하겠죠?

 

gridfs-steam모듈은 gridfs를 이용하여 스트림으로 파일을 주고 받을 수 있게 만들어진 모듈입니다. 스트림으로 하게 되면 아무래도 시스템의 리소스 측면에서 많은 이점을 갖게 됩니다. 따라서 대용량 파일을 서비스 하게 된다면 반드시 고려되어야 할 것 중 하나입니다.

 

사용방법


기본적인 프로그램의 틀은 다음과 같습니다.

  1. var mongo = require(‘mongodb’);
  2. var Grid = require(‘gridfs-stream’);
  3. // create or use an existing mongodb-native db instance.
  4. // for this example we’ll just create one:
  5. var db = new mongo.Db(‘yourDatabaseName’, new mongo.Server(“127.0.0.1”, 27017));
  6. // make sure the db instance is open before passing into `Grid`
  7. db.open(function (err) {
  8. if (err) return handleError(err);
  9. var gfs = Grid(db, mongo);
  10. // all set!
  11. })

all set 주석 되어진 곳에 프로그램을 작성하면 됩니다.

모든 데이터베이스가 그렇듯이 데이터베이스는 작업하기 전에 한번 열어줘야 됩니다.

이 곳에서 프로그래밍 하는 내용은 파일을 읽고 쓰고 지우고 조회하는 정도 입니다.

 

createReadStream

createReadStream의 사용법은 fs모듈에서 쓰는 방법과 동일합니다.

  1. var readstream = gfs.createReadStream(options);
  2. readstream.pipe(response);

 

createWriteStream

  1. var writestream = gfs.createWriteStream([options]);
  2. fs.createReadStream(‘/some/path’).pipe(writestream);

createWriteStream의 사용법은 fs모듈에서 쓰는 방법과 동일합니다.

클라이언트에서 보낸 파일을 하드에 저장하게 되고 createWriteStream을 사용하여 mongoDB에 보낼 수 있습니다.

GridFS에 파일을 보낼 때 스트리밍 방식으로 보내는데, 스트림으로 만든 파일을 스트림으로 보내는데 .pipe()를 사용합니다.

 

파일 삭제

  1. gfs.remove(options, function (err) {
  2. if (err) return handleError(err);
  3. console.log(‘success’);
  4. });

options에 {filename: "file.txt"} 또는 {_id : "50e03d29edfdc00d34000001"} 를 써서 특정한 파일을 찾아서 삭제 가능합니다.

 

파일 존재 여부 체크

  1. gfs.exist(options, function (err, found) {
  2. if (err) return handleError(err);
  3. found ? console.log(‘File exists’) : console.log(‘File does not exist’);
  4. });

options에 filename 또는 _id를 사용해서 체크 할 수 있습니다.

 

파일 검색하기

  1. var gfs = Grid(conn.db);
  2. gfs.files.find({ filename: ‘myImage.png’ }).toArray(function (err, files) {
  3. if (err) ...
  4. console.log(files);
  5. })

단 한개의 파일을 찾을 때는,

  1. gfs.findOne({ _id: ’54da7b013706c1e7ab25f9fa’}, function (err, file) {
  2. console.log(file);
  3. });

mongoDB의 사용법과 유사합니다.

검색할 때 filename과 _id뿐만 아니라 metadata를 이용해서 검색도 가능합니다.

metadata는 이 전에 썼던 글을 참고하시면 무슨 뜻인지 충분히 아실거라고 생각합니다.

 

실제로 적용해보기


이번에는 예전에 만든 socket.io를 이용해서 업로드 하는 방법을 수정해서 파일시스템에 저장하는것이 아니라 GridFS에 저장되게 만들겠습니다.

  1. socket.on(‘Upload’, function (data){
  2. var Name = data.Name;
  3. Files[Name].Downloaded += data.Data.length;
  4. Files[Name].Data += data.Data;
  5. if(Files[Name].Downloaded == Files[Name].FileSize){
  6. fs.write(Files[Name].Handler, Files[Name].Data, null, ‘Binary’, function(err, written) {
  7. if (err) console.error(err);
  8. //Generate movie thumbnail
  9. var readable = fs.createReadStream(“Temp/” + Name);
  10. var writable = fs.createWriteStream(“Video/” + Name);
  11. readable.pipe(writable);
  12. writable.on(‘finish’, function (err) {
  13. if (err) console.error(err);
  14. console.log(Name + ” : writing is completed.”);
  15. fs.close(Files[Name].Handler, function (err) { //close fs module
  16. if (err) console.error(err);
  17. fs.unlink(“Temp/” + Name, function (err) {
  18. //Moving File is Completed
  19. if (err) console.error(err);
  20. console.log(Name + ” is deleted.”);
  21. });
  22. });
  23. });
  24. });
  25. }
  26. else if(Files[Name].Data.length > 10485760){
  27. fs.write(Files[Name].Handler, Files[Name].Data, null, ‘Binary’, function(err, written){
  28. Files[Name].Data = “”; //Reset The Buffer
  29. var Place = Files[Name].Downloaded / 524288;
  30. var Percent = (Files[Name].Downloaded / Files[Name].FileSize) * 100;
  31. socket.emit(‘MoreData’, { ‘Place’ : Place, ‘Percent’ : Percent});
  32. });
  33. } else {
  34. var Place = Files[Name].Downloaded / 524288;
  35. var Percent = (Files[Name].Downloaded / Files[Name].FileSize) * 100;
  36. socket.emit(‘MoreData’, { ‘Place’ : Place, ‘Percent’ : Percent});
  37. }
  38. });

위의 프로그램은 소켓으로 데이터를 전송 받고, 데이터 전송이 끝나면 파일을 옮기고 임시 파일을 삭제하는 내용입니다.

이 곳에서 우리가 필요없는 부분은 파일을 옮기는 부분이겠네요.

그 부분을 삭제하고 GridFS에 저장하는 부분을 만들면 됩니다. 기존에 써있던 틀에서 조금만 수정하면 만들수 있겠네요.

  1. var mongo = require(‘mongodb’);
  2. var Grid = require(‘gridfs-stream’);
  3. var db = new mongo.Db(‘test’, new mongo.Server(“localhost”, 27017));
  4. var gfs = Grid(db, mongo);
  5. socket.on(‘Upload’, function (data){
  6. var Name = data.Name;
  7. Files[Name].Downloaded += data.Data.length;
  8. Files[Name].Data += data.Data;
  9. if(Files[Name].Downloaded == Files[Name].FileSize){
  10. fs.write(Files[Name].Handler, Files[Name].Data, null, ‘Binary’, function(err, written) {
  11. if (err) console.error(err);
  12. /* GridFS writing stream */
  13. db.open(function(err){
  14. if(err) console.error(err);
  15. var readable = fs.createReadStream(“Temp/” + Name);
  16. var writeStream = gfs.createWriteStream({
  17. filename: Name,
  18. metadata:{
  19. writer:socket.id,
  20. serial:“12345678”
  21. }
  22. });
  23. readable.pipe(writeStream);
  24. writeStream.on(‘close’, function(file){
  25. console.log(file.filename + ” is written to DATABASE….”);
  26. fs.close(Files[Name].Handler, function (err) { //Close fs module
  27. if (err) console.error(err);
  28. fs.unlink(“Temp/” + Name, function (err) { //This Deletes The Temporary File
  29. //Moving File Completed
  30. if (err) console.error(err);
  31. console.log(Name + ” is deleted.”);
  32. });
  33. });
  34. })
  35. });
  36. });
  37. }
  38. else if(Files[Name].Data.length > 10485760){ //If the Data Buffer reaches 10MB
  39. fs.write(Files[Name].Handler, Files[Name].Data, null, ‘Binary’, function(err, written){
  40. Files[Name].Data = “”; //Reset The Buffer
  41. var Place = Files[Name].Downloaded / 524288;
  42. var Percent = (Files[Name].Downloaded / Files[Name].FileSize) * 100;
  43. socket.emit(‘MoreData’, { ‘Place’ : Place, ‘Percent’ : Percent});
  44. });
  45. }
  46. else
  47. {
  48. var Place = Files[Name].Downloaded / 524288;
  49. var Percent = (Files[Name].Downloaded / Files[Name].FileSize) * 100;
  50. socket.emit(‘MoreData’, { ‘Place’ : Place, ‘Percent’ : Percent});
  51. }
  52. });

예제를 보고 헷갈려 하실 수도 있는데요, multer나 busboy같은 모듈을 사용하셨더라도 파일을 임시로 받은 저장소에서 동일한 방법으로 처리하면 사용 가능합니다.

임시로 받은 파일을 스트리밍으로 만들기 위해 fs.createReadStream을 사용했구요, 이 파일을 GridFS에 넣기 위해서 gfs.createWriteStream을 사용해서 처리했습니다.

파일을 데이터베이스에 넣을때 나중에 특별하게 구별하기 위한 metadata도 작성해줬습니다. 이런식으로 metadata를 남겨주면 나중에 해당 파일을 검색하거나 통계를 낼 때 프로그램 작성이 더욱 용이해집니다.

이제 해당 파일이 존재하는지 확인해 봐야겠죠?

mongoDB의 내용을 살펴 볼수 있는 어떠한 툴을 사용해도 상관 없습니다.

저는 그냥 쉘 명령어를 사용해서 확인해보겠습니다.

> show dbs
abc 0.000GB
abc2 0.000GB
local 0.000GB
exam_board 0.001GB
largefile-test 0.170GB

> use largefile-test
switched to db largefile-test

> show collections
fs.chunks
fs.files

> db.fs.files.distinct("filename")
[ "abc1.m4v", "abc2.m4v", "abc3.mp4", "abc4.mp4" ]

>

저는 largefile-test라는 데이터베이스에서 작업을 했고, 이 데이터베이스를 조회해보니 fs.chunks와 fs.files라는 콜렉션이 존재합니다.

GridFS를 사용하여 파일을 저장하면 fs.chunks와 fs.files 콜렉션이 생성됩니다. 파일에 대한 정보는 fs.files에 저장되어 있습니다.

files의 내용을 전체적으로 살펴보기 귀찮으니까 전체적으로 어떤 파일이 올라가 있는지 확인해보려면,

db.fs.files.distinct("filename") 으로 파일이름만 따로 불러낼 수 있습니다.

파일이 잘 올라갔다면 이 곳에서 금방 올리신 파일 이름을 확인 할 수 있을 겁니다.

 

스트리밍으로 서비스하기


스트리밍으로 서비스 하기 위해서는 html에서 사용가능한 플레이어가 있어야하고, 서버에서는 해당 주소의 요청이 있을 경우 스트리밍으로 파일을 전송해줘야 합니다.

이것을 응용하면 핸드폰에서도 얼마든지 영화나 동영상을 볼 수 있는 시스템을 제작할 수 있습니다.

여기에서는 서버단에서 어떻게 하면 스트리밍으로 만들 수 있는지 설명하겠습니다.

  1. router.get(‘/video’, function(req,res){
  2. console.log(“video connected”);
  3. //var emitter = new EventEmitter();
  4. db.close();
  5. db.open(function(err) {
  6. console.log(“DB open”);
  7. if(err) console.error(err);
  8. var readStream = gfs.createReadStream({
  9. filename: “abc1.mp4”
  10. }).pipe(res);
  11. readStream.on(‘error’, function (err) {
  12. console.log(‘An error occurred!’, err);
  13. throw err;
  14. });
  15. res.on(‘finish’, function (err) {
  16. console.log(‘File streaming end’);
  17. db.close();
  18. });
  19. });
  20. });

GridFS에서 바로 스트리밍으로 내보내는 작업은 생각보다 간단합니다.

createReadStream에서 filename 또는 metadata의 내용을 사용하여 내가 원하는 1개의 파일을 써야합니다.

어떤 파일을 쓸지 모르는 상황에서는 find또는 exist등의 명령어를 이용해서 검색하면 됩니다.

 

여기까지 작성을 마쳤으면, 원하는 플레이어에서 직접 ‘/video’를 요청하면 요청한 동영상 또는 음악을 감상 하실 수 있습니다.

설명이 부족하거나 이해가 잘 안가는 부분이 있으면 댓글 남겨주세요.