웹 소켓으로 바이너리 파일 업로드하기

Log. Wed, Nov 30, 2016 2:21 PM | Category : HTML5 API | Views: 308

 WebSocket은 ArrayBuffer를 지원하기 때문에 충분히 파일 업로드가 가능합니다. 단지 기본적인 WebSocket은 send + onmessage라는 하나의 경로를 통해 모든 데이터를 주고 받기 때문에 이를 적절하게 구분해줘야 합니다. 따라서 데이터가 어떤 데이터인지를 알아내기 위해 직렬화된 JSON으로 데이터를 주고받되, action이라는 문자열 데이터를 추가해 여기에 어떤 행동에 대한 데이터를 주고받는지, 그리고 실제 데이터는 data라는 속성에 넣고 주고받겠습니다. 즉, 아래와 같은 구조로 데이터를 주고받습니다.

{
    action: 'doSomething',
    data: {
        a: 1,
        b: 2
    }
}

 

아래는 서버 코드입니다. 주목할 점은 ws모듈은 ArrayBuffer로 데이터를 받아오지만 Node.js에서 ArrayBuffer로 파일을 쓸 수 없기 때문에 Node.js의 Buffer로 변환하는 부분, 그리고 클라이언트가 전송한 데이터가 문자열인 경우 방금 언급한 정해진 구조로 데이터를 주고받지만 바이너리 데이터(ArrayBuffer)가 전달될 경우 바로 파일로 기록된다는 부분 입니다.

"use strict";
const fs = require('fs');
const path = require('path');
const http = require('http');
const server = http.createServer();
const url = require('url');
const express = require('express');
const ws = require('ws');

const app = express();
const wss = new ws.Server({ server: server });

app.use(express.static(__dirname));

app.get('/', (req, res) => {
	res.sendFile(__dirname + '/index.html');
});

function toBuffer(ab) {
	var buf = new Buffer(ab.byteLength);
	var view = new Uint8Array(ab);
	
	for(var i = 0; i < buf.length; i++) {
		buf[i] = view[i];
	}

	return buf;
}

wss.on('connection', (socket) => {
	console.log('Socket connected.');

	// start sending file
	var filename = '';
	var filesize = 0;
	var wrote = 0;
	var writeStream = null;

	socket.binaryType = 'arraybuffer';
	socket.onmessage = function(ev) {
		var recv = ev.data;

		if(typeof recv === 'string') {
			var parsedRecv = JSON.parse(recv);

			switch(parsedRecv.action) {
				case 'start':
					filename = parsedRecv.data.filename;
					filesize = parsedRecv.data.filesize;
					wrote = 0;
					
					socket.send(JSON.stringify({ action: 'continue'} ));
					writeStream = fs.createWriteStream(path.join(__dirname, filename));
					break;
				case 'done':
					console.log('All data sent. End Transmission.');
					writeStream.close();
					socket.send(JSON.stringify({ action: 'complete' }));
					break;
			}
		}
		else {
			// if ArrayBuffer trasmitted, convert to Node.js Buffer
			if(recv instanceof ArrayBuffer) {
				recv = toBuffer(recv);	
			}

			function write() {
				var writeDone = writeStream.write(recv);
				wrote += recv.byteLength;

				console.log(wrote + ' / ' + filesize);

				if(!writeDone) {
					writeStream.once('drain', () => socket.send(JSON.stringify({ action: 'continue' })));
				}
				else {
					socket.send(JSON.stringify({ action: 'continue' }));
				}
			}

			write();
		}
	};
});

server.on('request', app);
server.listen(3000, () => {
	console.log('Listening on ' + 3000);
});

 

다음으로 클라이언트 코드를 살펴보겠습니다.

var socket = new WebSocket('ws://127.0.0.1:3000');
socket.onopen = function() {
	console.log('Socket connected.');

	socket.binaryType = 'arraybuffer';
	socket.onmessage = function(ev) {
		var recv = ev.data;
		var parsedRecv = JSON.parse(recv);
		
		if(parsedRecv.action === 'continue') {
			sendChunk();
		}
		else if(parsedRecv.action === 'complete') {
			console.log('Upload complete!');
		}
	};
};

var buffer = null;
var CHUNK_SIZE = 10240;
var sent = 0;

function sendChunk() {
	if(sent >= buffer.byteLength) {
		socket.send(JSON.stringify({ action: 'done' }));
		return;
	}

	var chunk = buffer.slice(sent, sent + CHUNK_SIZE);
	socket.send(chunk);

	sent += chunk.byteLength;

	console.log(sent + '/' + buffer.byteLength);
}


var fileEl = document.getElementById('file');
var form = document.getElementById('form');

form.onsubmit = function(ev) {
	ev.preventDefault();

	var file = fileEl.files[0];

	if(!file) return;
	var filename = file.name;
	var filesize = file.size;

	// start uploading here	
	var fileReader = new FileReader();
	fileReader.onloadend = function() {
		buffer = fileReader.result;
		sent = 0;
		
		socket.send(JSON.stringify({
			action: 'start',
			data: {
				filename: filename,
				filesize: filesize
			}
		}));
	};

	fileReader.readAsArrayBuffer(file);
};

 

서버 코드와 마찬가지로 직렬화된 JSON으로 데이터를 주고 받습니다. 중요한 부분은 sendChunk메서드인데, 이 메서드는 바이너리 파일을 쪼개서 정해진 크기만큼 서버로 전송하는 메서드입니다.

마지막으로 아래는 HTML입니다.

<html>
<head>
	<meta charset="UTF-8">
	<title>Document</title>
</head>
<body>
	<form id="form">
		<h1>Fileupload with WebSocket as ArrayBuffer</h1>
	
		<input type="file" id="file" />
		
		<input type="submit" value="upload" />
	</form>

	<script src="app.js"></script>
</body>
</html>

 

테스트해보니 코드는 잘 동작하네요. 참고로 이 코드들은 제가 개발했던 Socket.io-fileSocket.io-file-client에서 내부적으로 사용된 코드입니다. 다만 이 코드는 하나의 파일에 대한 업로드만을 지원하며 복수 파일을 업로드할 경우 데이터가 꼬일 수 있습니다. 바이너리 데이터를 주고받는 부분에서 어디서 전달된 데이터인지 식별할 방법이 없거든요. 이 부분을 해결하려면 제가 예전에 만들었던 string-binary-attacher를 활용해 바이너리 데이터에 메타 데이터(문자열)을 붙여서 보낸 후 서버단에서 분리해 활용하는 방법이 있을 수 있습니다. 이론상으론 가능하니 언젠가 테스트해보긴 해야겠네요.