1. 들어가기 앞서
Flask SocketIO를 접하고, Full Stack으로 개발하면서 실시간 공지사항과 같은 알람을 나타낼 수 있으면 좋겠다는 생각으로 연구를 시작했다. Flask를 처음 개발할 때는 Apache2를 Reverse Proxy로 사용하였다. 그러나 Apache2에서 Upgrade WebSocket 헤더를 요청하는 명령어에 대해 명확하게 나타난 자료가 없어 모두 실패하게 되었다.
1.1 개발 환경 구성 및 문제점
아직 웹 소켓에 대한 개념 이해가 부족하여 HTTPS로 지속적으로 연결 요청이 이루어지는 것에 대해 별 문제가 없다고 판단했다. 이때 다양한 문제점이 발생하였고, 더불어 스트레스 테스트에 대한 필요성을 가장 잘 와닿았던 경험이었던 것 같다. 개발 환경과 문제점의 내용은 아래와 같다.
개발 환경
•
Flask workers
WSGI 설정상 processes(20) x threads(20) 설정으로 진행
•
Clients
100 명 내외
•
Settings
Python Flask + Flask-SocketIO + Apache2 + mod_wsgi
•
Comments
Flask SocketIO를 통해 웹 소켓 통신을 구현하였으나, 101 Upgrade 패킷으로 연결이 되지 않고 지속적인 HTTPS 패킷으로 웹 소켓을 연결하게 되는 현상이 있었음
문제점
•
클라이언트당 지속적인 소켓 연결 요청
웹 소켓 연결이 제대로 이루어지지 않으니 한 Client당 HTTPS로 웹 소켓 연결 요청을 지속적으로 날리게 된다. 이렇게 되면 요청에 대한 적절한 응답을 날리지 못하고 내부 로직이 꼬이게 된다. 무엇보다 Client당 웹 소켓 요청을 반복하다보니 부하가 지나치게 높아지고 Process 점유율도 비정상적으로 높아졌다.
•
프로세스 점유 상태
웹 소켓 연결을 요청할 때, 적절한 응답을 줄 수 없기 때문에 Time-out 상태가 되기 전까지 한 클라이언트가 프로세스를 점유하게 되는 현상이 발생하기도 한다.
이번 포스트에서는 Flask SocketIO를 보다 심도 있게 연구하기 위해 Nginx와 Gunicorn을 이용하여 서비스를 구동하는 방법, 그리고 이를 연구했던 과정을 다루도록 할 것이다.
1.2 참고 자료
Flask SocketIO연구에는 주로 Docs를 참고하여 Upgrade 패킷을 요청하고 처리하는 설정을 참고하였고, 기타 여러 블로그 자료를 참고하였다.
2. Socket IO & Web Socket
2.1 Web Socket 개요
먼저 웹 소켓 서비스를 개발하는 방법을 알아보기 전에, 웹 소켓에 대한 이해할 필요가 있다. 웹 소켓은 W3C에서 관장하는 표준으로, 프로토콜은 IETF(Internet Engineering Task Force) 에서 관장한다. 웹 소켓은 HTTP 요청과 마찬가지로 80번 포트를 통해 웹 서버에 연결하고, HTTP 버전은 1.1이다. 단, 요청 헤더를 살펴보면 Upgrade 헤더를 사용하여 웹 서버에 요청하게 되며, 클라이언트의 브라우저에서 웹 소켓 기능을 지원해야 기능이 정상적으로 제공된다.
GET /... HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Plain Text
복사
웹 소켓을 이용한 통신 방식인 Polling과 WebSocket 방식을 설명하면 다음과 같다.
Polling 방식
클라이언트에서 일정 주기마다 요청을 보내며, 서버는 현재 상태를 바로 응답하는 방식이다. 이때 실시간으로 반영되는 것이 중요한 서비스에는 적합하지 않다. 이는 서버에서 변화가 없더라도 매 요청마다 응답을 내려주기 때문에 불필요한 트래픽이 발생하게 된다.
WebSocket 방식
웹 소켓이란 웹 서버와 웹 브라우저간 실시간 양방향 통신환경을 제공해주는 실시간 통신 기술이다. Polling과 다르게 양방향으로 원할 때 요청을 보낼 수 있으며, stateless한 HTTP에 비해 오버헤드가 적으므로 유용하게 사용할 수 있다.
2.2 WebSocket과 SocketIO의 차이
위와 같은 개념을 이해하고, 웹 소켓을 구현하려고 할 때 Web Socket과 Socket IO의 구분이 어려울 수 있다. 웹 소켓 기능을 구현함에 있어서 두 가지 내용의 구분이 필요하다고 생각된다. 따라서 이 포스트를 작성하면서 참고한 자료와 이해한 내용을 설명하도록 하겠다.
WebSocket
웹 소켓은 양방향 소통을 위한 프로토콜으로써, 서로 다른 컴퓨터끼리 소통하기 위한 약속 정도로 이해하면 된다.
•
HTML5 웹 표준 기술
•
매우 빠르게 작동하며 통신할 때 아주 적은 데이터를 이용함
•
이벤트를 단순히 듣고, 보내는 것만 가능함
Socket.io
socket.io는 양방향 통신을 하기 우해 웹 소켓 기술을 활용하는 라이브러리다. Javascript와 jQuery의 관계와 비슷하다고 할 수 있으며, 속도는 약간 느리지만 보다 많은 편의성을 제공할 수 있다.
•
표준 기술이 아니며, 라이브러리임
•
소켓 연결 실패 시 fallback을 통해 다른 방식으로 알아서 해당 클라이언트와 연결을 시도함
•
방 개념을 이용하여 일부 클라이언트에게만 데이터를 전송하는 브로드캐스팅이 가능함
2. Flask SocketIO
flask_socketio 모듈은 Flask 내에서, 혹은 Flask를 이용하여 웹 소켓 통신을 구현할 수 있도록 하는 라이브러리다. 또한 Socket.io 라이브러리를 활용하여 구현할 수 있어 웹 소켓 통신 기능을 구현하는데 있어서 높은 생산성을 제공해준다.
2.1 install & Getting Started
flask가 설치되었다면 아래와 같이 추가적인 모듈을 설치하고 실행하면 된다.
$ pip3 install flask-socketio
Bash
복사
위와 같이 설치하였다면, 간단하게 Flask SocketIO를 구동하는 예제를 살펴보도록 하겠다.
flask를 이용하여 socketio를 사용하기 위한 간단한 예제는 아래와 같다. 단, 아래의 소스는 간소화된 코드로써, 이것만으로는 원활한 실행은 어려울 수 있다. 따라서 구조를 이해하는 것으로만 살펴보도록 한다.
__init__.py
from flask import Flask, render_template
from flask_socketio import SocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret!'
socketio = SocketIO(app)
if __name__ == '__main__':
socketio.run(app)
Python
복사
javascript
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8">
var socket = io();
socket.on('connect', function() {
socket.emit('my event', {data: 'I\'m connected!'});
});
</script>
JavaScript
복사
2.2 Flask full stack + Flask SocketIO
Flask를 full stack으로 사용하면서 flask socketio를 사용할 때는 위와 같은 방법으로는 한계가 있다. flask를 실행할 때는 socketio.run(app) 과 실행방법으로 실행할 수 없다. 또한 Reverse Proxy으로 서비스하기 위해서는 create_app() 내에 기능을 구현해야 하므로 초기화 형태를 다르게 가져가야 한다. 따라서 create_app() 함수 내에 flask와 함께 초기화할 수 있는 방법을 함께 소개하도록 하겠다.
Flask 내에 Flask SocketIO 실행
flask socketio를 사용하기 위해 파일을 구분하여 아래와 같이 작성한다.
# file : app/sockets/__init__.py
from flask_socketio import SocketIO
socketio = SocketIO()
Python
복사
여기서 핵심적으로 살펴봐야 하는 부분은 socketio.init_app(app) 부분이다. flask를 구동하면서, 동시에 socketio를 app 내에 초기화하기 위한 방법으로 이용할 수 있다.
# file : app/__init__.py
from flask import Flask
from app.sockets import socketio # app/sockets/__init__.py에 선언된 socketio 변수
def create_app():
app = Flask(__name__)
# Socket.IO
socketio.init_app(app) # socketio 변수를 이용하여 app을 초기화
return app
Python
복사
간단하게 flask를 실행해보고 싶다면, 아래와 같이 run.py 파일을 작성하면 좋다. 단, Flask SocketIO 테스트를 정상적으로 해보고 싶다면, 아래와 같읕 코드로 테스트가 불가능하며 임의의 Gunicorn 혹은 uWSGI와 같은 라이브러리를 통해 실행해야 한다.
# file : run.py
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True,
threaded=True,
host="0.0.0.0",
port=8080)
Python
복사
포스트가 길어지게 되어 Nginx + Gunicorn + Flask-SocketIO 구성 방법과 Client 구성, 그리고 Flask 테스트 코드는 다음 글에서 다루도록 하겠다.