Web/Flask-python

웹소켓 in Flask

웹소켓을 쓰는 상황?

HTTP - 클라이언트 웹 브라우저 요청에 의해 서버가 응답하고, 이를 가지고 다시 클라이언트를 갱신 - 매 요청시마다 페이지 전체를 갱신하는 "동기적" 문제 발생. 즉, 통신할 때 마다 페이지 전체를 새로 랜더링해야하는것.

AJAX - 동기적 문제 해결을 위해 클라이언트 XMLhttpRequest 객체의 요청에 의한 서버 응답을 받아 페이지를 갱신하지만, 페이지를 이동하거나 전체를 갱신하는것이 아닌 페이지의 일부를 구성하는 DOM을 갱신할 수 있게됨.

그러나 여전히 요청에 의해 서버의 응답을 받아 페이지를 갱신한다는 문제점은 여전함.

웹소켓을 이용하면 클라이언트의 별도 요청없이도 서버에서 보내는 데이터를 받아 페이지에 표현할 수 있음.

클라이언트의 요청 - 서버의 데이터 응답 의 방식이 아닌 서버의 데이터 전송 이 된다.

일회성의 통신만 제공하는 ajax와는 달리 , 웹소켓은 한번 열어두면 단방향이건 양방항이건 닫기 전까지 계속해서 통신이 가능함.


flask에서 웹소켓 사용

pip install flask-socketio

1. socketio 시작 + app 등록

flask에서 socketio를 사용하게 되면 기존의 app.run()이 아니라 socketio.run()으로 서버를 실행하게 된다. 이렇게 되면 기존 Flask의 Werkzeug 서버가 아닌, websocket을 지원하는 evenlet이나 gevent 웹서버가 실행된다.

init.py에 아래 코드를 추가해주자. socketio 객체를 선언하면서 여타 다른 확장 라이브러리를 등록하듯이 init_app() 함수를 실행하여 app 객체에 등록해준다.

#__init__.py
from flask_socketio import SocketIO
~~~
socketio = SocketIO(logger=True,engineio_logger=True)
~~~
def create_app():
~~~
    socketio.init_app(app)

2.run 수정

그 다음, 직접적인 실행에 관련된 파일(ex.manage.py)에서 앱 생성을 담당하는 코드인 create_app()을 실행하는 부분이 있다면 app대신에 socketio으로 run()함수를 실행할 객체를 교체해주면 된다.

app = create_app()
manager = Manager(app)

@manager.command
def run():
    # app.run(host='0.0.0.0',port='80',debug=True)
    socketio.run(app,port=80,debug=True)

if __name__ == "__main__" : 
    manager.run()

3.socket event 생성

socket 이벤트 발생시 실질적인 동작을 수행하는 코드가 작성되어야 하는데, 여기서는 events.py 라는 별개 파일로 분리하여 관리한다.

해당 코드들이 클라이언트와 데이터를 주고받고 통신하는 역할을 담당한다.

#events.py
from flask import session 
from flask_socketio import emit

def socketio_init(socketio):
    @socketio.on('testSocket',namespace='/test')
    def testEvent(message):
        tsession = session.get('test')
        print('received message'+str(message))
        retMessage = { 'msg' : "hello response" }
        emit('test',retMessage,callback=tsession)

하나씩 나눠보면, socketio_init 함수를 선언하여 웹소켓으로 통신하는 sokectio 객체들을 관리한다. route 규칙과 유사하게 socketio.on() 으로 선언하여 들어오는 경로와 이벤트 이름을 처리하는데, /testSocket 이벤트 ****수신시에 testEvent 함수가 실행된다.

넘어온 인자는 message로, testSession 이라는 이름으로 저장된 세션은 tsession으로 생성되어 관리되고, emit() 으로 클라이언트에 데이터를 전송할 수 있다.

emit 함수의 첫째인자는 클라이언트에게 발생시킬 이벤트 이름이고, 두번째 인자는 넘길 데이터, 세번째 인자는 해당 이벤트를 송신할 대상인데 여기서는 세션이 되겠다.

이렇게 하면 해당 이벤트 실행시에 서버에서는 "received message+수신 메시지" 를 출력하고 클라이언트에 "hello response" 데이터를 보낼것이다. 세번째 인자가 비어있다면 현재 연결이 수립된 모든 세션에 데이터가 전송된다.

4.socket event 등록

이제 생성한 이벤트를 **init.py** 파일에서 app에 등록해주면 된다.

def create_app():
~~~~~

    from app.events import socketio_init
    socketio_init(socketio)

이렇게 하면 route와 같은 원리로 event에 정의된 함수들이 동작하게 된다.

(객체지향형으로 class를 선언하는 경우에는 다른 방식을 사용해야한다. )

이 부분이 빠지면 event가 프로젝트에 아예 존재하지 않는 셈이 되어버린다.

