python-socketio로 Socket.io서버를 생성하고 Django와 통합하여 배포하기

개발자 강세영·2022년 10월 10일
0
post-thumbnail

설명에 앞서 기본 개념 복습

1) WebSocket

  • HTML5 웹 표준 기술이다

  • 매우 빠르게 작동하며 통신할 때 아주 적은 데이터를 이용한다.

  • 이벤트를 단순히 듣고, 보내는 것만 가능하다.

  • HTML5 Websocket은 유용한 기술이지만, 브라우저별로 지원하는 웹소켓 버전이 다르며 오래된 브라우저의 경우엔 지원하지 않는다.

2) Socket.IO

  • Socket.IO는 node.js 기반으로 만들어진 기술로, 거의 모든 웹 브라우저와 모바일 장치를 지원하는 실시간 웹 애플리케이션 지원 라이브러리이다.

  • Socket.IO는 보다 저수준의 양방향 전송 프로토콜인 Engine.IO에 기반하고 있다.

  • 여러 선택지 중에서 websocket을 이용해서 실시간 & 양방향 & event기반 통신을 기능을 제공한다. (websocket만 활용하는게 아니다.)

  • 어떤 브라우저나 핸드폰이 websocket을 지원하지 않을 때 socket.IO는 다른 방법(HTTP long polling 등)을 이용해서 계속 작동을 한다. 즉 socket.IO는 websocket만을 활용하는 기술이 아니다.

  • 소켓 연결 실패 시 fallback을 통해 다른 방식으로 알아서 해당 클라이언트와 연결을 시도한다.

  • 만약 네트워크 연결이 잠시동안 끊겨도 재연결을 시도한다. 웹소켓의 경우 이러한 재연결 기능을 제공하지 않으므로 필요하다면 직접 구현해야 한다.

  • 모든 플랫폼, 브라우저, 디바이스에서 이용 가능하다.

📌 어떤 상황에서 어떤걸 사용해야 할까?

  • 서버에서 연결된 소켓(사용자)들을 세밀하게 관리해야하는 서비스인 경우에는 Broadcasting 기능이 있는 socket.io를 쓰는 게 유지보수 측면에서 이점이 많다.
  • 가상화폐 거래소 같이 데이터 전송이 많고 처리 속도가 중요한 서비스의 경우 socket.io보다 빠르고 오버헤드가 적은 웹소켓을 이용하는게 좋다.

python-socketio

  • python-socketio는 node.js 기반의 Socket.io를 파이썬으로 구현한 패키지이다.

  • Socket.IO에 Engine.IO가 있듯이, python-socketio 또한 python-engineio 기반이다.

  • 다양한 파이썬 웹 프레임워크들과 호환된다.

  • 최신 Socket.io 표준을 지원하며 업데이트도 꾸준히 되고 있다.

  • 표준 파이썬(WSGI)과 비동기 I/O(asyncio, ASGI)를 모두 지원한다.

  • 웹소켓과 Socket.IO는 완벽하게 호환되지 않으며 나의 목표는 따로 채팅 전용의 node.js Socket.IO 서버를 만드는 게 아니라 Django와 Socket.IO 서버를 통합해서 배포하는 것이었다.

  • 그래서 Django Channels 대신 python-socketio라는 파이썬 패키지를 활용하게 되었다.

Client & Server

  • python-socketio으로 socket.io 클라이언트와 서버를 모두 구현할 수 있다.

  • 나는 이번 프로젝트에서 서버 기능만 개발했기 때문에 서버와 배포 방법에 대해서만 설명하도록 하겠다.

  • 공식문서를 보면 알겠지만 서버와 클라이언트 간 문법이나 사용법 등이 크게 다르지 않다.

  • 서버는 WSGI에 호환되는 서버인 Server와 ASGI에 호환되는 AsyncServer가 있다.

  • 클라이언트 또한 서버와 마찬가지로 Client, AsyncClient가 있다.

  • AsyncServer 또는 AsyncClient는 파이썬 asyncio의 문법 async, await를 활용할 수 있다.

서버 인스턴스 생성

import socketio

sio = socketio.Server(
    async_mode="eventlet",
    cors_allowed_origins=settings.CORS_ALLOWED_ORIGINS,
    # cors_allowed_origins='*',
    # logger=True
)
  • async_mode: 비동기처리 방법 설정, Server의 경우 따로 설정하지 않으면 eventlet부터 통신 시도
  • cors_allowed_origins: '*'로 설정하면 CORS 모두 허용
  • logger: True로 설정하면 서버에서 발생하는 이벤트 로그를 터미널에 출력하며 fatal error는 False로 설정해도 출력된다.

