web|粗读web框架之go gin和python django

为什么引入web框架 web应用的本质

  • 浏览器发送一个HTTP请求;
  • 服务器收到请求,生成一个HTML文档;
  • 服务器把HTML文档作为HTTP响应的Body发送给浏览器;
  • 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示;
涉及的问题
  • 解析http请求
  • 找到对应的处理函数
  • 生成并发送http响应
    • *
web框架工作流程 web|粗读web框架之go gin和python django
文章图片
web|粗读web框架之go gin和python django
文章图片
?
中间件
  • 中间件是请求或者应用开始和结束时注入代码的机制
  • 常见的web中间件: 鉴权、打印log、session、统计信息、处理数据库连接等等
web|粗读web框架之go gin和python django
文章图片
web|粗读web框架之go gin和python django
文章图片
? ===
golang http 服务端编程
  • golang 的net/http包提供了http编程的相关接口,封装了内部TCP连接和报文解析的复杂琐碎的细节
    go c.serve(ctx)最终会启动goroutine处理请求
  • 使用者只需要和http.requesthttp.ResponseWriter 两个对象交互就行(也就是一个struct 实现net/http包中的Handler interface中的 ServeHttp方法 )
//E:\Go\src\net\http\server.go +82 type Handler interface { ServeHTTP(ResponseWriter, *Request) }//纯 net.http包的server方法 package mainimport ( "io" "net/http" )type helloHandler struct{}func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, world!")) }func main() { http.Handle("/", &helloHandler{}) http.ListenAndServe(":12345", nil) }//////////////////////////////////////////////////////////////////import ( "net/http" )type Handle struct{}func (h Handle) ServeHTTP(response http.ResponseWriter, request *http.Request) { switch request.URL.Path { case "/info": response.Write([]byte("info")) default:} }func main() { http.ListenAndServe(":8888", Handle{}) }

web|粗读web框架之go gin和python django
文章图片

  • net/http 的另外一个重要的概念,ServeMux (多路传输):ServeMux 可以注册多了 URL 和 handler 的对应关系,并自动把请求转发到对应的 handler 进行处理
  • // 下面看一个带路由的http server package mainimport ( "io" "net/http" )func helloHandler(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello, world!\n") }func echoHandler(w http.ResponseWriter, r *http.Request) { io.WriteString(w, r.URL.Path) }func main() { mux := http.NewServeMux() mux.HandleFunc("/hello", helloHandler) mux.HandleFunc("/", echoHandler)http.ListenAndServe(":12345", mux) }// 其实ServeMux 也实现了Handler的ServeHTTP方法所以也是Handler接口 //E:\Go\src\net\http\server.go 2382 func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r) }// E:\Go\src\net\http\server.go +2219可以看到 net/http包中的 基于map 路由查找 // Find a handler on a handler map given a path string. // Most-specific (longest) pattern wins. func (mux *ServeMux) match(path string) (h Handler, pattern string) { // Check for exact match first. v, ok := mux.m[path] if ok { return v.h, v.pattern }// Check for longest valid match. var n = 0 for k, v := range mux.m { if !pathMatch(k, path) { continue } if h == nil || len(k) > n { n = len(k) h = v.h pattern = v.pattern } } return }

    web|粗读web框架之go gin和python django
    文章图片

