Flask源码篇:wsgi、Werkzeug与Flask启动工作流程

目录

  • 1 wsgi介绍
  • 2 使用wsgi实现一个web应用
  • 3 Werkzeug介绍
  • 4 Flask工作流程分析
    • (1)创建Flask app
    • (2)启动Falsk app
    • (3)分析run_simple方法
    • (4)分析make_server方法
    • (5)分析BaseWSGIServer类
    • (6)分析HTTPServer类
    • (7)分析serve_forever方法
    • (8)分析WSGIRequestHandler类
  • 5 Flask工作流程总结

源码系列:

Flask源码篇:Flask路由规则与请求匹配过程
Flask源码篇:2w字彻底吃透Flask是上下文原理

前面介绍Flask的时候提到过wsgi和Werkzeug,下面来从源码详细看下两个到底是什么,和Flask的工作流程有什么关系。

如果不想看过程分析,可以看最后的总结,还有流程图,也可以一看就懂!

1 wsgi介绍

wsgi全称为Web Server Gateway Interface,是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。类似于Java语言中的Servlet。

实际上wsgi是一种协议或规范,它规范了web服务器如何与Flask等python web服务器进行交互和通信。

通常,一个生产环境完整的web请求有如下流程:

在这里插入图片描述

实际上,在我们使用wsgi时,并不是一个单独的网关服务器,通常是继承在python后端服务器中。很多后端框架,比如Flask、Django等都自己实现了。其中Flask就是自己实现了Werkzeug(根据wsgi进一步抽象了)。

WSGI的主要作用是 Web Server 和 Python Application 之间的桥梁,或者翻译官,让两者能够无障碍沟通。可以归纳为以下几点:

  1. Web服务器的责任在于监听和接收请求。在处理请求的时候调用WSGI提供的标准化接口,将请求的信息转给WSGI
  2. WSGI的责任在于“中转”请求和响应信息。WSGI接收到Web服务器提供的请求信息后可以做一些处理,之后通过标准化接口调用Web应用,并将请求信息传递给Web应用。同时,WSGI还将会处理Web应用返回的响应信息,并通过服务器返回给客户端;
  3. Web应用的责任在于接收请求信息,并且生成响应。

2 使用wsgi实现一个web应用

WSGI 通常有以下协议:

每个 python web 应用都是一个可调用(callable)的对象,函数或者一个带有__call__方法的类。__call__方法有2个参数:第一个参数是WSGI的environ,第二个参数是一个start_response函数。environ 包含了请求的所有信息,都是一些键值对,要么是提供给server,要么提供给middleware。start_response 是 application 处理完之后需要调用的函数,参数是状态码、响应头部还有错误信息。

application 还有个非常重要的特点是:**它是可以嵌套的。**换句话说,我可以写个 application,它做的事情就是调用另外一个 application,然后再返回(类似一个 proxy)。一般来说,嵌套的最后一层是业务应用,中间就是 middleware。这样的好处是,可以解耦业务逻辑和其他功能,比如限流、认证、序列化等都实现成不同的中间层,不同的中间层和业务逻辑是不相关的,可以独立维护;而且用户也可以动态地组合不同的中间层来满足不同的需求。

基于以上特点,我们实现一个简单的python后端服务(功能上相当于Falsk),代码如下:

# 定义我们自己的python 应用
def application(environ, start_response):status = '200 OK'response_headers = [('Content-type', 'text/plain')]start_response(status, response_headers)return [b"hello word"]

application 是一个函数,肯定是可调用对象,然后接收两个参数,两个参数分别是:environ和start_response

  • environ是一个字典,里面储存了HTTP request相关的所有内容,比如header、请求参数等等
  • start_response是一个WSGI 服务器传递过来的函数,用于将response header,状态码传递给Server。

调用 start_response 函数负责将响应头、状态码传递给服务器, 响应体则由application函数返回给服务器, 一个完整的http response 就由这两个函数提供。

下面使用python自带的wsgi包实现一个wsgi网关服务器:

from wsgiref.simple_server import make_server
server = make_server('localhost', 8080, application)
server.serve_forever()

一个完整的python web后端服务器代码如下:

# wsgi使用from wsgiref.simple_server import make_server# 定义我们自己的python 应用
def application(environ, start_response):status = '200 OK'response_headers = [('Content-type', 'text/plain')]start_response(status, response_headers)return [b"hello word"]if __name__ == "__main__":server = make_server('localhost', 8080, application)server.serve_forever()

