web 框架的本质

web框架的本质——通俗的说,web框架封装了socket、数据库操作、路由分发、模板配置等,留给我们现成的接口,让我们更专注业务逻辑本身。

一个简单的web示例

import socket

# 1. 创建socket对象
sk = socket.socket()

# 2. 绑定IP和端口
sk.bind(('127.0.0.1', 8888))

# 3. 监听
sk.listen(5)

# 4. 循环监听

while 1:
    conn, addr = sk.accept()  # 等待连接
    received_data = conn.recv(8192)  # 接收数据
    print(received_data)
    print(received_data.decode(encoding="utf-8"))
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')  # 接收数据
    conn.send(b'<h1>Successful</h1>')
    conn.close()  # 关闭连接

现在,使用浏览器地址栏访问http://127.0.0.1:8888/,就会得到Successful

用户的浏览器一输入网址,会给服务端发送数据,那浏览器会发送什么数据?怎么发?这个谁来定? 你这个网站是这个规定,他那个网站按照他那个规定,这互联网还能玩么?

所以,必须有一个统一的规则,让大家发送消息、接收消息的时候有个格式依据,不能随便写。

这个规则就是HTTP协议,以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。

HTTP协议主要规定了客户端和服务器之间的通信格式,那HTTP协议是怎么规定消息格式的呢?

让我们首先打印下我们在服务端接收到的消息是什么。

b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nsec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: "Windows"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\n\r\n'
GET / HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9


b'GET /favicon.ico HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nConnection: keep-alive\r\nsec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36\r\nsec-ch-ua-platform: "Windows"\r\nAccept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8888/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\n\r\n'
GET /favicon.ico HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
sec-ch-ua-platform: "Windows"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:8888/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

根据不同路径返回不同内容

第一版·普通版

import socket

# 创建socket对象
sk = socket.socket()

# 绑定IP和端口
sk.bind(('127.0.0.1', 8888))

# 监听
sk.listen()

while True:
    # 等待连接
    conn, addr = sk.accept()
    # 接收数据
    data = conn.recv(8192)
    data = data.decode('utf-8')
    url = data.split()[1]
    print(url)
    # 返回状态行
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    if url == '/oumei':
        conn.send(b'<h1>oumei</h1>')
    elif url == '/rihan':
        conn.send(b'<h1>rihan</h1>')
    else:
        conn.send(b'<h1>404</h1>')
    # 关闭连接
    conn.close()

第二版·函数版

import socket
# 创建socket对象
sk = socket.socket()
# 绑定IP和端口
sk.bind(('127.0.0.1', 8888))
# 监听
sk.listen()
def oumei(url):
    ret = 'oumei   -  {}'.format(url)
    return ret.encode('utf-8')
def rihan(url):
    ret = 'rihan////   -  {}'.format(url)
    return ret.encode('utf-8')
while True:
    # 等待连接
    conn, addr = sk.accept()
    # 接收数据
    data = conn.recv(8192)
    data = data.decode('utf-8')
    url = data.split()[1]
    print(url)
    # 返回状态行
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    if url == '/oumei':
        ret = oumei(url)
    elif url == '/rihan':
        ret = rihan(url)
    else:
        ret = b'404 not found'
    conn.send(ret)
    # 关闭连接
    conn.close()

第三版·函数进阶版

import socket
# 创建socket对象
sk = socket.socket()
# 绑定IP和端口
sk.bind(('127.0.0.1', 8888))
# 监听
sk.listen()
def oumei(url):
    ret = 'oumei   -  {}'.format(url)
    return ret.encode('utf-8')
def rihan(url):
    ret = 'rihan////   -  {}'.format(url)
    return ret.encode('utf-8')
def guochan(url):
    ret = 'guochan   -  {}'.format(url)
    return ret.encode('utf-8')
