HTML5 웹 표준 기술이다
매우 빠르게 작동하며 통신할 때 아주 적은 데이터를 이용한다.
이벤트를 단순히 듣고, 보내는 것만 가능하다.
HTML5 Websocket은 유용한 기술이지만, 브라우저별로 지원하는 웹소켓 버전이 다르며 오래된 브라우저의 경우엔 지원하지 않는다.
Socket.IO는 node.js 기반으로 만들어진 기술로, 거의 모든 웹 브라우저와 모바일 장치를 지원하는 실시간 웹 애플리케이션 지원 라이브러리이다.
Socket.IO는 보다 저수준의 양방향 전송 프로토콜인 Engine.IO에 기반하고 있다.
여러 선택지 중에서 websocket을 이용해서 실시간 & 양방향 & event기반 통신을 기능을 제공한다. (websocket만 활용하는게 아니다.)
어떤 브라우저나 핸드폰이 websocket을 지원하지 않을 때 socket.IO는 다른 방법(HTTP long polling 등)을 이용해서 계속 작동을 한다. 즉 socket.IO는 websocket만을 활용하는 기술이 아니다.
소켓 연결 실패 시 fallback을 통해 다른 방식으로 알아서 해당 클라이언트와 연결을 시도한다.
만약 네트워크 연결이 잠시동안 끊겨도 재연결을 시도한다. 웹소켓의 경우 이러한 재연결 기능을 제공하지 않으므로 필요하다면 직접 구현해야 한다.
모든 플랫폼, 브라우저, 디바이스에서 이용 가능하다.
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라는 파이썬 패키지를 활용하게 되었다.
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
)
eventlet
부터 통신 시도'*'
로 설정하면 CORS 모두 허용@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
는 데코레이터로만 쓸 수 있다, event
는 on
을 단순하게 만든 것이다.
connect와 disconnect는 클라이언트가 서버에 접속하거나 서버와 접속이 끊어질 때 발생하는 이벤트이다.
이벤트 명으로 connect
, message
, disconnect
등은 미리 명시적으로 정의된 이벤트로, python-socketio에서 정해진 대로만 사용할 수 있다.
socketio.Server.emit(), @sio.emit()
안의 인자인 to와 room은 완전히 똑같다(room이 to의 alias이다), 둘 중 아무거나 써도 되며 특정 room으로만 메시지를 보낼 때 사용한다.
채팅을 위한 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 &