手写Web框架

Web框架简介

Python的web开发有很多现成的框架:比如,大名鼎鼎的Django,短小精悍的Flask,牛气哄哄的Tornado,以及最近比较火的FastAPI。这些都是别人封装好的web开发框架,我们直接拿来使用就好。不过,也不是说随随便便就能玩的,你是用这些框架之前,肯定需要对网络协议,网络通信这些概念有一些认识,这样才能玩得转。

你可能感觉他们高高在上的很牛逼,但是其实他们本质上,还是基于最基础的网络通信,socket套接字等这些基础概念。今天我们就尝试着,亲自动手写一个简单版的web框架,目的是为了理解web开发框架的基本实现原理。

Web软件开发分两种,一种是BS架构软件,另一种是CS架构软件。两者本质上是一致的,不论是BS还是CS,本质上都是CS(BS是一个特殊的CS,浏览是充当了客户端软件)。

这里,我们使用BS架构的软件为例,介绍BS软件开发框架的基本实现流程。我们知道,BS模式是浏览器-服务端,也就是说我们在开发此类web应用程序(网站)的时候,只需要做服务端的工作,市面上的各大浏览器直接充当客户端即可。这儿时候问题就出现了,要想让浏览器这个客户端认识我们的服务端,我们在开发服务端的时候一定要遵循某种协议规范,这样浏览器-服务端才能顺利通信,否则彼此语言不同,是无法完成通信的。这个协议就是HTTP协议。在此之前请自行参考学习HTTP协议的基本常识。

版本1

版本一:使用socket模块,并按照HTTP协议的规范,实现浏览器-客户端通信

需求:在浏览器网址输入框中,根据用户的输入,给用户展示不同的页面内容

my_first_web_fram.py

import socket

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)


while True:
    conn, addr = server.accept()

    data = conn.recv(1024).decode('utf-8') 	# 接受浏览器输入的url:http://127.0.0.1:8080/index

    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')	# 按照HTTP协议发送响应头和响应首行以及空格

    current_path = data.split(' ')[1]		# 分割请求体的数据,获取url中携带的index

    if current_path == '/index':		# 条件判读,响应不同的内容
        conn.send(b'index')			# 因为TCP协议的黏包问题,所以可以将响应体单独发
    elif current_path == '/login':
        conn.send(b'login')
    else:
        conn.send(b'hello web')

    conn.close()

补充:data = conn.recv(1024).decode('utf-8') 接收的数据格式

GET /index HTTP/1.1			# 第一条请求消息中,希望获取的数据
Host: 127.0.0.1:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,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


GET /favicon.ico HTTP/1.1		# 第二个请求消息展示不考虑
Host: 127.0.0.1:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:8080/index
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

版本2

版本1有很多可以优化的问题:

1.代码重复(服务端代码所有人都要重复写)
2.手动处理http格式的数据,并且只能拿到url后缀,其他数据获取繁琐(数据格式一样处理的代码其实也大致一样 重复写)
3.并发的问题

为了解决这些,我们需要借助一些模块,首先介绍wsgiref模块,

wsgiref模块可以帮我们处理请求信息的处理(避免手动分割请求数据)和响应信息的处理(避免手动处理响数据的格式)。

my_second_web_fram.py

from wsgiref.simple_server import make_server
from views import *


# url与函数的对应关系, 这个对应关系可以单独放在一个urls.py文件
urls = [
    ('/index', index),
    ('/login', login),
    ('/xxx', xxx),
    ('/get_time', get_time),
    ('/get_dict', get_dict),
    ('/get_user', get_user)
]

def run(env, response):
    """
    :param env:请求相关的所有数据
    :param response:响应相关的所有数据
    :return: 返回给浏览器的数据
    """
    # print(env)                    # 大字典  wsgiref模块帮你处理好http格式的数据 封装成了字典让你更加方便的操作
    
    response('200 OK', [])          # 响应首行 响应头
    current_path = env.get('PATH_INFO')
    
    func = None
    for url in urls: 
        if current_path == url[0]:
            func = url[1]
            break  
    if func:
        res = func(env)
    else:
        res = error(env)

    return [res.encode('utf-8')]


if __name__ == '__main__':
    server = make_server('127.0.0.1', 8080, run)
    """
    会实时监听127.0.0.1:8080地址 只要有客户端来了
    都会交给run函数处理(加括号触发run函数的运行)
    """
    server.serve_forever()  # 启动服务端

