본문으로 바로가기

서버에서 프로그램을 실행시켜 실행되는 프로그램이 출력하는 문장을 클라이언트에게 전송하는 기능을 구현중이었다.

클라이언트와 연결되면 특정 프로그램을 실행시키고 해당 프로그램이 출력문을 뱉을 때 마다 send()함수를 이용하여 클라이언트로 출력문을 송신하도록 했다. 즉, 한 이벤트 핸들러 안에서 반복문을 이용하여 매 반복마다 데이터를 송신하도록 했다.

@socketio.on('connect')
def connect_event():
    p = redirect_test.run_file('./root/redirect.py')
    while p.poll() == None:
        out = p.stdout.readline()
        socketio.send({"data":out},json=True)

그러나 매 루프마다 데이터가 보내지지 않고 while문이 종료된 후 한번에 데이터가 보내졌다.

이 문제를 해결하기 위해 여러 시행착오를 겪어 글로 남기게 되었다.

 

0.flask-socketio에서 emit() 함수의 동작 방식

데이터를 send()하거나 emit()하게 되면 데이터는 클라이언트에게 바로 송신되지 않고 버퍼에 저장한 후 이벤트가 끝나면 버퍼에 있는 내용을 클라이언트에게 송신한다. 따라서 나는 데이터가 버퍼에 쌓이게 하지 않고 바로 클라이언트에게 송신하도록 해야된다.

 

1. eventlet 사용하기

@socketio.on('connect')
def connect_event():
    p = redirect_test.run_file('./root/redirect.py')
    while p.poll() == None:
        out = p.stdout.readline()
        socketio.send({"data":out},json=True)
        eventlet.sleep(0)

eventlet.sleep() 함수를 사용하면 버퍼에 있는 내용을 flush할 수 있다.

따라서 send() 함수를 호출한 뒤 eventlet.sleep() 함수를 호출하게 되면 버퍼를 거치지 않고 바로 클라이언트에게 송신하는 것처럼 작동한다.

그러나 예상처럼 작동하지 않았고 똑같이 버퍼에 쌓인 후 클라이언트에게 송신되었다.

 

2.monkey.patch_all()

from gevent import monkey
monkey.patch_all()

gevent의 monkey 패치는 자동으로 코루틴을 생성하여 비동기적으로 프로그램이 동작하게 한다. 그러나 이 방법 또한 똑같이 버퍼에 쌓인 후 클라이언트에게 송신되었다.

 

3. 해결방법

@socketio.on('send')
def send_me(mes):
    print(mes)
    p = redirect_test.run_file('./root/redirect.py')
    while p.poll() == None:
        out = p.stdout.readline()
        print(out, end='')
        socketio.send({"data":out},json=True)
        socketio.sleep(0)

참 어이없게 해결했다.. 인터넷에 나오는 방법을 다 해봐도 해결되지 않다가 데이터를 보내는 코드를 connect 이벤트가 아닌 다른 이벤트로 변경하여 해결했다. 물론 버퍼에 있는 내용을 flush해주는 코드가 필요했다. 따라서 evnetlet과 마찬가지로 socketio.sleep(0)도 버퍼에 있는 내용을 flush하므로 추가적인 라이브러리가 필요한 eventlet 대신 socketio.sleep(0)를 사용했다.

코드를 뜯어보진 않았지만 아마 connect 이벤트는 이벤트가 모두 종료된 후에 클라이언트에게 데이터를 송신하는 방식인 것 같다.

 

결론적으로 flask-socketio에서 버퍼링 없이 데이터 보내기 위해서는 먼저 flask에서 만들어놓은 특별한 이벤트가 아닌 이벤트여야 하며, socketio.sleep(0) 혹은 eventlet.sleep(0)과 같이 버퍼에 있는 내용을 flush 해주는 코드를 send나 emit 코드 이후에 작성해 주면 된다.

 

아래는 클라이언트가 버튼을 클릭하면 서버에서 데이터를 연속적으로 보내주는 코드이다.

server.py

from flask import Flask
from flask_socketio import SocketIO, send
from flask_cors import CORS
from root import redirect_test


app = Flask(__name__)
CORS(app)
socketio = SocketIO(app,  cors_allowed_origins="*")

@app.route('/')
def hello_world():
    return "."

@socketio.on('example')
def example_send(msg):
    print(msg)
    for i in range(10):
        socketio.send(str(i))
        socketio.sleep(0.5)
        
@socketio.on('message')
def handle_message(data):
    print('received message: ' + data)

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0', port=5000)

 

client.js

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tutorial on Flask: A Web socket example</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.1.2/socket.io.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
    var socket = io.connect('http://IP:PORT');

    socket.send("socket is connected");
    socket.on('message', (msg) => {
        console.log(msg)
        document.getElementById("log").innerText = msg;
    });

    $('#sendbutton').on('click', function(){
        socket.emit("example","button click!");
    });
});


</script>
</head>
</head>
<body>
<h1>Example on SocketIO</h1>
<h2>Message Received:</h2>
<div id="log"></div>
<button id="sendbutton">Send</button>
</body>
</html>