启动后,访问127.0.0.1:8080,即可得到响应hello word。

以上就是使用自己定义的python 可调用对象application和自带的wsgi包实现了一个非常简单web应用。

3 Werkzeug介绍

Werkzeug就是一个WSGI工具包,他可以作为一个Web框架的底层库。前面我们说过,Flask主要用到了Werkzeug和jinja2两个库。

其中Werkzeug提供了Flask很核心的功能:

  1. 请求和响应对象:提供了RequestResponseRequest可以包装WSGI服务器传入的environ参数,并对其进行进一步的解析,以使我们更容易的使用请求中的参数。Response可以根据传入的参数,来发起一个特定的响应。
  2. 路由解析:Werkzeug提供了强大的路由解析功能。比如Flask框架中经常用到的RuleMap类等。
  3. 本地上下文:在许多Web程序中,本地上下文是个非常重要的概念。而实现本地上下文需要用到不同线程间数据的隔离。werkzeug.local中定义了LocalLocalStackLocalProxy等类用于实现全局数据的隔离。

Werkzeug还提供了很多工具,例如WSGI中间件、HTTP异常类、数据结构等。

后面还会详细介绍一些主要功能。

4 Flask工作流程分析

注:以下分析会着重通过源码分析Flask启动过程,Flask版本:2.0.2。

以一个最简单Flask应用来举例,代码如下:

from flask import Flaskapp = Flask(__name__)@app.route('/')
def hello_world():return 'Hello World!'if __name__ == '__main__':app.run()

下面来分析下,Flask应用启动时到底发生了什么。

(1)创建Flask app

首先app = Flask(__name__)代码创建了一个Flask对象。它实现了wsgi应用,并且是扮演了最核心的角色。

第一个参数__name__是模块或包的名字,一般使用这个就好,也可以自己定义名字。

(2)启动Falsk app

接着看最后一行app.run()run() 方法的定义,调用了werkzeug库中的一个 run_simple() 方法,最后启动了 BaseWSGIServer 服务器。

我们看下源码:

def run(self,host: t.Optional[str] = None,port: t.Optional[int] = None,debug: t.Optional[bool] = None,load_dotenv: bool = True,**options: t.Any,
) -> None:# 如果从命令行启动if os.environ.get("FLASK_RUN_FROM_CLI") == "true":from .debughelpers import explain_ignored_app_runexplain_ignored_app_run()return# 加载配置if get_load_dotenv(load_dotenv):cli.load_dotenv()# if set, let env vars override previous valuesif "FLASK_ENV" in os.environ:self.env = get_env()self.debug = get_debug_flag()elif "FLASK_DEBUG" in os.environ:self.debug = get_debug_flag()# debug passed to method overrides all other sourcesif debug is not None:self.debug = bool(debug)server_name = self.config.get("SERVER_NAME")sn_host = sn_port = Noneif server_name:sn_host, _, sn_port = server_name.partition(":")# 下面结果if是设置host、port参数默认值if not host:if sn_host:host = sn_hostelse:host = "127.0.0.1"if port or port == 0:port = int(port)elif sn_port:port = int(sn_port)else:port = 5000options.setdefault("use_reloader", self.debug)options.setdefault("use_debugger", self.debug)options.setdefault("threaded", True)cli.show_server_banner(self.env, self.debug, self.name, False)from werkzeug.serving import run_simpletry:# 这一行是核心,调用app.run方法,实际上是执行了werkzeug服务的run_simple方法run_simple(t.cast(str, host), port, self, **options)finally:self._got_first_request = False

可以看到,run方法有host、post、debug、load_dotenv等参数。load_dotenv主要用来指定配置文件,加载配置的。

启动falsk app的核心就是调用werkzeug库中的一个run_simple()方法,同时传递了参数。

(3)分析run_simple方法

run_simple方法源码如下:

