본문 바로가기
Back-end/Python

[Python] Flask & Socket.io를 이용한 채팅

by 허도치 2020. 8. 3.
서론

  최근에 깃허브를 둘러보다가 Javascript를 공부하면서 Express.js와 Socket.io 구현했던 간단한 채팅 예제를 발견했다. 예전과 비교해서 얼마나 레벨업이 되었는지 확인할겸 새로운 채팅 앱을 만들어보려고 한다. 다만, 이번에는 웹 어플리케이션 서버를 Express.js 대신 Python의 Flask를 사용할 계획이다.

 

 

 

1. 프로젝트 구성

1-1. 프로젝트 폴더 생성
1
2
3
mkdir flask-chat-simple
 
cd flask-chat-simple
cs

 

1-2. 파일 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
### For Windows
 
# Environment Variables File
type NUL > .env
 
# Web Application File
type NUL > app.py
 
# Socket Namespaces File
type NUL > namespaces.py
 
# Client Directory
mkdir src
 
# Client Files
type NUL > src/index.html
type NUL > src/index.css
type NUL > src/index.js
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
### For Linux/OSX
 
# Environment Variables File
touch .env
 
# Web Application File
touch app.py
 
# Socket Namespaces File
touch namespaces.py
 
# Client Directory
mkdir src
 
# Client Files
touch src/index.html
touch src/index.css
touch src/index.js
cs

 

1-3. 가상환경 생성
1
2
3
4
5
6
7
8
9
# Create virtual ENV.
virtualenv .venv
 
# Activate virtual ENV.
### for Windows
".venv/Scripts/activate"
 
### for Linux/OSX
. .venv/bin/activate
cs

 

1-4. 라이브러리 설치
1
pip install flask python-socketio python-dotenv
cs

 

 

 

2. 환경변수 설정
flask-chat-simple/.env
1
2
FLASK_MODE=development
FLASK_DEBUG=1
cs

 

 

 

3. Server: Flask Application 생성
flask-chat-simple/app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import os
import dotenv
  
from flask import Flask, url_for, render_template
from socketio import Server, WSGIApp
from namespaces import ChatNamespace
 
# Set Constant Variables
ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
STATIC_FOLDER = os.path.join(ROOT_PATH, "src")
TEMPLATE_FOLDER = STATIC_FOLDER
 
# Create Flask Application
app = Flask(
  __name__,
  static_url_path="/static/",
  static_folder=STATIC_FOLDER,
  template_folder=TEMPLATE_FOLDER
)
 