list = [
    ('/oumei', oumei),
    ('/rihan', rihan),
    ('/guochan', guochan),
]
while True:
    # 等待连接
    conn, addr = sk.accept()
    # 接收数据
    data = conn.recv(8192)
    data = data.decode('utf-8')
    url = data.split()[1]
    print(url)
    # 返回状态行
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    func = None
    for i in list:
        if i[0] == url:
            func = i[1]
            break
    if func:
        ret = func(url)
    else:
        ret = b'404 not found'
    conn.send(ret)
    # 关闭连接
    conn.close()

第四版·反射版

ps:仅针对当前脚本使用的反射。

import socket
sk = socket.socket()
sk.bind(("127.0.0.1", 8888))
sk.listen()
class Server(object):
    """ 模拟多个页面 """
    def europe(self):
        return "<h1>europe</h1>".encode('utf8')
    def kkc(self):
        return "<h1>kkc</h1>".encode('utf8')
    def notfond(self):
        return "<h1>404 not fond</h1>".encode('utf8')
while 1:
    # 等待请求
    conn, address = sk.accept()
    # 获取数据
    data = conn.recv(8192).decode('utf8')
    # 处理请求拿到请求路径
    # print(data)
    url = data.split(' ', 2)[1][1:]
    # print(url, data)
    # 返回数据
    server_obj = Server()
    if hasattr(server_obj, url):
        res = getattr(server_obj, url)()
    else:
        res = server_obj.notfond()
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    conn.send(res)
    conn.close()

返回HTML页面

首先在同级目录有个index.html页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Title</title>
</head>
<body>

<h1>index page</h1>

</body>
</html>

然后写server端:

import socket

# 创建socket对象
sk = socket.socket()

# 绑定IP和端口
sk.bind(('127.0.0.1', 8888))

# 监听
sk.listen()


def oumei(url):
    ret = 'oumei   -  {}'.format(url)
    return ret.encode('utf-8')


def rihan(url):
    ret = 'rihan////   -  {}'.format(url)
    return ret.encode('utf-8')


def guochan(url):
    ret = 'guochan   -  {}'.format(url)
    return ret.encode('utf-8')


def index(url):
    with open('index.html', 'rb') as f:
        ret = f.read()
        return ret


list = [
    ('/oumei', oumei),
    ('/rihan', rihan),
    ('/guochan', guochan),
    ('/index', index),
]

while True:
    # 等待连接
    conn, addr = sk.accept()
    # 接收数据
    data = conn.recv(8192)
    data = data.decode('utf-8')
    url = data.split()[1]
    print(url)

    # 返回状态行
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    func = None
    for i in list:
        if i[0] == url:
            func = i[1]
            break

    if func:
        ret = func(url)
    else:
        ret = b'404 not found'

    conn.send(ret)

    # 关闭连接
    conn.close()

返回动态页面

在同级目录有个tiem.html页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>当前时间是: @@time@@ </h1>
</body>
</html>

在来实现代码:

import socket
import time

# 创建socket对象
sk = socket.socket()

# 绑定IP和端口
sk.bind(('0.0.0.0', 8000))

# 监听
sk.listen()


def oumei(url):
    ret = 'oumei   -  {}'.format(url)
    return ret.encode('utf-8')


def rihan(url):
    ret = 'rihan////   -  {}'.format(url)
    return ret.encode('utf-8')


def guochan(url):
    ret = 'guochan   -  {}'.format(url)
    return ret.encode('utf-8')


def index(url):
    with open('index.html', 'rb') as f:
        ret = f.read()
        return ret


def timer(url):
    now = time.strftime('%Y-%m-%d %H:%M:%S')
    with open('time.html', 'r', encoding='utf-8') as f:
        data = f.read()
        data = data.replace('@@time@@', now)

        return data.encode('utf-8')


list = [
    ('/oumei', oumei),
    ('/rihan', rihan),
    ('/guochan', guochan),
    ('/index', index),
    ('/time', timer),
]

while True:
    # 等待连接
    conn, addr = sk.accept()
    # 接收数据
    data = conn.recv(8096)
    data = data.decode('utf-8')
    url = data.split()[1]
    print(url)

    # 返回状态行
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    func = None
    for i in list:
        if i[0] == url:
            func = i[1]
            break

    if func:
        ret = func(url)
    else:
        ret = b'404 not found'

    conn.send(ret)

    # 关闭连接
    conn.close()