def run_simple(hostname: str,port: int,application: "WSGIApplication",use_reloader: bool = False,use_debugger: bool = False,use_evalex: bool = True,extra_files: t.Optional[t.Iterable[str]] = None,exclude_patterns: t.Optional[t.Iterable[str]] = None,reloader_interval: int = 1,reloader_type: str = "auto",threaded: bool = False,processes: int = 1,request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None,static_files: t.Optional[t.Dict[str, t.Union[str, t.Tuple[str, str]]]] = None,passthrough_errors: bool = False,ssl_context: t.Optional[_TSSLContextArg] = None,
) -> None:"""Start a WSGI application. Optional features include a reloader,multithreading and fork support.This function has a command-line interface too::python -m werkzeug.serving --help以下参数介绍省略"""if not isinstance(port, int):raise TypeError("port must be an integer")if use_debugger:from .debug import DebuggedApplicationapplication = DebuggedApplication(application, use_evalex)if static_files:from .middleware.shared_data import SharedDataMiddlewareapplication = SharedDataMiddleware(application, static_files)def log_startup(sock: socket.socket) -> None:all_addresses_message = (" * Running on all addresses.\n""   WARNING: This is a development server. Do not use it in"" a production deployment.")if sock.family == af_unix:_log("info", " * Running on %s (Press CTRL+C to quit)", hostname)else:if hostname == "0.0.0.0":_log("warning", all_addresses_message)display_hostname = get_interface_ip(socket.AF_INET)elif hostname == "::":_log("warning", all_addresses_message)display_hostname = get_interface_ip(socket.AF_INET6)else:display_hostname = hostnameif ":" in display_hostname:display_hostname = f"[{display_hostname}]"_log("info"," * Running on %s://%s:%d/ (Press CTRL+C to quit)","http" if ssl_context is None else "https",display_hostname,sock.getsockname()[1],)def inner() -> None:try:fd: t.Optional[int] = int(os.environ["WERKZEUG_SERVER_FD"])except (LookupError, ValueError):fd = Nonesrv = make_server(hostname,port,application,threaded,processes,request_handler,passthrough_errors,ssl_context,fd=fd,)if fd is None:log_startup(srv.socket)# 这里是inner函数的核心,调用了wsgi的make_server创建了一个wsgi服务器,并且执行serve_forever方法srv.serve_forever()if use_reloader:if not is_running_from_reloader():if port == 0 and not can_open_by_fd:raise ValueError("Cannot bind to a random port with enabled ""reloader if the Python interpreter does ""not support socket opening by fd.")address_family = select_address_family(hostname, port)server_address = get_sockaddr(hostname, port, address_family)s = socket.socket(address_family, socket.SOCK_STREAM)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)s.bind(server_address)s.set_inheritable(True)if can_open_by_fd:os.environ["WERKZEUG_SERVER_FD"] = str(s.fileno())s.listen(LISTEN_QUEUE)log_startup(s)else:s.close()if address_family == af_unix:server_address = t.cast(str, server_address)_log("info", "Unlinking %s", server_address)os.unlink(server_address)from ._reloader import run_with_reloader as _rwr# 这里是主要代码,把inner函数传递给run_with_reloader函数,开启一个线程执行inner_rwr(inner,extra_files=extra_files,exclude_patterns=exclude_patterns,interval=reloader_interval,reloader_type=reloader_type,)else:inner()

我们只看最核心的代码部分即可。

可以看到方法里定义了一个非常重要的函数inner()use_reloader这个参数也是从run方法传递过来的,默认为False。

如果use_reloader为True,最后流程会走到执行run_with_reloader方法,及run_with_reloader(inner, extra_files, reloader_interval, reloader_type)

run_with_reloader代码如下:

def run_with_reloader(main_func: t.Callable[[], None],extra_files: t.Optional[t.Iterable[str]] = None,exclude_patterns: t.Optional[t.Iterable[str]] = None,interval: t.Union[int, float] = 1,reloader_type: str = "auto",
) -> None:"""Run the given function in an independent Python interpreter."""import signalsignal.signal(signal.SIGTERM, lambda *args: sys.exit(0))reloader = reloader_loops[reloader_type](extra_files=extra_files, exclude_patterns=exclude_patterns, interval=interval)try:if os.environ.get("WERKZEUG_RUN_MAIN") == "true":ensure_echo_on()# 这里是主要代码:把我们上面传递过来的inner函数作为主函数,开始一个新线程(守护线程)t = threading.Thread(target=main_func, args=())t.daemon = Truewith reloader:t.start()reloader.run()else:sys.exit(reloader.restart_with_reloader())except KeyboardInterrupt:pass