# For reload changed static file.
def dated_url_for(endpoint, **values):
  if endpoint == 'static':
    filename = values.get('filename'None)
    if filename:
      file_path = os.path.join(app.static_folder, filename)
      values['q'= int(os.stat(file_path).st_mtime)
  return url_for(endpoint, **values)
 
@app.context_processor
def override_url_for():
  return dict(url_for=dated_url_for)
 
@app.route('/', methods=["GET""POST"])
def index():
  return render_template("index.html")
 
# Create SocketIO Server
sio = Server(
  async_mode="threading",
  logger=app.logger,
  engineio_logger=app.logger
)
sio.register_namespace(ChatNamespace(sio, '/chat'))
 
# Set SocketIO WSGI Application
app.wsgi_app = WSGIApp(sio, app.wsgi_app)
 
if __name__ == "__main__":
  dotenv.load_dotenv(dotenv_path=".env")
  
  app.run(host="localhost", port=3000, threaded=True)
cs

1. [ 22-32 ln ] - Static 파일 Reload 설정.

   - 정적 파일은 내용이 수정되어도 Server를 재실행하지 않으면 반영되지 않음.

   - 따라서, 정적 파일을 요청할 때마다 파일의 수정일자를 QueryString으로 붙여서 다시 load되도록 설정.

 

2. [ 39-43 ln ] - Socketio Server 객체 생성.

   - [ 40 ln ] - async_mode는 engineio.Server 설정 옵션으로 비동기 처리를 위한 모드 옵션. eventlet이 기본설정이며, gevent, gevent_uwsgi, threading을 사용할 수 있음. 예제에서는 간단하게 사용가능한 threading을 적용.

 

3. [ 44 ln ] - Socketio Server Namespace 설정.

   - Namespace를 설정하는 방법은 크게 두가지가 있음.

   - 첫째, 데코레이터를 사용하는 방법, @sio.on("이벤트명", namespace="/chat")

   - 둘째, socketio.Namespace 클래스를 생성하는 방법.

   - 첫번째 방법을 사용하는게 편하긴하지만 객체지향적으로 작성하는 편이 좀 더 깔끔하기 때문에 예제에서는 두번째 방법을 사용.

 

4. [ 47 ln ] - Socket Server를 WSGI로 변환.

   - Flask, Django로 구현된 WSGI와 통합할 수도 있음.

   - 이번예제에서는 Flask를 사용하므로, Flask WSGI와 Socket Server를 통합.

 

5. [ 52 ln ] - threaded 옵션 설정.

   - async_mode="threading"을 사용하려면, 서버가 Thread를 처리할 수 있어야 하므로 Threaded 옵션 설정.

 

 

 

4. Server: Socket Namespace 생성
flask-chat-simple/namespaces.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from socketio import Namespace
from urllib.parse import urlsplit, parse_qsl 
 
class ChatNamespace(Namespace):
  def __init__(self, sio, namespace, *args, **kwargs):
    super(Namespace, self).__init__(namespace)
    
    self.sio = sio
    self.logger = sio.logger
 
  def on_connect(self, sid, env):
    params = dict(parse_qsl(urlsplit(env["QUERY_STRING"]).path))
    
    self.sio.logger.info(f'{ sid } [SOCKET][CONNECT]')    
    self.sio.logger.info(f'{ sid } [SOCKET][CONNECT][PARAMS] { params }')
    
  def on_message(self, sid, data):
    self.sio.logger.info(f'{ sid } [SOCKET][MESSAGE] { data }')
    
    self.emit("receive_message", data)
  
  def on_disconnect(self, sid):
    self.sio.logger.info(f'{ sid } [SOCKET][DISCONNECT]')
 
cs

1. [ 5~9 ln ] - ChatNamespace 생성자 설정.

   - socketio.Namespace 클래스는 첫번째 인자로 namespace path를 받는데, sio(socketio.Server)객체를 받을 수 있도록 확장.

   - sio는 session을 처리할 때, 필요한데 이번 예제에서는 다루지 않음.

 

2. [ 11-15 ln ] - "connect" Event를 처리하는 Event Handler.

   - Socket Server에 접속하면 내부적으로 "connect" Event를 발생시키는데, 이를 Server와 Client에 정의된 "connect" Event Handler를 통해 Event을 받아서 처리함.

   - [ 12 ln ] - Socket Client에서 데이터와 함께 접속을 시도하려면 URI에 QueryString으로 데이터를 붙여야함.

     예) io("http://localhost:3000/chat?username=dochi")

 

3. [ 17-20 ln ] - "message" emit Event를 처리하는 Event Handler.

   - Client로부터 "message" Events를 받아서 Namespace에 접속한 모든 Client에게 "receive_message" Event를 보냄.

 

4. [ 22-23 ln ] - "disconnect" Event를 처리하는 Event Handler.

 

 

 

5. Client: HTML 생성
flask-chat-simple/src/index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
html<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Socket App</title>
    <link href="{{ url_for('static', filename='index.css') }}" rel="stylesheet">
    <script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.10.4/polyfill.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js" integrity="sha256-yr4fRk/GU1ehYJPAs8P4JlTgu0Hdsp4ZKrx8bDEDC3I=" crossorigin="anonymous"></script>
  </head>
  <body>
    <div id="root">
      <div class="chat-container">
        <div class="scroll-wrapper">
          <ul class="chat-msg-list"></ul>
        </div>
        <div class="chat-msg-controller">
          <form id="message_form">
            <input type="text" name="message" placeholder="message">
            <button type="submit">send</button>
          </form>
        </div>
      </div>
    </div>
    <script src="{{ url_for('static', filename='index.js') }}"></script>
  </body>
</html>
cs