golang web框架 GIN golang gin web框架
//gin框架初始化的流程 1.初始化engine 2.注册中间件 3.注册路由//响应流程 1.路由,找到handle 2.将请求和响应用Context包装起来供业务代码使用 3.依次调用中间件和处理函数 4.输出结果//gin 里面最重要的两个数据结构 //1.Context在中间件中传递本次请求的各种数据、管理流程,进行响应 //2.Engine gin 引擎,是框架的实例,它包含多路复用器,中间件和配置设置// 下面看下open-falcon-api中的实际应用 //open-falcon-api里面注册路由和中间件 //E:\go_path\src\github.com\open-falcon\falcon-plus\modules\api\app\controller\graph\graph_routes.go // 首先注册/api/v1/开头的path的路由组 // 然后Use 一个auth的中间件 ,作用是检查token // 这个组后面的所有path 都使用这个中间件 // 也就是访问 /graph/endpoint 时会过 3个中间件(log recoveryauth )+一个EndpointRegexpQuery处理函数 // func Routes(r *gin.Engine) {db = config.Con() authapi := r.Group("/api/v1") authapi.Use(utils.AuthSessionMidd) authapi.GET("/graph/endpointobj", EndpointObjGet) authapi.GET("/graph/endpoint", EndpointRegexpQuery) authapi.GET("/graph/endpoint_counter", EndpointCounterRegexpQuery) authapi.POST("/graph/history", QueryGraphDrawData) authapi.POST("/graph/lastpoint", QueryGraphLastPoint) authapi.POST("/graph/info", QueryGraphItemPosition) authapi.DELETE("/graph/endpoint", DeleteGraphEndpoint) authapi.DELETE("/graph/counter", DeleteGraphCounter)grfanaapi := r.Group("/api") grfanaapi.GET("/v1/grafana", GrafanaMainQuery) grfanaapi.GET("/v1/grafana/metrics/find", GrafanaMainQuery) grfanaapi.POST("/v1/grafana/render", GrafanaRender) grfanaapi.GET("/v1/grafana/render", GrafanaRender)}func AuthSessionMidd(c *gin.Context) { auth, err := h.SessionChecking(c) if !viper.GetBool("skip_auth") { if err != nil || auth != true { log.Debugf("error: %v, auth: %v", err, auth) c.Set("auth", auth) h.JSONR(c, http.StatusUnauthorized, err) c.Abort() return } } c.Set("auth", auth) }// E:\go_path\src\github.com\open-falcon\falcon-plus\vendor\github.com\gin-gonic\gin\context.go +715 最后会调用这里的Render方法 // Render writes the response headers and calls render.Render to render data. func (c *Context) Render(code int, r render.Render) { c.Status(code)if !bodyAllowedForStatus(code) { r.WriteContentType(c.Writer) c.Writer.WriteHeaderNow() return }if err := r.Render(c.Writer); err != nil { panic(err) } }// 可以看到这里的bind 是在gin在解析请求payload是否和函数中要求的struct一致 // E:\go_path\src\github.com\open-falcon\falcon-plus\vendor\github.com\gin-gonic\gin\context.go +504 // bind会根据请求中的Content-Type header判断是json 还是xml // 并且根据struct中的tag通过反射解析payload// Bind checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: //"application/json" --> JSON binding //"application/xml"--> XML binding // otherwise --> returns an error. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It decodes the json payload into the struct specified as a pointer. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj interface{}) error { b := binding.Default(c.Request.Method, c.ContentType()) return c.MustBindWith(obj, b) }type APIEndpointObjGetInputs struct { Endpoints []string `json:"endpoints" form:"endpoints"` Deadlineint64`json:"deadline" form:"deadline"` }func EndpointObjGet(c *gin.Context) { inputs := APIEndpointObjGetInputs{ Deadline: 0, } if err := c.Bind(&inputs); err != nil { h.JSONR(c, badstatus, err) return } if len(inputs.Endpoints) == 0 { h.JSONR(c, http.StatusBadRequest, "endpoints missing") return }var result []m.Endpoint = []m.Endpoint{} dt := db.Graph.Table("endpoint"). Where("endpoint in (?) and ts >= ?", inputs.Endpoints, inputs.Deadline). Scan(&result) if dt.Error != nil { h.JSONR(c, http.StatusBadRequest, dt.Error) return }endpoints := []map[string]interface{}{} for _, r := range result { endpoints = append(endpoints, map[string]interface{}{"id": r.ID, "endpoint": r.Endpoint, "ts": r.Ts}) }h.JSONR(c, endpoints) }//E:\go_path\src\github.com\open-falcon\falcon-plus\modules\api\main.go+78 //初始化gin routes := gin.Default()//E:\go_path\src\github.com\open-falcon\falcon-plus\vendor\github.com\gin-gonic\gin\gin.go +148 // Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine }//E:\go_path\src\github.com\open-falcon\falcon-plus\vendor\github.com\gin-gonic\gin\gin.go +119 // new方法 返回一个不带中间件的 单例engine ,并且把context 放在池中 func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, basePath: "/", root:true, }, FuncMap:template.FuncMap{}, RedirectTrailingSlash:true, RedirectFixedPath:false, HandleMethodNotAllowed: false, ForwardedByClientIP:true, AppEngine:defaultAppEngine, UseRawPath:false, UnescapePathValues:true, MaxMultipartMemory:defaultMultipartMemory, trees:make(methodTrees, 0, 9), delims:render.Delims{Left: "{{", Right: "}}"}, secureJsonPrefix:"while(1); ", } engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { return engine.allocateContext() } return engine }//E:\go_path\src\github.com\open-falcon\falcon-plus\modules\api\app\controller\routes.go //r.Run(port) 最后调用的是net.http.ListenAndServe func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }()address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) return }//E:\Go\src\net\http\server.go +2686 func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } if req.RequestURI == "*" && req.Method == "OPTIONS" { handler = globalOptionsHandler{} } handler.ServeHTTP(rw, req) }//E:\go_path\src\github.com\open-falcon\falcon-plus\vendor\github.com\gin-gonic\gin\gin.go +321 //我们可以看到 在gin中实现了ServeHTTP方法net.http.Handler// ServeHTTP conforms to the http.Handler interface. // 这里使用sync.pool cache context数据结构,避免频繁GC,每次使用都初始化 //一个struct实现了 interface中的方法 就说明这个struct是这个类型 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset()engine.handleHTTPRequest(c)engine.pool.Put(c) }// gin 里面处理请求的核心方法 // 根据一些配置去 压缩前缀树 radix.tree中找到对应的handleChain 然后执行 // 注意这句handlers, params, tsr := root.getValue(path, c.Params, unescape) // 路由查找的过程是 从基数树的根节点一直匹配到和请求一致的节点找到对应的handlerchain // 注册路由 E:\go_path\src\github.com\open-falcon\falcon-plus\vendor\github.com\gin-gonic\gin\gin.go +243 // 从下面的addRoute方法中可以看到gin 为每个HttpMethodGET POST PUT DELETE都注册了一颗tree // 并且有priority 即最长的路径优先匹配 /* func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler")debugPrintRoute(method, path, handlers) root := engine.trees.get(method) if root == nil { root = new(node) engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) } */func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method path := c.Request.URL.Path unescape := false if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { path = c.Request.URL.RawPath unescape = engine.UnescapePathValues }// Find root of the tree for the given HTTP method t := engine.trees for i, tl := 0, len(t); i < tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root // Find route in tree handlers, params, tsr := root.getValue(path, c.Params, unescape) if handlers != nil { c.handlers = handlers c.Params = params c.Next() c.writermem.WriteHeaderNow() return } if httpMethod != "CONNECT" && path != "/" { if tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(c) return } if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) { return } } break }if engine.HandleMethodNotAllowed { for _, tree := range engine.trees { if tree.method == httpMethod { continue } if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { c.handlers = engine.allNoMethod serveError(c, http.StatusMethodNotAllowed, default405Body) return } } } c.handlers = engine.allNoRoute serveError(c, http.StatusNotFound, default404Body) }