其主要功能就是把我们上面传递过来的inner函数作为主函数,开始一个新线程(守护线程),本质还是执行inner函数。

如果use_reloader为False,则直接执行inner函数。

所以,inner()函数是run_simple()函数的核心。

下面看看inner函数主要做了什么。把inner函数代码单独拿出来,如下:

def inner() -> None:try:fd: t.Optional[int] = int(os.environ["WERKZEUG_SERVER_FD"])except (LookupError, ValueError):fd = Nonesrv = make_server(hostname,port,application,threaded,processes,request_handler,passthrough_errors,ssl_context,fd=fd,)if fd is None:log_startup(srv.socket)# 这里是inner函数的核心,调用了wsgi的make_server创建了一个wsgi服务器,并且执行serve_forever方法srv.serve_forever()

inner()函数的核心就是:调用了make_server()方法,并且执行了serve_forever方法。

下面来分析下make_server方法做了什么。

(4)分析make_server方法

make_server()方法是werkzeug包内的方法。其源码如下:

def make_server(host: str,port: int,app: "WSGIApplication",threaded: bool = False,processes: int = 1,request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None,passthrough_errors: bool = False,ssl_context: t.Optional[_TSSLContextArg] = None,fd: t.Optional[int] = None,
) -> BaseWSGIServer:"""Create a new server instance that is either threaded, or forksor just processes one request after another."""if threaded and processes > 1:raise ValueError("cannot have a multithreaded and multi process server.")elif threaded:return ThreadedWSGIServer(host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd)elif processes > 1:return ForkingWSGIServer(host,port,app,processes,request_handler,passthrough_errors,ssl_context,fd=fd,)else:# 返回了一个BaseWSGIServer对象return BaseWSGIServer(host, port, app, request_handler, passthrough_errors, ssl_context, fd=fd)

首先这个方法接受了run_simple函数传递进来的参数:host、port、app(Flask对象)、thread(线程)、 processes = 1 (单进程 )。

最重要的是这个方法返回了一个BaseWSGIServer对象(ThreadedWSGIServer和ForkingWSGIServer都继承了它),后面会启动BaseWSGIServer,创造一个单线程,单进程的WSGI server

(5)分析BaseWSGIServer类

BaseWSGIServer源码如下:

class BaseWSGIServer(HTTPServer):"""Simple single-threaded, single-process WSGI server."""multithread = Falsemultiprocess = Falserequest_queue_size = LISTEN_QUEUEdef __init__(self,host: str,port: int,app: "WSGIApplication",handler: t.Optional[t.Type[WSGIRequestHandler]] = None,passthrough_errors: bool = False,ssl_context: t.Optional[_TSSLContextArg] = None,fd: t.Optional[int] = None,) -> None:if handler is None:# wsgi的请求处理器handler = WSGIRequestHandlerself.address_family = select_address_family(host, port)if fd is not None:real_sock = socket.fromfd(fd, self.address_family, socket.SOCK_STREAM)port = 0# 获取服务地址server_address = get_sockaddr(host, int(port), self.address_family)# remove socket file if it already existsif self.address_family == af_unix:server_address = t.cast(str, server_address)if os.path.exists(server_address):os.unlink(server_address)# 调用父类HTTPServer的初始化方法,参数有地址和请求处理器super().__init__(server_address, handler)  # type: ignoreself.app = appself.passthrough_errors = passthrough_errorsself.shutdown_signal = Falseself.host = hostself.port = self.socket.getsockname()[1]# Patch in the original socket.if fd is not None:self.socket.close()self.socket = real_sockself.server_address = self.socket.getsockname()if ssl_context is not None:if isinstance(ssl_context, tuple):ssl_context = load_ssl_context(*ssl_context)if ssl_context == "adhoc":ssl_context = generate_adhoc_ssl_context()self.socket = ssl_context.wrap_socket(self.socket, server_side=True)self.ssl_context: t.Optional["ssl.SSLContext"] = ssl_contextelse:self.ssl_context = Nonedef log(self, type: str, message: str, *args: t.Any) -> None:_log(type, message, *args)def serve_forever(self, poll_interval: float = 0.5) -> None:"""核心方法:调用了父类HTTPServer的serve_forever方法"""self.shutdown_signal = Falsetry:super().serve_forever(poll_interval=poll_interval)except KeyboardInterrupt:passfinally:self.server_close()def handle_error(self, request: t.Any, client_address: t.Tuple[str, int]) -> None:if self.passthrough_errors:raisereturn super().handle_error(request, client_address)