5.route 작성

클라이언트는 서버의 특정 URL로 요청을 보낼것이니, 해당 URL을 처리할 route를 작성해주어야 한다.

#main_view.py
@bp.route('/test')
def test():
    if request.method == 'POST' : 
        session['testSession'] = request.form['testSession']
        testSession = session.get('testSession','')
        return render_template('index.html',testSession=testSession)        
    else :
        return render_template('index.html')

/test 경로로 URL 요청이 들어올 때 POST 인자가 있다면, testSession 이름에 있는 인자값을 동일 명칭의 세션에 저장하고 이를 index.html에 넘겨준다.

  • 이때 socketio와 engineio의 버전을 체크해봐야 한다. 특정 버전끼리 맞춰줘야 정상동작함.
pip install --upgrade python-socketio==4.6.0
pip install --upgrade python-engineio==3.13.2
pip install --upgrade Flask-SocketIO==4.3.1

6.웹소켓 생성

이제부터는 클라이언트 단에서 이뤄지는 동작들이다.

웹소켓을 생성하여 웹 페이지가 로드될 때 실행해 해당 서버와 소켓 연결을 수행한다.

$(document).on('click','#socketTest',function(){
    var socket = io.connect('http://127.0.0.1/test');
    socket.on('connect',function(ret){
        console.log("connected");
    });

    socket.on('test',function(ret){
        console.log("server told : "+ret.msg);
    });

    socket.emit('testSocket',{
            message : "test socket message"
    });
});

io.connect() 함수로 연결할 URL을 지정하면 소켓 연결이 수립되고, 서버측 콘솔에서 GET /socket.io/?EIO=3&transport=polling&t=Nmh2pjO HTTP/1.1 같은 로그가 보일것이다. 연결 수립 이후엔 주기적으로 ping pong을 주고받으며 연결을 유지하려 한다.

socket.on() 함수의 첫번째 인자는 이벤트 이름으로, ‘connect’, ‘disconnect’, ‘message’, ‘json’은 socketIO에 의해 생성된 특수 이벤트로써 특정 조건에만 동작하고, 그 외 이름들은 사용자 정의 이벤트로 간주되어 지정한 이름대로 동작을 실행한다.

#python console
received event "testSocket" from 51cbc48b8d6b4cc1bbc5cb7524ac0515 [/test]
received message{'message': 'test socket message'}
~~~
#web console 
connected
server told : hello response

동작 실행 결과 . 웹 브라우저의 socket.emit 함수가 실행되면서 /test 하위 경로의 testSocket 이벤트에 메시지가 전달되었고,

서버의 testEvent 함수속 emit 함수가 실행되며 웹 브라우저의 test 이벤트에 메시지가 전달되었다.

이렇게 수립된 소켓 연결은 계속 핑퐁이 오가면서 유지되므로, 서버측에서 지속적으로 이벤트를 전달하는게 가능해진다.


정리

flask에서 세션을 기반으로 한 웹소켓의 구조는 아래와 같다

  1. 웹 페이지 접속시 소켓 통신의 대상을 특정할 세션 생성
  2. 클라이언트에서 io.connect() 함수 실행하여 소켓 연결
  3. 클라이언트로부터 서버에 @socketio.on() 에 정의된 이벤트에 emit 하거나 서버로부터 클라이언트의 socket.on() 에 정의된 이벤트에 emit 하여 소켓 통신 진행

이때 3번에서 emit을 사용할때 evnets.py의 함수 내에 정의된 경우라면 그냥 emit()을 단독으로 사용해도 되지만, 그렇지 않은 경우는 from app import socketio 로 socketio 객체를 받아온 다음 socketio.emit() 으로 브라우저의 이벤트를 호출해야 한다.

또, emit시에 대상을 특정하지 않았다면 현재 연결되어있는 모든 클라이언트 소켓에 동일한 응답이 가므로 필요에 따라 이를 조절할 필요가 있다. 이렇게 한다면 1번의 세션을 특정하기 위해 생성하는 과정이 불필요하다.

추가로, 동작의 순서를 보다 정확하게 조절하고 싶다면 서버측에서 sleep() 함수 등을 이용해 socket.on() 이벤트 호출 시간에 지연을 걸어주는게 좋다.

def eaxmple():
~~~
    ret = { 
            'name' : name,
            'id' : id,
            'path' : path.replace('\\','%5C')
        }
    sleep(3)
    socketio.emit('waitEvent',ret)

참고 문서

https://ljs93kr.tistory.com/58

https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&blogId=shino1025&logNo=222179697262&parentCategoryNo=&categoryNo=33&viewDate=&isShowPopularPosts=true&from=search

https://bokyeong-kim.github.io/python/flask/2020/05/09/flask(1).html

https://yumere.tistory.com/53