이벤트 핸들링

@sio.event
def connect(sid, environ, auth):
    """
    socket.io 클라이언트가 연결되면 아래 코드를 실행한다, connect는 미리 정의된 이벤트이며
    파라미터인 sid, environ, auth 들도 python-socketio 패키지에서 미리 정해놓은 것들이다.
    - environ: http 헤더를 포함한 http 리퀘스트 데이터를 담는 WSGI 표준 딕셔너리
    - auth: 클라이언트에서 넘겨준 인증 데이터, 데이터가 없으면 None이 된다
    - 클라이언트가 보낸 auth의 유저id값으로 해당 유저가 존재하지 않거나 
    - 인증이 불가한 유저는 return False해서 연결이 안되게 막는다
    - return False 하는 대신 raise ConnectionRefusedError으로 연결 거부 메시지를 전송할 수도 있다
    """
    if not auth:
    	return False
    
    
@sio.on('join')
def handle_join(sid, data):
    sio.save_session(sid, data)
    sio.enter_room(sid, room=data['room'])
    sio.emit(
        'add_message', 
        {
            "user_nickname": '함께하개 관리자', 
            "text": f"{data['nickname']}님이 들어왔어요."
        }, 
        to=data['room']
    )
  • Socket.IO 프로토콜은 이벤트 기반이며 클라이언트가 서버와 통신할 때 이벤트를 송신(emit)한다. 각각의 이벤트는 이름과 인자를 가진다.

  • python-socketio 서버는 on 또는 event로 이벤트 핸들링을 한다.

  • 위에서 만든 서버 인스턴스의 on 또는 event를 데코레이터로 쓰고 이름이 일치하는 이벤트가 발생하면 래핑된 이벤트 핸들링 함수가 실행된다.

  • @sio.event는 밑에서 데코레이터 받은 함수명(def 함수명)을 이벤트 명으로 받으며, @sio.on('이벤트 명')는 인자로 문자열 이벤트 명을 받는다.

  • 따라서 @sio.on으로는 파이썬에서 함수명으로 쓰지 못하는 이름이나 공백이 들어간 문자열을 이벤트명으로 사용 할 수 있다.

  • socket.io 클라이언트가 서버에 연결되면 위의 connect 함수가 실행되고, 클라이언트가 'join'이라는 이벤트 명으로 송신(emit)하면 위의 handle_join 함수가 실행되는 것이다.

  • @sio.on('*')으로 모든 이벤트를 핸들링할 수 있다. 단 connect와 disconnect는 제외된다.

  • on은 데코레이터 외에도 인자 값을 바꿔서 메서드로 활용할 수도 있지만 event는 데코레이터로만 쓸 수 있다, eventon을 단순하게 만든 것이다.

  • connect와 disconnect는 클라이언트가 서버에 접속하거나 서버와 접속이 끊어질 때 발생하는 이벤트이다.

  • 이벤트 명으로 connect, message, disconnect 등은 미리 명시적으로 정의된 이벤트로, python-socketio에서 정해진 대로만 사용할 수 있다.

  • socketio.Server.emit(), @sio.emit()안의 인자인 to와 room은 완전히 똑같다(room이 to의 alias이다), 둘 중 아무거나 써도 되며 특정 room으로만 메시지를 보낼 때 사용한다.

서버 전체 코드와 설명(Github Gist) 바로가기

클라이언트 코드(React) 바로가기