这个版本中,我们使用的wsgiref,它里面的make_server()封装了socket操作,将自动监听服务端的请求,然后将请求事件交给run函数处理。

另外,它自动将请求数据帮我们转成了字典,我们通过字典的方法可以方便的取出需要的数据。处理后的这个字典就是run函数中的参数env

最后我们在run函数中根据url与具体视图函数的对应关系,执行具体的函数,完成客户端的请求处理。

这里面,我们处理url的函数单独拎了出来,放在一个views.py文件。具体的代码无关紧要,重要的是理解这个流程。并且,我们在views视图函数中,不仅可以返回一个字符串,还是可以返回html页面或者实现户后端数据交给前段显示的。

views.py

from jinja2 import Template
import pymysql

def index(env):
    return 'index'


def login(env):
    return "login"


def error(env):
    return '404 error'


def get_dict(env):
    user_dic = {'username':'jason','age':18,'hobby':'read'}
    with open(r'templates/04 get_dict.html','r',encoding='utf-8') as f:
        data = f.read()
    tmp = Template(data)
    res = tmp.render(user=user_dic)
    # 给get_dict.html传递了一个值 页面上通过变量名user就能够拿到user_dict
    return res


def get_user(env):
    # 去数据库中获取数据 传递给html页面 借助于模版语法 发送给浏览器
    conn = pymysql.connect(
        host = '127.0.0.1',
        port = 3306,
        user = 'root',
        password = 'admin123',
        db='day59',
        charset = 'utf8',
        autocommit = True
    )
    cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
    sql = 'select * from userinfo'
    affect_rows = cursor.execute(sql)
    data_list = cursor.fetchall()  # [{},{},{}]
    # 将获取到的数据传递给html文件
    with open(r'templates/05 get_data.html','r',encoding='utf-8') as f:
        data = f.read()
    tmp = Template(data)
    res = tmp.render(user_list=data_list)
    # 给get_dict.html传递了一个值 页面上通过变量名user就能够拿到user_dict
    return res

templates/04 get_dict.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>我是一个页面</h1>
{{ user }}						
{{ user.get('username')}}		<!--模版语法-->
{{ user.age }}
{{ user['hobby'] }}
</body>
</html>

补充jinja2模块。

这个模块是三方模块需要pip下载。它的功能是将数据渲染到html页面内,可以在html页面内使用jinja2的语法,实现后端数据实时显示到前端页面。需要注意的是,该模版语法是在后端起作用的。该模块很强大,flask模块里面使用的就是jinja2

# 模版语法(非常贴近python语法)
{{ user }}
{{ user.get('username')}}
{{ user.age }}
{{ user['hobby'] }}


{% for user_dict in user_list %}
    <tr>
        <td>{{ user_dict.id}}</td>
        <td>{{ user_dict.username}}</td>
        <td>{{ user_dict.password}}</td>
        <td>{{ user_dict.hobby}}</td>
    </tr>
{% endfor%}

手写框架总结

简易版本web框架流程图

流程1:浏览器请求、wsgiref解析封装数据、urls路由分发、views视图逻辑处理、返回模版html页面、wsgiref打包封装、交给浏览器展示数据。

流程2:浏览器请求、wsgiref解析封装数据、urls路由分发、views视图逻辑处理、调用数据库数据、渲染到模版html页面上、返回模版html页面、wsgiref打包封装、交给浏览器展示数据。

三大主流框架介绍

"""
django
	特点:大而全 自带的功能特别特别特别的多 类似于航空母舰
	不足之处:
		有时候过于笨重

flask
	特点:小而精  自带的功能特别特别特别的少 类似于游骑兵
	第三方的模块特别特别特别的多,如果将flask第三方的模块加起来完全可以盖过django
	并且也越来越像django
	不足之处:
		比较依赖于第三方的开发者
		
tornado
	特点:异步非阻塞 支持高并发
		牛逼到甚至可以开发游戏服务器
	不足之处:
		暂时你不会
"""
# 框架实现分三部分
 -A:socket部分
 -B:路由与视图函数对应关系(路由匹配)
 -C:模版语法

django
  A用的是别人的		wsgiref模块
  B用的是自己的
  C用的是自己的(没有jinja2好用 但是也很方便)

flask
  A用的是别人的		werkzeug(内部还是wsgiref模块)
  B自己写的
  C用的别人的(jinja2)

tornado
  A,B,C都是自己写的
posted @ 2020-05-22 16:26  the3times  阅读(397)  评论(0编辑  收藏  举报