服务器程序和应用程序

正如文章开始所示,web框架本质就是一个socket服务端。

web框架功能:

  • socket收发消息
  • 根据不同的路径返回不同的内容
  • 可以返回动态页面(使用字符串替换等方式进行模板渲染)

对于真实开发中的python web程序来说,一般会分为两部分:服务器程序和应用程序。

服务器程序负责对socket服务器进行封装,并在请求到来时,对请求的各种数据进行整理。

应用程序则负责具体的逻辑处理。为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。

这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。

这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。

WSGI(Web Server Gateway Interface)就是一种规范,它定义了使用Python编写的web应用程序与web服务器程序之间的接口格式,实现web应用程序与web服务器程序间的解耦。

常用的WSGI服务器有uwsgi、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgiref,Django开发环境用的就是这个模块来做服务器。

为什么需要WSGI
在Web部署的方案上,有一个方案是目前应用最广泛的:

  首先,部署一个Web服务器专门用来处理HTTP协议层面相关的事情,比如如何在一个物理机上提供多个不同的Web服务(单IP多域名,单IP多端口等)这种事情。
  然后,部署一个用各种语言编写(Java, PHP, Python, Ruby等)的应用程序,这个应用程序会从Web服务器上接收客户端的请求,处理完成后,再返回响应给Web服务器,最后由Web服务器返回给客户端。
那么 Web Server 和 Application 之间就要知道如何进行交互。为了定义Web服务器和应用程序之间的交互过程,就形成了很多不同的规范。比如改进CGI性能的FasgCGI,Java专用的Servlet规范,还有Python专用的WSGI规范等。提出这些规范的目的就是为了定义统一的标准,提升程序的可移植性。

WSGI原理

 

 

 

WSGI 相当于 Web Server 和 Python Application 之间的桥梁,隐藏了很多HTTP相关的细节。其存在的目的有两个:

  让 Web Server 知道如何调用 Python Application,并且将 Client Request 传递给 Application。
  让 Python Application 能理解 Client Request 并执行对应操作,以及将执行结果返回给 Web Server,最终响应到Client。

Server 调用 Application

从上图可知,Server端调用Application端必须以WSGI为桥梁,因此WSGI定义了application可调用对象以提供Server端和Application端通信,这个可调用对象既可以是函数也可以是类。

# 函数形式
def application(environ, start_response):
    '''
    doing something
    '''
    return [response_body]

# 类形式
class Application:
    def __init__(self, environ, start_response):
        self.environ = environ 
        self.start = start_response

    def __iter__(self):
        status = ‘200 OK’
        response_headers = [(‘Content-type’, ‘text/plain’)]
        self.start(status, response_headers) 
        yield HELLO_WORLD