web|粗读web框架之go gin和python django
文章图片

python django(django框架复杂,先简单看一下)
# 入口文件 def execute_from_command_line(argv=None): """ A simple method that runs a ManagementUtility. """ utility = ManagementUtility(argv) utility.execute()def execute(self): """ Given the command-line arguments, figure out which subcommand is being run, create a parser appropriate to that command, and run it. """ try: subcommand = self.argv[1] except IndexError: subcommand = 'help'# Display help if no arguments were given.# Preprocess options to extract --settings and --pythonpath. # These options could affect the commands that are available, so they # must be processed early. parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False) parser.add_argument('--settings') parser.add_argument('--pythonpath') parser.add_argument('args', nargs='*')# catch-all try: options, args = parser.parse_known_args(self.argv[2:]) handle_default_options(options) except CommandError: pass# Ignore any option errors at this point.try: settings.INSTALLED_APPS except ImproperlyConfigured as exc: self.settings_exception = exc except ImportError as exc: self.settings_exception = excif settings.configured: # Start the auto-reloading dev server even if the code is broken. # The hardcoded condition is a code smell but we can't rely on a # flag on the command class because we haven't located it yet. if subcommand == 'runserver' and '--noreload' not in self.argv: try: autoreload.check_errors(django.setup)() except Exception: # The exception will be raised later in the child process # started by the autoreloader. Pretend it didn't happen by # loading an empty list of applications. apps.all_models = defaultdict(OrderedDict) apps.app_configs = OrderedDict() apps.apps_ready = apps.models_ready = apps.ready = True# Remove options not compatible with the built-in runserver # (e.g. options for the contrib.staticfiles' runserver). # Changes here require manually testing as described in # #27522. _parser = self.fetch_command('runserver').create_parser('django', 'runserver') _options, _args = _parser.parse_known_args(self.argv[2:]) for _arg in _args: self.argv.remove(_arg)# In all other cases, django.setup() is required to succeed. else: django.setup()self.autocomplete()if subcommand == 'help': if '--commands' in args: sys.stdout.write(self.main_help_text(commands_only=True) + '\n') elif not options.args: sys.stdout.write(self.main_help_text() + '\n') else: self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0]) # Special-cases: We want 'django-admin --version' and # 'django-admin --help' to work, for backwards compatibility. elif subcommand == 'version' or self.argv[1:] == ['--version']: sys.stdout.write(django.get_version() + '\n') elif self.argv[1:] in (['--help'], ['-h']): sys.stdout.write(self.main_help_text() + '\n') else: self.fetch_command(subcommand).run_from_argv(self.argv)#C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\site-packages\django\core\management\__init__.py +301 ''' #1.fetch_command 最终会调用C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\site-packages\django\core\management\__init__.py 的find_commands() 最终会找到 django\core\management\commands 下面的所有的command check compilemessages createcachetable dbshell diffsettings dumpdata flush inspectdb loaddata makemessages makemigrations migrate runserver sendtestemail shell showmigrations sqlflush sqlmigrate sqlsequencereset squashmigrations startapp startproject test testserver2. run_from_argv 调 execute() 再调用handle() '''# 最常用的runserver #C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\site-packages\django\core\management\commands\runserver.py + # execute()-->handle()-->run()-->inner_run()-->get_wsgi_application() #WSGIHandler 在这里加载中间件 # C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\site-packages\django\core\handlers\wsgi.py class WSGIHandler(base.BaseHandler): request_class = WSGIRequestdef __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.load_middleware()def __call__(self, environ, start_response): set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) request = self.request_class(environ) response = self.get_response(request)response._handler_class = self.__class__status = '%d %s' % (response.status_code, response.reason_phrase) response_headers = [ *response.items(), *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()), ] start_response(status, response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return responsedef inner_run(self, *args, **options): # If an exception was silenced in ManagementUtility.execute in order # to be raised in the child process, raise it now. autoreload.raise_last_exception()threading = options['use_threading'] # 'shutdown_message' is a stealth option. shutdown_message = options.get('shutdown_message', '') quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'self.stdout.write("Performing system checks...\n\n") self.check(display_num_errors=True) # Need to check migrations here, so can't use the # requires_migrations_check attribute. self.check_migrations() now = datetime.now().strftime('%B %d, %Y - %X') self.stdout.write(now) self.stdout.write(( "Django version %(version)s, using settings %(settings)r\n" "Starting development server at %(protocol)s://%(addr)s:%(port)s/\n" "Quit the server with %(quit_command)s.\n" ) % { "version": self.get_version(), "settings": settings.SETTINGS_MODULE, "protocol": self.protocol, "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr, "port": self.port, "quit_command": quit_command, })try: handler = self.get_handler(*args, **options) run(self.addr, int(self.port), handler, ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls) except socket.error as e: # Use helpful error messages instead of ugly tracebacks. ERRORS = { errno.EACCES: "You don't have permission to access that port.", errno.EADDRINUSE: "That port is already in use.", errno.EADDRNOTAVAIL: "That IP address can't be assigned to.", } try: error_text = ERRORS[e.errno] except KeyError: error_text = e self.stderr.write("Error: %s" % error_text) # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: if shutdown_message: self.stdout.write(shutdown_message) sys.exit(0)# C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\site-packages\django\core\handlers\base.py class BaseHandler: _view_middleware = None _template_response_middleware = None _exception_middleware = None _middleware_chain = Nonedef load_middleware(self): """ Populate middleware lists from settings.MIDDLEWARE.Must be called after the environment is fixed (see __call__ in subclasses). """ self._view_middleware = [] self._template_response_middleware = [] self._exception_middleware = []handler = convert_exception_to_response(self._get_response) for middleware_path in reversed(settings.MIDDLEWARE): middleware = import_string(middleware_path) try: mw_instance = middleware(handler) except MiddlewareNotUsed as exc: if settings.DEBUG: if str(exc): logger.debug('MiddlewareNotUsed(%r): %s', middleware_path, exc) else: logger.debug('MiddlewareNotUsed: %r', middleware_path) continueif mw_instance is None: raise ImproperlyConfigured( 'Middleware factory %s returned None.' % middleware_path )if hasattr(mw_instance, 'process_view'): self._view_middleware.insert(0, mw_instance.process_view) if hasattr(mw_instance, 'process_template_response'): self._template_response_middleware.append(mw_instance.process_template_response) if hasattr(mw_instance, 'process_exception'): self._exception_middleware.append(mw_instance.process_exception)handler = convert_exception_to_response(mw_instance)# We only assign to this when initialization is complete as it is used # as a flag for initialization being complete. self._middleware_chain = handler#get_handler 函数最终会返回一个 WSGIHandler 的实例。WSGIHandler 类只实现了 def __call__(self, environ, start_response) , 使它本身能够成为 WSGI 中的应用程序, 并且实现 __call__ 能让类的行为跟函数一样。 class WSGIHandler(base.BaseHandler): request_class = WSGIRequestdef __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.load_middleware()def __call__(self, environ, start_response): set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) request = self.request_class(environ) response = self.get_response(request)response._handler_class = self.__class__status = '%d %s' % (response.status_code, response.reason_phrase) response_headers = [ *response.items(), *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()), ] start_response(status, response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return response# C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\socketserver.py + 215 def serve_forever(self, poll_interval=0.5): """ 处理一个http请求直到关闭 """ 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) if ready: #如果 fd可用调用处理方法 self._handle_request_noblock()self.service_actions() finally: self.__shutdown_request = False self.__is_shut_down.set()def _handle_request_noblock(self): """Handle one request, without blocking.I assume that selector.select() has returned that the socket is readable before this function was called, so there should be no risk of blocking in get_request(). """ try: request, client_address = self.get_request() except OSError: return if self.verify_request(request, client_address): try: #这里是真正处理请求的地方 self.process_request(request, client_address) except Exception: self.handle_error(request, client_address) self.shutdown_request(request) except: self.shutdown_request(request) raise else: self.shutdown_request(request)def process_request(self, request, client_address): """Call finish_request.Overridden by ForkingMixIn and ThreadingMixIn.""" self.finish_request(request, client_address) self.shutdown_request(request)#finish_request 最后调用这个BaseRequestHandler class BaseRequestHandler: ''''''def __init__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server self.setup() try: self.handle() finally: self.finish()# C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Lib\site-packages\django\core\servers\basehttp.py +156 def handle(self): ''' 这里对请求长度做限制 parse_request对http解包 '''self.raw_requestline = self.rfile.readline(65537) if len(self.raw_requestline) > 65536: self.requestline = '' self.request_version = '' self.command = '' self.send_error(414) returnif not self.parse_request():# An error code has been sent, just exit returnhandler = ServerHandler( self.rfile, self.wfile, self.get_stderr(), self.get_environ() ) handler.request_handler = self# backpointer for logging handler.run(self.server.get_app())#get_app 返回之前装配的WSGIAPP最终 class WSGIHandler(base.BaseHandler): request_class = WSGIRequestdef __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.load_middleware()def __call__(self, environ, start_response): set_script_prefix(get_script_name(environ)) signals.request_started.send(sender=self.__class__, environ=environ) request = self.request_class(environ) response = self.get_response(request)response._handler_class = self.__class__status = '%d %s' % (response.status_code, response.reason_phrase) response_headers = [ *response.items(), *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()), ] start_response(status, response_headers) if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'): response = environ['wsgi.file_wrapper'](response.file_to_stream) return response

【web|粗读web框架之go gin和python django】web|粗读web框架之go gin和python django
文章图片

    推荐阅读