[Python] 웹소켓을 이용해서 웹 페이지에서 파일 업로드하는 방법


Development note/Python  2020. 3. 27. 18:57

안녕하세요. 명월입니다.


이 글은 Python의 웹 소켓을 이용해서 웹 페이지에서 파일 업로드하는 방법에 대한 글입니다.


예전에 제가 작성한 글 중에 ajax를 이용해서 파일 업로드하는 방법에 대한 글이 있습니다.

링크 - [Jquery] ajax를 이용해 파일 업로드하는 방법


그 내용의 원래 목적은 업로드 할 파일이 대용량일 경우, form submit을 이용해서 서버에 업로드를 할 때 여러가지 제한 사항에 걸려서 ajax로 파일을 나누어 전송하는 방법에 대해 설명하고 있습니다.

업로드를 해야할 파일이 1기가나 2기가 정도의 큰 파일를 업로드를 하려고 하면 server 측의 request buffer max size도 재설정을 해야합니다.그 때 버퍼 사이즈가 너무 크게 설정되면 전체적인 웹 성능에 영향을 주기 때문에 파일을 쪼개서 전송할 필요가 있습니다. form request로는 파일을 나눌 수가 없으니 ajax로 전송을 하는 것 입니다.

프로그램의 흐름은 업로드할 데이터를 base64코드로 변환하고 string타입의 데이터로 반복적으로 ajax 호출을 통해 파일을 업로드하는 것입니다.


글 내용 중에 이런 방식은 ajax보다는 websocket이 더 나을 것 같다라고 직상한 것이 있는데 어떤 분이 그럼 websocket으로 구현된 파일 업로드하는 방법에 대한 질문이 있어서 구현했습니다.

websocket은 python으로 구현을 했습니다. 혹시 다른 언어도 필요하다면 댓글 남겨주세요.

링크 - [Python] Websocket을 사용하는 방법

import asyncio;
# 웹 소켓 모듈
import websockets;
# base64를 binary로 변환하는 모듈
import base64;
# 업로드 할 때 데이터 정보에 관한 클래스
class Node():
  # 생성자
  def __init__(self):
    # 파일 이름
    self.__filename = '';
    # 파일 사이즈
    self.__filesize = 0;
    # base64로 된 파일 데이터
    self.__data = '';
  # 파일 이름 프로퍼티
  @property
  def filename(self):
    return self.__filename;
  @filename.setter
  def filename(self, filename):
    self.__filename = filename;
  # 파일 사이즈 프로퍼티
  @property
  def filesize(self):
    return self.__filesize;
  @filesize.setter
  def filesize(self, filesize):
    # websocket에서는 string type으로 데이터가 오기 때문에 int형으로 변환
    self.__filesize = int(filesize);
  # 파일 데이터 프로퍼티
  @property
  def data(self):
    return self.__data;
  @data.setter
  def data(self, data):
    self.__data = data;
  # 파일 데이터를 연속적으로 추가하는 함수
  def add_data(self, data):
    self.__data += data;
  # 파일 전송이 끝났는지 확인하는 함수
  def is_complate(self):
    # 다운 받은 파일 크기와 요청된 파일 크기가 같으면 종료
    return self.__filesize == len(self.__data);
  # base64로 된 데이터를 파일로 저장하는 함수
  def save(self):
    # string을 byte로 변환(base64는 ascii코드로 구성되어 있음)
    byte = self.__data.encode("ASCII");
    # byte64를 binary로 디코딩
    byte = base64.b64decode(byte);
    # 파일 IO 오픈
    with open("d:\\workd2\\"+self.__filename, "wb") as handle:
      # 파일 작성
      handle.write(byte);
    # 콘솔 출력
    print("craete file - d:\\workd2\\"+self.__filename);

# 웹 소켓 클라이언트가 접속이 되면 호출된다.
async def accept(websocket, path):
  # 데이터 정보에 관한 클래스 할당
  node = Node();
  # 무한 루프, 파일 전송이 끝나면 종료한다.
  while True:
    # cmd를 받는다.
    cmd = await websocket.recv();
    # 처음 접속시 웹소켓에서 START 명령어가 온다.
    if cmd == 'START':
      # 파일 이름을 요청한다.
      await websocket.send("FILENAME");
    # 파일 이름에 대한 명령어가 오면,
    elif cmd == 'FILENAME':
      # 파일 이름을 받는다.
      node.filename = await websocket.recv();
      # 파일 사이즈를 요청한다.
      await websocket.send("FILESIZE");
    # 파일 사이즈에 대한 명령어가 오면
    elif cmd == 'FILESIZE':
      # 파일 사이즈를 설정한다.
      node.filesize = await websocket.recv();
      # 파일 데이터를 요청한다.
      await websocket.send("DATA");
    # 파일 데이터에 대한 명령어가 오면
    elif cmd == 'DATA':
      # 파일을 받아서 데이터를 추가한다.
      node.add_data(await websocket.recv());
      # 파일 전송이 끝나지 않으면
      if node.is_complate() == False:
        # 파일 데이터를 요청한다.
        await websocket.send("DATA");
      else:
        # 파일 전송이 끝나면 저장한다.
        node.save();
        # 웹 소켓을 닫는다.
        await websocket.close();
        # 종료!
        break;