# 上面的类形式是将“Application”类作为服务端调用的application,
# 调用这个类会返回它的实例,其结果会返回规范中要求的可迭代响应值。
# 如果要使用这个类的实例作为application对象,就需要实现__call__方法,服务端会调用这个实例去执行应用。
# 下面是pecan的实现方法。
class PecanBase(object):
    def __init__(self, root, default_renderer='mako',
                 template_path='templates', hooks=lambda: [],
                 custom_renderers={}, extra_template_vars={},
                 force_canonical=True, guess_content_type_from_ext=True,
                 context_local_factory=None, request_cls=Request,
                 response_cls=Response, **kw):
    '''
    省略
    '''

    def __call__(self, environ, start_response):
        '''
        Implements the WSGI specification for Pecan applications, utilizing
        ``WebOb``.
        '''

        # create the request and response object
        req = self.request_cls(environ)
        resp = self.response_cls()
        state = RoutingState(req, resp, self)
        environ['pecan.locals'] = {
            'request': req,
            'response': resp
        }
        controller = None

        # track internal redirects
        internal_redirect = False

        # handle the request
        try:
            # add context and environment to the request
            req.context = environ.get('pecan.recursive.context', {})
            req.pecan = dict(content_type=None)

            controller, args, kwargs = self.find_controller(state)
            self.invoke_controller(controller, args, kwargs, state)
        except Exception as e:
            # if this is an HTTP Exception, set it as the response
            if isinstance(e, exc.HTTPException):
                # if the client asked for JSON, do our best to provide it
                accept_header = acceptparse.create_accept_header(
                    getattr(req.accept, 'header_value', '*/*') or '*/*')
                offers = accept_header.acceptable_offers(
                    ('text/plain', 'text/html', 'application/json'))
                best_match = offers[0][0] if offers else None
                state.response = e
                if best_match == 'application/json':
                    json_body = dumps({
                        'code': e.status_int,
                        'title': e.title,
                        'description': e.detail
                    })
                    if isinstance(json_body, six.text_type):
                        e.text = json_body
                    else:
                        e.body = json_body
                    state.response.content_type = best_match
                environ['pecan.original_exception'] = e

            # note if this is an internal redirect
            internal_redirect = isinstance(e, ForwardRequestException)

            # if this is not an internal redirect, run error hooks
            on_error_result = None
            if not internal_redirect:
                on_error_result = self.handle_hooks(
                    self.determine_hooks(state.controller),
                    'on_error',
                    state,
                    e
                )

            # if the on_error handler returned a Response, use it.
            if isinstance(on_error_result, WebObResponse):
                state.response = on_error_result
            else:
                if not isinstance(e, exc.HTTPException):
                    raise

            # if this is an HTTP 405, attempt to specify an Allow header
            if isinstance(e, exc.HTTPMethodNotAllowed) and controller:
                allowed_methods = _cfg(controller).get('allowed_methods', [])
                if allowed_methods:
                    state.response.allow = sorted(allowed_methods)
        finally:
            # if this is not an internal redirect, run "after" hooks
            if not internal_redirect:
                self.handle_hooks(
                    self.determine_hooks(state.controller),
                    'after',
                    state
                )

        self._handle_empty_response_body(state)

        # get the response
        return state.response(environ, start_response)

这个可调用对象需要满足两个条件:

两个参数
一个dict对象,Web Server会将HTTP请求相关的信息添加到这个字典中,供Web application使用
一个callback函数,Web application通过这个函数将HTTP status code和headers发送给Web Server
以字符串的形式返回response,并且包含在可迭代的list中
Server端将http请求相关信息、wsgi变量以及一些服务端环境变量添加到environ传给Application端,Application端处理完所需信息后将http状态码和header通过start_response回调函数传给Server端,而http响应body则以返回值的形式传给服务端。

可以看出,仅仅一个application(environ, start_response)仍然显得太底层,在web应用开发过程中效率不高,因此衍生出各种 Web 框架来帮助开发人员快速的开发Web应用,开发人员只需要关注业务层逻辑,不需要过多的处理http相关信息。

示例

在Python中就有一个WSGI server,提供给开发人员测试使用。

# WSGI server in Python
from wsgiref.simple_server import make_server

def application(environ, start_response):
    response_body = ['%s: %s' % (key, value)
                    for key, value in sorted(environ.items())]
    response_body = '\n'.join(response_body)
    
    status = '200 OK'
    response_headers = [('Content-Type', 'text/plain'),
                ('Content-Length', str(len(response_body)))]
    start_response(status, response_headers)
    
    return [response_body.encode('utf8')]

httpd = make_server(
    'localhost',
    8080,
    application
    )

# 请求处理完退出
httpd.handle_request()

访问http://localhost:8080返回结果:

