Jelajahi Sumber

add Flask backend 🎉

wangweimin 5 tahun lalu
induk
melakukan
f464ae924d
2 mengubah file dengan 152 tambahan dan 1 penghapusan
  1. 136 0
      pywebio/platform/flask.py
  2. 16 1
      pywebio/utils.py

+ 136 - 0
pywebio/platform/flask.py

@@ -0,0 +1,136 @@
+"""
+Flask backend
+
+"""
+import asyncio
+import threading
+import time
+from functools import partial
+from typing import Dict
+
+from flask import Flask, request, jsonify, send_from_directory
+
+from . import STATIC_PATH
+from ..framework import WebIOSession
+from ..utils import random_str, LRUDict
+
+# todo Flask 的线程模型是否会造成竞争条件?
+_webio_sessions: Dict[str, WebIOSession] = {}  # WebIOSessionID -> WebIOSession()
+_webio_expire = LRUDict()  # WebIOSessionID -> last active timestamp
+
+DEFAULT_SESSION_EXPIRE_SECONDS = 60 * 60 * 4  # 超过4个小时会话不活跃则视为会话过期
+REMOVE_EXPIRED_SESSIONS_INTERVAL = 120  # 清理过期会话间隔(秒)
+
+_event_loop = None
+
+
+def _make_response(webio_session: WebIOSession):
+    res = webio_session.unhandled_server_msgs
+    webio_session.unhandled_server_msgs = []
+    return jsonify(res)
+
+
+def _remove_expired_sessions(session_expire_seconds):
+    while _webio_expire:
+        sid, active_ts = _webio_expire.popitem(last=False)
+        if time.time() - active_ts < session_expire_seconds:
+            _webio_expire[sid] = active_ts
+            _webio_expire.move_to_end(sid, last=False)
+            break
+        del _webio_sessions[sid]
+
+
+_last_check_session_expire_ts = 0  # 上次检查session有效期的时间戳
+
+
+def _remove_webio_session(sid):
+    del _webio_sessions[sid]
+    del _webio_expire[sid]
+
+
+def _webio_view(coro_func, session_expire_seconds):
+    """
+    todo use cookie instead of session
+    :param coro_func:
+    :param session_expire_seconds:
+    :return:
+    """
+    global _last_check_session_expire_ts, _event_loop
+    if _event_loop:
+        asyncio.set_event_loop(_event_loop)
+
+    webio_session_id = None
+    if 'webio_session_id' not in request.cookies:  # start new WebIOSession
+        webio_session_id = random_str(24)
+        webio_session = WebIOSession(coro_func)
+        _webio_sessions[webio_session_id] = webio_session
+        _webio_expire[webio_session_id] = time.time()
+    elif request.cookies['webio_session_id'] not in _webio_sessions:  # WebIOSession deleted
+        return jsonify([dict(command='close_session')])
+    else:
+        webio_session_id = request.cookies['webio_session_id']
+        webio_session = _webio_sessions[webio_session_id]
+
+    if request.method == 'POST':  # client push event
+        webio_session.send_client_msg(request.json)
+
+    elif request.method == 'GET':  # client pull messages
+        pass
+
+    if time.time() - _last_check_session_expire_ts > REMOVE_EXPIRED_SESSIONS_INTERVAL:
+        _remove_expired_sessions(session_expire_seconds)
+        _last_check_session_expire_ts = time.time()
+
+    response = _make_response(webio_session)
+    if webio_session.closed():
+        _remove_webio_session(webio_session_id)
+    elif 'webio_session_id' not in request.cookies:
+        response.set_cookie('webio_session_id', webio_session_id)
+    return response
+
+
+def webio_view(coro_func, session_expire_seconds):
+    """获取Flask view"""
+    view_func = partial(_webio_view, coro_func=coro_func, session_expire_seconds=session_expire_seconds)
+    view_func.__name__ = 'webio_view'
+    return view_func
+
+
+def _setup_event_loop():
+    global _event_loop
+    _event_loop = asyncio.new_event_loop()
+    _event_loop.set_debug(True)
+    asyncio.set_event_loop(_event_loop)
+    _event_loop.run_forever()
+
+
+def start_flask_server(coro_func, port=8080, host='localhost', disable_asyncio=False,
+                       session_expire_seconds=DEFAULT_SESSION_EXPIRE_SECONDS,
+                       debug=False, **flask_options):
+    """
+
+    :param coro_func:
+    :param port:
+    :param host:
+    :param disable_asyncio: 禁用 asyncio 函数。在Flask backend中使用asyncio需要单独开启一个线程来运行事件循环,
+        若程序中没有使用到asyncio中的异步函数,可以开启此选项来避免不必要的资源浪费
+    :param session_expire_seconds:
+    :param debug:
+    :param flask_options:
+    :return:
+    """
+    app = Flask(__name__)
+    app.route('/io', methods=['GET', 'POST'])(webio_view(coro_func, session_expire_seconds))
+
+    @app.route('/')
+    def index_page():
+        return send_from_directory(STATIC_PATH, 'index.html')
+
+    @app.route('/<path:static_file>')
+    def serve_static_file(static_file):
+        return send_from_directory(STATIC_PATH, static_file)
+
+    if not disable_asyncio:
+        threading.Thread(target=_setup_event_loop, daemon=True).start()
+
+    app.run(host=host, port=port, debug=debug, **flask_options)

+ 16 - 1
pywebio/utils.py

@@ -1,4 +1,6 @@
-import random, string
+import random
+import string
+from collections import OrderedDict
 
 
 def random_str(len=16):
@@ -7,3 +9,16 @@ def random_str(len=16):
     :param int len: 字符串长度
     """
     return ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(len))
+
+
+class LRUDict(OrderedDict):
+    """
+    Store items in the order the keys were last recent updated.
+
+    The last recent updated item was in end.
+    The last furthest updated item was in front.
+    """
+
+    def __setitem__(self, key, value):
+        OrderedDict.__setitem__(self, key, value)
+        self.move_to_end(key)