一、Web框架的本质:

  我们可以这样理解:所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端。 这样我们就可以自己实现Web框架了。

  • 半成品自定义web框架

    import socket
    
    server = socket.socket()
    server.bind(("127.0.0.1", 8080))
    server.listen() while True:
    conn, addr = server.accept()
    data = conn.recv(8096)
    conn.send(b"OK")
    conn.close()

    可以说Web服务本质上都是在这十几行代码基础上扩展出来的。这段代码是web服务的核心。
    那么用户的浏览器输入一个网址,会给服务端发送数据,那么服务端会发送什么格式的数据来进行浏览器与服务端直接的收发通信呢?
    必须有一个统一的规则,让双方发送消息、接收消息的时候有个格式依据,这样就不会出乱子。
    这个规则就是HTTP-超文本传输协议
    先看看浏览器访问服务端会发生什么:
    前端Web框架的实现过程-LMLPHP

    import socket
    
    server = socket.socket()
    server.bind(("127.0.0.1", 8080))
    server.listen() while True:
    conn, addr = server.accept()
    data = conn.recv(8096)
    print(data)
    conn.send(b"OK")
    conn.close()

    输出:

    b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nDNT: 1\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: csrftoken=RKBXh1d3M97iz03Rpbojx1bR6mhHudhyX5PszUxxG3bOEwh1lxFpGOgWN93ZH3zv\r\n\r\n'

    这就是浏览器给服务端发送的请求数据,接下来得知道,HTTP请求和相应的格式:

    数据格式之GET请求:
      请求首行
           请求头(一堆k,v键值对)

    请求体(post请求携带的数据)
    数据格式之响应:
      响应首行
      响应头(一堆k,v键值对)

      响应体(post请求携带的数据)
    前端Web框架的实现过程-LMLPHP

    前端Web框架的实现过程-LMLPHP

    接下来继续完善web框架:(根据不同路径返回不同的内容):

    '''
    根据URL中不同的路径返回不同的内容
    ''' import socket # 创建server服务连接
    server = socket.socket()
    server.bind(('127.0.0.1', 8080)) # 绑定服务端IP和端口(本机做服务端则为127.0.0.1,这样socket就会自动识别。
    server.listen(5) # 半连接池 # 下面是通信循环:
    while True:
    conn, addr = server.accept() # 等待客户的连接,这个地方是阻塞点
    data_bytes = conn.recv(1024) # 接收客户的发来的请求数据
    data = data_bytes.decode('utf-8') # 解码收到的数据
    data1 = data.split('\r\n')[0] # 分离出第一行数据(第一个\r\n之前的数据)
    url = data1.split()[1] # 继续分离,用空格来分割,然后取索引为1的元素就是请求的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同的内容:
    if url == '/index':
    response = b'index'
    elif url == '/home':
    response = b'home'
    else:
    response = b'404 not found!'
    conn.send(response)
    conn.close()

    前端Web框架的实现过程-LMLPHP

    前端Web框架的实现过程-LMLPHP

    继续优化:用函数来返回不同的内容:

    import socket
    
    # 创建server服务连接
    server = socket.socket()
    server.bind(('127.0.0.1', 8080)) # 绑定服务端IP和端口(本机做服务端则为127.0.0.1,这样socket就会自动识别。
    server.listen(5) # 半连接池 def index(url):
    response = b'index'
    return response def home(url):
    response = b'home'
    return response url_list = [
    ('/index', index),
    ('/home', home),
    ] # 下面是通信循环:
    while True:
    conn, addr = server.accept() # 等待客户的连接,这个地方是阻塞点
    data_bytes = conn.recv(1024) # 接收客户的发来的请求数据
    data = data_bytes.decode('utf-8') # 解码收到的数据
    data1 = data.split('\r\n')[0] # 分离出第一行数据(第一个\r\n之前的数据)
    url = data1.split()[1] # 继续分离,用空格来分割,然后取索引为1的元素就是请求的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同的内容:
    func = None
    for i in url_list:
    if i[0] == url:
    func = i[1]
    break
    if func:
    response = func(url)
    else:
    response = b'404 not found!' conn.send(response)
    conn.close()

    返回具体的HTML文件

    完美解决了不同URL返回不同内容的问题。 但是我不想仅仅返回几个字符串,我想给浏览器返回完整的HTML内容,这又该怎么办呢?

    没问题,不管是什么内容,最后都是转换成字节数据发送出去的。 我们可以打开HTML文件,读取出它内部的二进制数据,然后再发送给浏览器。

    import socket
    
    # 创建server服务连接
    server = socket.socket()
    server.bind(('127.0.0.1', 8080)) # 绑定服务端IP和端口(本机做服务端则为127.0.0.1,这样socket就会自动识别。
    server.listen(5) # 半连接池 def index(url):
    # 读取index.html页面的内容
    with open("index.html", "r", encoding="utf8") as f:
    s = f.read()
    # 返回字节数据
    return s.encode('utf-8') def home(url):
    # 读取index.html页面的内容
    with open("home.html", "r", encoding="utf8") as f:
    s = f.read()
    # 返回字节数据
    return s.encode('utf-8') url_list = [
    ('/index', index),
    ('/home', home),
    ] # 下面是通信循环:
    while True:
    conn, addr = server.accept() # 等待客户的连接,这个地方是阻塞点
    data_bytes = conn.recv(1024) # 接收客户的发来的请求数据
    data = data_bytes.decode('utf-8') # 解码收到的数据
    data1 = data.split('\r\n')[0] # 分离出第一行数据(第一个\r\n之前的数据)
    url = data1.split()[1] # 继续分离,用空格来分割,然后取索引为1的元素就是请求的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同的内容:
    func = None
    for i in url_list:
    if i[0] == url:
    func = i[1]
    break
    if func:
    response = func(url)
    else:
    response = b'404 not found!' conn.send(response)
    conn.close()

    让网页动态起来

    这网页能够显示出来了,但是都是静态的啊。页面的内容都不会变化的,我想要的是动态网站。

    没问题,我也有办法解决。我选择使用字符串替换来实现这个需求。(这里使用时间戳来模拟动态的数据)

    import socket
    import time # 创建server服务连接
    server = socket.socket()
    server.bind(('127.0.0.1', 8080)) # 绑定服务端IP和端口(本机做服务端则为127.0.0.1,这样socket就会自动识别。
    server.listen(5) # 半连接池 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 s.encode('utf-8') def home(url):
    # 读取index.html页面的内容
    with open("index.html", "r", encoding="utf8") as f:
    s = f.read()
    # 返回字节数据
    return s.encode('utf-8') url_list = [
    ('/index', index),
    ('/home', home),
    ] # 下面是通信循环:
    while True:
    conn, addr = server.accept() # 等待客户的连接,这个地方是阻塞点
    data_bytes = conn.recv(1024) # 接收客户的发来的请求数据
    data = data_bytes.decode('utf-8') # 解码收到的数据
    data1 = data.split('\r\n')[0] # 分离出第一行数据(第一个\r\n之前的数据)
    url = data1.split()[1] # 继续分离,用空格来分割,然后取索引为1的元素就是请求的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同的内容:
    func = None
    for i in url_list:
    if i[0] == url:
    func = i[1]
    break
    if func:
    response = func(url)
    else:
    response = b'404 not found!' conn.send(response)
    conn.close()

    对于真实开发中的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开发环境用的就是这个模块来做服务器。
    前端Web框架的实现过程-LMLPHP

    服务器程序,正常开发环境下用wsgiref,而在正式假设服务上线用uWSGI
    接下来利用wsgiref模块来替换上面写的web框架的socket server部分:

    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 s def home(url):
    # 读取index.html页面的内容
    with open("index.html", "r", encoding="utf8") as f:
    s = f.read()
    return s def err():
    return '404 not found!' url_list = [
    ('/index', index),
    ('/home', home),
    ] def run(env, response):
    ''' :param env: 请求相关信息,一个大字典,里面装了一堆处理好了的键值对数据
    :param response: 相应相关信息
    :return:
    '''
    # 固定写法 后面列表里面一个个元祖会以响应头kv键值对的形式返回给客户端
    response('200 ok', [('username', 'sgt'), ('password', '')])
    # 获取用户访问路径:
    current_path = env.get('PATH_INFO')
    # 定义一个存储函数名的变量:
    func = None
    for i in url_list:
    if i[0] == current_path:
    func = i[1]
    break
    if func:
    res = func()
    else:
    res = err() return [res.encode('utf-8')] if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, run)
    server.serve_forever()

    jinja2

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

    下载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>

    index2 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的语法规则写上,其内部就会按照指定的语法进行相应的替换,从而达到动态的返回内容。

05-29 00:52