python-socketio 배포

  • 채팅을 위한 socket.io는 웹소켓 기반 라이브러리인데, 웹소켓은 비동기 통신에 기반한다.

  • 따라서 파이썬 WSGI(동기식, Gunicorn 등)와 통합하여 python-socketio 서버를 실행하려면 비동기 처리에 대한 설정을 해줘야 한다.

  • python-socketio 공식문서에 따르면 서버 생성 시 async_mode 파라미터에 값을 지정하여 원하는 배포 방식을 선택할 수 있다고 한다.

  • async_mode는 다양한 방법이 있는데 나는 그중에서 Eventlet을 선택했다. 비동기가 아닌 표준 서버(Server)는 async_mode을 지정하지 않으면 자동으로 eventlet부터 통신을 시도하므로 사실상 기본 값이다.

  • 당연하게도 eventlet을 사용하려면 pip install eventlet으로 설치해줘야 한다. 나는 배포 과정 중에 gunicorn과의 호환성 에러가 나서 최신버전이 아닌 0.30.2 버전으로 설치했다.

  • Eventlet은 동시성 네트워킹을 위한 파이썬 라이브러리이며, 코드 작성 방법이 아니라 코드 실행 방법을 바꿔준다고 한다.

  • Eventlet 공식문서 설명:
    Eventlet is a concurrent networking library for Python that allows you to change how you run your code, not how you write it.

  • python-socketio 공식문서의 Eventlet 설명:
    Eventlet is a high performance concurrent networking library for Python 2 and 3 that uses coroutines, enabling code to be written in the same style used with the blocking standard library functions.

  • 나는 이 설명을 평범한 동기식(sync and blocking) 코드를 알아서 비동기(async and non-blocking) 방식으로 실행 시켜주는 것이라고 이해하고 있다. 이에 관련해서 eventlet에는 monkey_patch라는 기능이 있다.

  • Server의 async_mode의 선택지 중에서 Eventlet, Gevent는 웹소켓 뿐 아니라 http long-polling 또한 지원한다고 한다.

  • Eventlet을 WSGI 앱(Django)과 연동하여 직접 실행하는 방법과 Gunicorn의 워커 클래스로 Eventlet를 지정하여 실행하는 방법이 있는데 나는 후자를 택했다. 두 가지 방법을 동시에 같은 포트에서 실행하려고 하면 포트 충돌이 나서 실행되지 않으므로 주의해야 한다.

  • Gunicorn과 Eventlet를 연동하여 실행하는 명령어
    gunicorn -k eventlet -w 1 module:app

  • -w 옵션은 worker 프로세스의 수이며 반드시 1이어야 한다, 1보다 높은 값을 입력하면 채팅 시 에러가 발생한다, -k 옵션은 worker의 종류를 지정하는 것이다.

  • python-socketio 공식문서:
    Due to limitations in its load balancing algorithm, gunicorn can only be used with one worker process, so the -w option cannot be set to a value higher than 1. A single eventlet worker can handle a large number of concurrent clients, each handled by a greenlet.

  • socket.io 클라이언트의 호스트 주소가 잘못되면 당연히 서버-클라이언트 간 통신이 안된다.

공식문서를 무지성으로 따라하다가 삽질한 기록

  • 문제: Django와 python-socketio 서버를 통합하고 gunicorn 실행이 안 되는 현상

  • 원인: wsgi.py 파일에서 밑의 eventlet.wsgi.server 코드가 실행되면 안된다. 밑의 코드는 파이썬 스크립트로 eventlet을 직접 실행하는 코드이다.

    • eventlet.wsgi.server(eventlet.listen(("", 8000)), application)
  • gunicorn togedog_dj.wsgi:application으로 실행할 때 wsgi.py 파일의 스크립트 코드도 실행되므로 wsgi.py에서 위의 코드를 주석처리 하거나 없애야 한다.

  • 그렇지 않으면 eventlet이 먼저 8000번 포트로 실행되고 다음에 gunicorn도 8000번 포트로 실행하려고 하기 때문에 포트 충돌로 gunicorn은 실행이 안된다. 포트번호를 다른걸로 지정하면 둘다 실행되긴 하겠지만 굳이 그럴 필요가 없다.

  • 공식 문서에 나오는 내용을 무지성으로 보고 따라할 게 아니라 생각하면서 적용해야 한다는 것을 깨달았고 영어 독해력도 코딩 못지않게 중요하다는 걸 실감했다.

# Django 프로젝트 폴더의 wsgi.py파일

import os
import socketio
# import eventlet

from django.core.wsgi import get_wsgi_application

from chat.socketio import sio # 서버 인스턴스 임포트

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "togedog_dj.settings")

# django wsgi app
application = get_wsgi_application()

# wrap with socketio's middleware
application = socketio.WSGIApp(sio, application)

# eventlet를 직접 실행하는 코드, gunicorn 사용시 주석처리 하거나 제거할 것
# eventlet.wsgi.server(eventlet.listen(("", 8000)), application)

일반 실행 명령어

  • gunicorn -k eventlet -w 1 <wsgi.py파일 경로>:<파일 안의 app이름>
  • gunicorn -k eventlet -w 1 togedog_dj.wsgi:application

백그라운드 실행 명령어

  • nohup gunicorn -k eventlet -w 1 togedog_dj.wsgi:application &

도움이 된 자료들

0개의 댓글