ADSK_3DSMAX_X64_2018: C:\Program Files\Autodesk\3ds Max 2018\
ADSK_CLM_WPAD_PROXY_CHECK: FALSE
ALLUSERSPROFILE: C:\ProgramData
ANDROID_HOME: C:\Program Files (x86)\Android\android-sdk
APPDATA: C:\Users\ryan\AppData\Roaming
COMMONPROGRAMFILES: C:\Program Files\Common Files
COMMONPROGRAMFILES(X86): C:\Program Files (x86)\Common Files
COMMONPROGRAMW6432: C:\Program Files\Common Files
COMPUTERNAME: LAPTOP-11PD23IA
COMSPEC: C:\WINDOWS\system32\cmd.exe
CONTENT_LENGTH: 
CONTENT_TYPE: text/plain
DRIVERDATA: C:\Windows\System32\Drivers\DriverData
FPS_BROWSER_APP_PROFILE_STRING: Internet Explorer
FPS_BROWSER_USER_PROFILE_STRING: Default
GATEWAY_INTERFACE: CGI/1.1
HOMEDRIVE: C:
HOMEPATH: \Users\ryan
HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
HTTP_ACCEPT_ENCODING: gzip, deflate, br
HTTP_ACCEPT_LANGUAGE: zh-CN,zh;q=0.9
HTTP_CONNECTION: keep-alive
HTTP_HOST: 127.0.0.1:8080
HTTP_SEC_CH_UA: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
HTTP_SEC_CH_UA_MOBILE: ?0
HTTP_SEC_CH_UA_PLATFORM: "Windows"
HTTP_SEC_FETCH_DEST: document
HTTP_SEC_FETCH_MODE: navigate
HTTP_SEC_FETCH_SITE: none
HTTP_SEC_FETCH_USER: ?1
HTTP_UPGRADE_INSECURE_REQUESTS: 1
HTTP_USER_AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
IDEA_INITIAL_DIRECTORY: C:\Users\ryan\Desktop
JAVA_HOME: C:\Program Files\Java\jdk1.8.0_151
LOCALAPPDATA: C:\Users\ryan\AppData\Local
LOGONSERVER: \\LAPTOP-11PD23IA
NUMBER_OF_PROCESSORS: 12
ONEDRIVE: C:\Users\ryan\OneDrive
ONLINESERVICES: Online Services
OS: Windows_NT
PATH: C:\Program Files\ImageMagick-7.1.0-Q16-HDRI;C:\Program Files\PlasticSCM5\server;C:\Program Files\PlasticSCM5\client;C:\ProgramData\Oracle\Java\javapath;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python39_64;C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python39_64\Scripts;C:\Users\ryan\AppData\Roaming\Python\Python39\Scripts;C:\Program Files\Java\jdk1.8.0_151\bin;C:\Program Files\Java\jdk1.8.0_151\lib\tools.jar;C:\Program Files\Java\jdk1.8.0_151\jre\bin;C:\Program Files (x86)\Android\android-sdk\tools;C:\Program Files (x86)\Android\android-sdk\platform-tools;C:\Program Files\Common Files\Autodesk Shared\;C:\Program Files (x86)\Autodesk\Backburner\;C:\Program Files\nodejs\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Program Files\MySQL\MySQL Shell 8.0\bin\;C:\Users\ryan\AppData\Local\Microsoft\WindowsApps;;C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\bin;;C:\Users\ryan\AppData\Roaming\npm;C:\Users\ryan\.dotnet\tools
PATHEXT: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
PATH_INFO: /
PLATFORMCODE: KV
PROCESSOR_ARCHITECTURE: AMD64
PROCESSOR_IDENTIFIER: Intel64 Family 6 Model 158 Stepping 13, GenuineIntel
PROCESSOR_LEVEL: 6
PROCESSOR_REVISION: 9e0d
PROGRAMDATA: C:\ProgramData
PROGRAMFILES: C:\Program Files
PROGRAMFILES(X86): C:\Program Files (x86)
PROGRAMW6432: C:\Program Files
PSMODULEPATH: C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules
PUBLIC: C:\Users\Public
PYCHARM COMMUNITY EDITION: C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\bin;
PYCHARM_HOSTED: 1
PYTHONIOENCODING: UTF-8
PYTHONPATH: D:\Develop\BackEnd\workkspace
PYTHONUNBUFFERED: 1
QUERY_STRING: 
REGIONCODE: APJ
REMOTE_ADDR: 127.0.0.1
REMOTE_HOST: 
REQUEST_METHOD: GET
SCRIPT_NAME: 
SERVER_NAME: LAPTOP-11PD23IA
SERVER_PORT: 8080
SERVER_PROTOCOL: HTTP/1.1
SERVER_SOFTWARE: WSGIServer/0.2
SESSIONNAME: Console
SSLKEYLOGFILE: D:\Develop\log
SYSTEMDRIVE: C:
SYSTEMROOT: C:\WINDOWS
TEMP: C:\Users\ryan\AppData\Local\Temp
TMP: C:\Users\ryan\AppData\Local\Temp
USERDOMAIN: LAPTOP-11PD23IA
USERDOMAIN_ROAMINGPROFILE: LAPTOP-11PD23IA
USERNAME: ryan
USERPROFILE: C:\Users\ryan
WINDIR: C:\WINDOWS
wsgi.errors: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>
wsgi.file_wrapper: <class 'wsgiref.util.FileWrapper'>
wsgi.input: <_io.BufferedReader name=604>
wsgi.multiprocess: False
wsgi.multithread: False
wsgi.run_once: False
wsgi.url_scheme: http
wsgi.version: (1, 0)