1. [ 7, 26 ln ] - Flask의 탬플릿인 Jinja2의 문법.

 

2. [ 8-10 ln ] - CDN 설정.

   - [ 8-9 ln ] - 최신 Javascript 문법을 사용하기위한 Babel CDN 설정.

   - [ [ 10 ln ] - Socket.io CDN 설정.

 

3 [ 13~25 ln ] - 채팅을 위한 Element 설정.

 

 

 

6. Client: Socket Client 생성
flask-chat-simple/src/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Create Socket Instance
const socket = io("http://localhost:3000/chat?username=Dochi");
 
// On Event, Wait for 'connect' emit.
socket.on("connect", ()=>{
  console.log("[connect]");
});
 
// On Event, Wait for 'disconnect' emit.
socket.on("disconnect", ()=>{
  console.log("[disconnect]");
});
 
// On Event, Wait for 'receive_message' emit.
socket.on("receive_message", ({ username, message })=>{
  const text = [username, message].join(":");
  
  const div_wrapper = document.querySelector(".chat-container > .scroll-wrapper");
  const ul_msg_list = div_wrapper.querySelector(".chat-msg-list");
  const li_msg_item = ul_msg_list.appendChild(document.createElement("li"));
  
  li_msg_item.appendChild(document.createTextNode(text));
  
  document.querySelector(".scroll-wrapper").scrollTop = div_wrapper.scrollHeight;
});
 
// Emit Event, Emit message data.
const send_message = ( data )=>{
  socket.emit("message", data);
}
 
// Find Message Form Element
const message_form = document.getElementById("message_form");
 
// Send Message Handler
const handle_send_message = ( event )=>{
  event.preventDefault();
    
  // Get Input Message
  const input_message = event.target.querySelector('input[name="message"]');
  const text_message = input_message.value;
 
  // Send Message Data
  send_message({
    username: "Dochi",
    message: text_message
  });
 
  // Initialize
  input_message.value = "";
}
message_form.addEventListener("submit", handle_send_message, false);
cs

1. [ 2 ln ] - Socket Client 객체 생성.

   - usage: io( URI, [options] )

   - 자세한 설정 옵션은 socket.io 공식 문서를 참조.

 

2. [ 5-25 ln ] - Event Handler 설정.

   - connect, disconnect는 접속, 접속해제시 발생되는 Event를 처리.

   - [ 15-25 ln ] - message 데이터를 받아서 Element를 생성하고 화면에 렌더링하는 Event Handler.

 

3. [ 28-30 ln ] - Event Emitter 설정.

   - Socket Server로 "message" Event를 보내는 Emitter.

 

4. [ 33-52 ln ] - Form에 입력된 message를 처리하는 로직.

   - 입력된 message를 Emitter로 Server에 전송.

 

 

 

7. Client: CSS 생성
flask-chat-simple/src/index.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
html, body { 
  padding: 0;
  margin: 0;
  height: 100%;
  width: 100%
}
.chat-container {
  position: relative; 
  height: 350px;
  padding: 2px;
}
.scroll-wrapper { 
  position: relative; 
  height: 100%; 
  box-sizing: border-box; 
  border: 1px solid darkgrey;
  overflow-y: scroll;
}
.chat-msg-list { 
  position: absolute; 
  bottom: 0; 
  margin: 0;
  list-style: none; 
  padding: 1px; 
  width: 100%; 
  max-height: 100%; 
  box-sizing: border-box; 
}
.chat-msg-item { 
  padding: 3px 3px; 
  overflow-wrap: break-word;
}
cs

 

 

 

8. 실행
python app.py

 

 

 

9. 실행 결과

 

 

 

마치며

  이렇게 간단한게 Flask와 Socket.io로 채팅을 구현해보았다. 확실히 처음 만들어봤을 때에 비해서 라이브러리에 대한 이해도가 높아서인지 뚝딱 만들어낼 수 있었다. 지금은 웹 브라우저를 통해 채팅할 수 있도록 구현하였는데, 추후에 PyQt5를 이용하여 웹과 응용프로그램에서 채팅을 할 수 있도록 구현해봐야겠다.

댓글