# 웹 소켓 서버 생성.호스트는 localhost에 port는 9998로 생성한다.
start_server = websockets.serve(accept, "localhost", 9998);
# 비동기로 서버를 대기한다.
asyncio.get_event_loop().run_until_complete(start_server);
asyncio.get_event_loop().run_forever();

클라이언트 측의 소스입니다.

<!DOCTYPE html>
<html>
  <head>
    <title>python websocket ex</title>
  </head>
  <body>
    <!-- 파일 업로드할 파일 요소 -->
    <input type="file" id="fileupload">
    <!-- 버튼을 누르면 파일 개시 -->
    <button id="uploadClick">파일 업로드</button>
    <!-- 진행 상황 표시 -->
    <span id="progress"></span>
    <!-- Jquery 연결 -->
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script>
      // 파일 업로드 버튼을 누르면
      $('#uploadClick').on('click', function(){
        // 파일 요소로 부터 파일 정보 취득
        var file = $("#fileupload")[0].files[0];
        // 파일 이름 취득
        var filename = file.name;
        // 파일을 base64로 변환할 FileReader 선언
        var reader = new FileReader();
        // 파일을 읽으면 요청되는 이벤트
        reader.onload = function(e) {
          // base64코드 
          var base64data = reader.result;
          var data = base64data.split(',')[1];
          // 전송 파일 버퍼
          var sendsize = 1024;
          // 파일 사이즈
          var filelength = data.length;
          // 전송되는 파일 조각 위치
          var pos = 0;
          setTimeout(function() {
            // 웹 소켓 접속
            var webSocket = new WebSocket("ws://localhost:9998");
            // 접속이 되면 START 명령어를 전송한다.
            webSocket.onopen = function(){
              webSocket.send("START");
            };
            // START -> FILENAME -> FILESIZE -> DATA -> DATA -> ... 순으로 파일 요청이 온다.
            webSocket.onmessage = function(message){
              // 명령어 echo
              webSocket.send(message.data);
              // 파일 이름 요청시
              if(message.data === 'FILENAME'){
                // 파일 이름 전송.
                webSocket.send(filename);
              // 파일 사이즈 요청시
              } else if(message.data === 'FILESIZE'){
                // 파일 사이즈 전송
                webSocket.send(filelength);
              // 파일 데이터 요청시
              } else if(message.data === 'DATA'){
                // buffer 크기만큼 파일을 나누어서 전송한다.
                webSocket.send(data.substring(pos, pos + sendsize));
                // 조각 위치 이동
                pos = pos + sendsize;
                // 파일 조각 위치가 파일 크기를 넘어서면 종료
                if (pos > filelength) {
                  pos = filelength;
                }
                // 프로그래스 상태바 설정
                $('#progress').text(pos + ' / ' + filelength);
              }
            };
            // 파일 전송이 완료되면 서버로 부터 웹 소켓 종료 요청이 온다.
            webSocket.onclose = function(){
              // 콘솔 출력
              console.log("completed");
            }
          });
        }
        // FileReader로 파일을 읽어 옴.
        reader.readAsDataURL(file);
      });
    </script>
  </body>
</html>

먼저 python의 웹 소켓을 기동합니다.

Python 소스에서 콘솔 출력을 아무 것도 하지 않았기 때문에 아무런 메시지가 안나오네요.

웹 페이지를 접속합니다.

클라이언트도 파일 업로드하기 전까지 웹 소켓을 접속하는 건 아니기 때문에 아무런 액션도 나오지 않습니다.

이 이미지 파일을 업로드 하겠습니다.

파일 크기도 크지 않고 로컬 안에서 통신이기 때문에 거의 순식간에 업로드가 끝납니다.

console 창에는 completed 로그가 남았습니다. onclose 이벤트가 발생해서 웹 소켓 접속이 종료가 된 것입니다.

Python 콘솔 창에도 파일이 저장이 되었다고 로그가 남았네요.

업로드가 완료되었습니다.


웹 프로그램에서 파일 업로드를 하는 페이지는 이렇게 웹 서버가 아닌 Python으로 독립된 서버를 두어서 업로드 파일을 따로 분리하면 성능 개선에도 많은 도움이 될 것 같습니다.


여기까지 Python의 웹소켓을 이용해서 웹 페이지에서 파일 업로드하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.