environ参数
environ字典包含了一些CGI规范要求的数据,以及WSGI规范新增的数据,还可能包含一些操作系统的环境变量以及Web服务器相关的环境变量。

CGI规范中要求的变量:

  REQUEST_METHOD: 请求方法,是个字符串,‘GET’, 'POST’等
  SCRIPT_NAME: HTTP请求的path中的用于查找到application对象的部分,比如Web服务器可以根据path的一部分来决定请求由哪个virtual host处理
  PATH_INFO: HTTP请求的path中剩余的部分,也就是application要处理的部分
  QUERY_STRING: HTTP请求中的查询字符串,URL中?后面的内容
  CONTENT_TYPE: HTTP headers中的content-type内容
  CONTENT_LENGTH: HTTP headers中的content-length内容
  SERVER_NAME和SERVER_PORT: 服务器名和端口,这两个值和前面的SCRIPT_NAME, PATH_INFO拼起来可以得到完整的URL路径
  SERVER_PROTOCOL: HTTP协议版本,HTTP/1.0或者HTTP/1.1
  HTTP_: 和HTTP请求中的headers对应。

WSGI规范中相关变量:

  wsgi.version:表示WSGI版本,一个元组(1, 0),表示版本1.0
  wsgi.url_scheme:http或者https
  wsgi.input:一个类文件的输入流,application可以通过这个获取HTTP request body
  wsgi.errors:一个输出流,当应用程序出错时,可以将错误信息写入这里
  wsgi.multithread:当application对象可能被多个线程同时调用时,这个值需要为True
  wsgi.multiprocess:当application对象可能被多个进程同时调用时,这个值需要为True
  wsgi.run_once:当server期望application对象在进程的生命周期内只被调用一次时,该值为True
  start_resposne参数
  start_response是一个可调用对象,接收两个必选参数和一个可选参数:

  status: 一个字符串,表示HTTP响应状态字符串
  response_headers: 一个列表,包含有如下形式的元组:(header_name, header_value),用来表示HTTP响应的headers
  exc_info(可选): 用于出错时,server需要返回给浏览器的信息

application对象的返回值

application对象的返回值用于为HTTP响应提供body,如果没有body,那么可以返回None。如果有body,那么需要返回一个可迭代的对象。server端通过遍历这个可迭代对象可以获得body的全部内容。

WSGI的实现和部署

  要使用WSGI,需要分别实现server端和application端。

  Application端的实现一般是由Python的各种框架来实现的,比如Django, web.py等,一般开发者不需要关心WSGI的实现,框架会会提供接口让开发者获取HTTP请求的内容以及发送HTTP响应。

  Server端的实现会比较复杂一点,这个主要是因为软件架构的原因。一般常用的Web服务器,如Apache和nginx,都不会内置WSGI的支持,而是通过扩展来完成。比如Apache服务器,会通过扩展模块mod_wsgi来支持WSGI。Apache和mod_wsgi之间通过程序内部接口传递信息,mod_wsgi会实现WSGI的server端、进程管理以及对application的调用。Nginx上一般是用proxy的方式,用nginx的协议将请求封装好,发送给应用服务器,比如uWSGI,应用服务器会实现WSGI的服务端、进程管理以及对application的调用。