BaseWSGIServer的核心就是继承了HTTPServer,其中初始化方法里主要做了两件事,一个是使用get_sockaddr方法找到服务地址,还有一个就是使用父类HTTPServer的初始化方法,参数有地址和请求处理器WSGIRequestHandler

其中最核心的方法就是前面inner方法里提到过的serve_forever()方法。
这个方法其实也是调用了父类BaseServer的serve_forever方法。BaseServer是socketserver模块下的一个类。

继承关系是:BaseWSGIServer继承HTTPServer,HTTPServer继承socketserver.TCPServer,socketserver.TCPServer继承BaseServer。

(6)分析HTTPServer类

HTTPServer在http.server.py 模块中。其源码如下:

class HTTPServer(socketserver.TCPServer):allow_reuse_address = 1    # Seems to make sense in testing environmentdef server_bind(self):"""Override server_bind to store the server name."""socketserver.TCPServer.server_bind(self)host, port = self.server_address[:2]self.server_name = socket.getfqdn(host)self.server_port = port

可以看到

  1. HTTPServer继承了socketserver.TCPServer类,socketserver.TCPServer类属于socketserver模块下下一个TCP服务类,不再做过多的解析了;
  2. 重写了一个server_bind方法,用于绑定服务器地址和端口。

所以HTTPServer主要作用是于绑定服务器地址和端口。

下面着重看下serve_forever方法。

(7)分析serve_forever方法

上面说了,这个方法其实也是调用了父类BaseServerserve_forever方法。

其代码如下:

def serve_forever(self, poll_interval=0.5):"""Handle one request at a time until shutdown.Polls for shutdown every poll_interval seconds. Ignoresself.timeout. If you need to do periodic tasks, do them inanother thread."""self.__is_shut_down.clear()try:# XXX: Consider using another file descriptor or connecting to the# socket to wake this up instead of polling. Polling reduces our# responsiveness to a shutdown request and wastes cpu at all other# times.with _ServerSelector() as selector:selector.register(self, selectors.EVENT_READ)while not self.__shutdown_request:ready = selector.select(poll_interval)# bpo-35017: shutdown() called during select(), exit immediately.if self.__shutdown_request:breakif ready:self._handle_request_noblock()self.service_actions()finally:self.__shutdown_request = Falseself.__is_shut_down.set()

可以看到主要作用就是写了一个死循环,然后不断处理接收到的请求。

(8)分析WSGIRequestHandler类

WSGIRequestHandler类的源码比较多,其中定义了很多方法用来处理请求。主要源码如下:

class WSGIRequestHandler(BaseHTTPRequestHandler):"""A request handler that implements WSGI dispatching."""server: "BaseWSGIServer"def run_wsgi(self) -> None:if self.headers.get("Expect", "").lower().strip() == "100-continue":self.wfile.write(b"HTTP/1.1 100 Continue\r\n\r\n")self.environ = environ = self.make_environ()status_set: t.Optional[str] = Noneheaders_set: t.Optional[t.List[t.Tuple[str, str]]] = Nonestatus_sent: t.Optional[str] = Noneheaders_sent: t.Optional[t.List[t.Tuple[str, str]]] = Nonedef write(data: bytes) -> None:nonlocal status_sent, headers_sentassert status_set is not None, "write() before start_response"assert headers_set is not None, "write() before start_response"if status_sent is None:status_sent = status_setheaders_sent = headers_settry:code_str, msg = status_sent.split(None, 1)except ValueError:code_str, msg = status_sent, ""code = int(code_str)self.send_response(code, msg)header_keys = set()for key, value in headers_sent:self.send_header(key, value)key = key.lower()header_keys.add(key)if not ("content-length" in header_keysor environ["REQUEST_METHOD"] == "HEAD"or code < 200or code in (204, 304)):self.close_connection = Trueself.send_header("Connection", "close")if "server" not in header_keys:self.send_header("Server", self.version_string())if "date" not in header_keys:self.send_header("Date", self.date_time_string())self.end_headers()assert isinstance(data, bytes), "applications must write bytes"self.wfile.write(data)self.wfile.flush()def start_response(status, headers, exc_info=None):  # type: ignorenonlocal status_set, headers_setif exc_info:try:if headers_sent:raise exc_info[1].with_traceback(exc_info[2])finally:exc_info = Noneelif headers_set:raise AssertionError("Headers already set")status_set = statusheaders_set = headersreturn writedef execute(app: "WSGIApplication") -> None:# 这里的app注意就是我们上面run_simple方法传递进来的app级Flask app# 执行execute方法会调用app(),这就是为什么要求我们的Flask对象要可被调用(实现__call__方法)# 调用app时把environ环境参数和start_response传进去,与第二节里返示例完全一致了application_iter = app(environ, start_response)try:for data in application_iter:write(data)if not headers_sent:write(b"")finally:if hasattr(application_iter, "close"):application_iter.close()  # type: ignoretry:# run_wsgi的核心是执行execute方法execute(self.server.app)except (ConnectionError, socket.timeout) as e:self.connection_dropped(e, environ)except Exception:if self.server.passthrough_errors:raisefrom .debug.tbtools import get_current_tracebacktraceback = get_current_traceback(ignore_system_exceptions=True)try:# if we haven't yet sent the headers but they are set# we roll back to be able to set them again.if status_sent is None:status_set = Noneheaders_set = Noneexecute(InternalServerError())except Exception:passself.server.log("error", "Error on request:\n%s", traceback.plaintext)def handle_one_request(self) -> None:"""Handle a single HTTP request."""self.raw_requestline = self.rfile.readline()if not self.raw_requestline:self.close_connection = Trueelif self.parse_request():self.run_wsgi()