基于wsgiref模块的web框架

import time
from wsgiref.simple_server import make_server


# 将返回不同的内容部分封装成函数
def index(url):
    with open("index.html", "r", encoding="utf8") as f:
        s = f.read()
        now = str(time.time())
        s = s.replace("@@oo@@", now)
    return bytes(s, encoding="utf8")


def home(url):
    with open("home.html", "r", encoding="utf8") as f:
        s = f.read()
    return bytes(s, encoding="utf8")


# 定义一个url和实际要执行的函数的对应关系
list1 = [
    ("/index/", index),
    ("/home/", home),
]


def run_server(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ])  # 设置HTTP响应的状态码和头信息
    url = environ['PATH_INFO']  # 取到用户输入的url
    func = None
    for i in list1:
        if i[0] == url:
            func = i[1]
            break
    if func:
        response = func(url)
    else:
        response = b"404 not found!"
    return [response, ]


if __name__ == '__main__':
    httpd = make_server('127.0.0.1', 8090, run_server)
    print("我在8090等你哦...")
    httpd.serve_forever()

jinja2
上面的代码实现了一个简单的动态,我完全可以从数据库中查询数据,然后去替换我html中的对应内容,然后再发送给浏览器完成渲染。 这个过程就相当于HTML模板渲染数据。 本质上就是HTML内容中利用一些特殊的符号来替换要展示的数据。 我这里用的特殊符号是我定义的,其实模板渲染有个现成的工具: jinja2。    安装:pip install jinja2

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="x-ua-compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Title</title>
</head>
<body>
    <h1>姓名:{{name}}</h1>
    <h1>爱好:</h1>
    <ul>
        {% for hobby in hobby_list %}
        <li>{{hobby}}</li>
        {% endfor %}
    </ul>
</body>
</html>

使用jinja2渲染index2.html文件:

from wsgiref.simple_server import make_server
from jinja2 import Template


def index():
    with open("index2.html", "r") as f:
        data = f.read()
    template = Template(data)  # 生成模板文件
    ret = template.render({"name": "Alex", "hobby_list": ["烫头", "泡吧"]})  # 把数据填充到模板里面
    return [bytes(ret, encoding="utf8"), ]


def home():
    with open("home.html", "rb") as f:
        data = f.read()
    return [data, ]


# 定义一个url和函数的对应关系
URL_LIST = [
    ("/index/", index),
    ("/home/", home),
]


def run_server(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ])  # 设置HTTP响应的状态码和头信息
    url = environ['PATH_INFO']  # 取到用户输入的url
    func = None  # 将要执行的函数
    for i in URL_LIST:
        if i[0] == url:
            func = i[1]  # 去之前定义好的url列表里找url应该执行的函数
            break
    if func:  # 如果能找到要执行的函数
        return func()  # 返回函数的执行结果
    else:
        return [bytes("404没有该页面", encoding="utf8"), ]


if __name__ == '__main__':
    httpd = make_server('', 8000, run_server)
    print("Serving HTTP on port 8000...")
    httpd.serve_forever()

现在的数据是我们自己手写的,那可不可以从数据库中查询数据,来填充页面呢?

使用pymysql连接数据库:

conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", passwd="xxx", db="xxx", charset="utf8")
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
cursor.execute("select name, age, department_id from userinfo")
user_list = cursor.fetchall()
cursor.close()
conn.close()

创建一个测试的user表:

CREATE TABLE user(
  id int auto_increment PRIMARY KEY,
  name CHAR(10) NOT NULL,
  hobby CHAR(20) NOT NULL
)engine=innodb DEFAULT charset=UTF8;

模板的原理就是字符串替换,我们只要在HTML页面中遵循jinja2的语法规则写上,其内部就会按照指定的语法进行相应的替换,从而达到动态的返回内容。

posted @ 2022-10-07 23:52  Einewhaw  阅读(76)  评论(0编辑  收藏  举报