非常重要:

首先handle_one_request(self)方法,主要用来处理一个请求。当一个请求进来时就会调用这个方法。接着handle_one_request调用了核心方法run_wsgi()方法。run_wsgi()方法的核心就是execute方法。其中最最重要的是execute方法里application_iter = app(environ, start_response)这行代码。

这里的app注意就是我们上面run_simple方法传递进来的app及Flask app。执行execute方法会调用app(),这就是为什么要求我们的Flask对象要可被调用(实现__call__方法)。调用app__call__方法时把environ环境参数和start_response传进去,与第二节里返示例完全一致了

5 Flask工作流程总结

经过以上的分析,可以看到,Flask的工作流程大致如下:

  1. 创建一个Flask对象app,并执行app.run()方法;
  2. app.run()方法主要调用了run_simple()方法;
  3. run_simple()方法主要执行了内部的inner()方法;
  4. inner()方法主要执行了make_server()方法,返回了一个BaseWSGIServer对象srv,srv对象有个WSGIRequestHandler对象属性,用于处理请求;
  5. 然后inner()方法执行了BaseWSGIServer对象的serve_forever()方法,这个方法主要是写了一个死循环,用于处理不断接收到的请求;
  6. 当一个请求进来时,会先执行WSGIRequestHandler对象的handle_one_request()方法;
  7. handle_one_request()方法调用了核心方法run_wsgi()方法;
  8. run_wsgi()方法的核心就是execute()方法;
  9. execute方法里核心代码是application_iter = app(environ, start_response),app就是上面run_simple方法传递进来的app,即Flask app,这样就会调用app的__call__方法,调用app__call__方法时把environ环境参数和start_response传进去。
  10. 接着就是执行app的__call__方法来处理请求了。

整个流程图可以概括如下图:

在这里插入图片描述

可以看到整个Falsk启动过程中,最重要的是创建了BaseWSGIServer对象,并执行了对象的serve_forever()方法。这样一个wsgi服务器就创建并启动好了,当请求进来时,就可以处理请求,并把请求header和参数等传递给Flask app的__call__方法,接着就是根据请求做出响应了。

所以wsgi在其中的作用就是一个"翻译官"或"媒介",接受到客户端的请求,进行处理和包装,再传给Falsk应用服务器。

参考:
https://blog.csdn.net/sinat_36651044/article/details/77462831

https://www.cnblogs.com/skyflask/p/9193828.html

https://blog.csdn.net/bestallen/article/details/54342120

https://cizixs.com/2017/01/11/flask-insight-start-process/

https://blog.csdn.net/lantian_123/article/details/109396576?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-109396576-blog-122802770.pc_relevant_3mothn_strategy_recovery&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-109396576-blog-122802770.pc_relevant_3mothn_strategy_recovery&utm_relevant_index=1


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部