web框架原理
web框架的原理:
所有的web应用其实本质上就是socket服务端,而我们的浏览器就是socket客户端。
那么知道了这个之后我们就可以基于socket来写一个我们的服务端:
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) conn.send(b'OK') conn.close()
但是有一个问题,我们如果使用这种方式的话,在页面上我们是没办法显示我们的一个服务端返还的内容的,这是为什么呢?
那么我们首先看下客户端发送过来的请求是什么样的:
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)
print(data) #这里打印我们客户端发送过来的请求 conn.send(b'OK') conn.close()
结果为:
那么我们基于网络进行收发消息那么就需要遵守我们的HTTP协议:
那么我们从一个实际的网站来看一下实际的请求以及响应的格式是什么样的?
请求头:request header
响应头:response header
那么既然收发消息都是按照HTTP协议来的,我们就来了解下和HTTP协议的格式:
每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。 HTTP响应的Header中有一个 Content-Type
表明响应的内容格式。如 text/html
表示HTML网页。
HTTP GET请求的格式:
HTTP响应的格式:
第一个简单丑陋的web框架:
import socket s = socket.socket() s.bind(('127.0.0.1', 8080)) s.listen(5) while True: conn, addr = s.accept() data = conn.recv(1024) print(data) with open('index.html', 'rb')as f: #这里我们可以把index.html另外创建一个HTML格式的文件,我们通过读取这个文件来达到把我们的HTML中的代码显示到浏览器。 data = f.read() conn.send(b'HTTP/1.1 200 OK\r\n\r\n%s' % data) #按照我们上面所讲的HTTP协议中的格式,进行相应。 conn.close()
对我们的框架进行进一步优化:
""" 根据URL中不同的路径返回不同的内容 """ import socket server = socket.socket() server.bind(("127.0.0.1", 8080)) # 绑定IP和端口 server.listen() # 监听 while 1: # 等待连接 conn, add = server.accept() data = conn.recv(8096) # 接收客户端发来的消息 # 从data中取到路径 data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串 # 按\r\n分割 data1 = data.split("\r\n")[0] url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径 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()
上述代码我们实现了一个,对于不同请求的分别相应。
根据不同的路径返回不同的内容--函数版:
上面的代码解决了不同URL路径返回不同内容的需求。
但是问题又来了,我们知道对于一个服务器来说他有很多的路径需要判断,难道我们都用if else,太麻烦了,我们使用函数。
""" 根据URL中不同的路径返回不同的内容--函数版 """ import socket sk = socket.socket() sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口 sk.listen() # 监听 # 将返回不同的内容部分封装成函数 def index(url): s = "这是{}页面!".format(url) return bytes(s, encoding="utf8") def home(url): s = "这是{}页面!".format(url) return bytes(s, encoding="utf8") while 1: # 等待连接 conn, add = sk.accept() data = conn.recv(8096) # 接收客户端发来的消息 # 从data中取到路径 data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串 # 按\r\n分割 data1 = data.split("\r\n")[0] url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行 # 根据不同的路径返回不同内容,response是具体的响应体 if url == "/index/": response = index(url) elif url == "/home/": response = home(url) else: response = b"404 not found!" conn.send(response) conn.close()
其实上述的方法依然不够好:
函数进阶版:
我们使用字典把每个URL地址与相应的函数使用key:value的形式对应起来:
""" 根据URL中不同的路径返回不同的内容--函数进阶版 """ import socket sk = socket.socket() sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口 sk.listen() # 监听 # 将返回不同的内容部分封装成函数 def index(url): s = "这是{}页面!".format(url) return bytes(s, encoding="utf8") def home(url): s = "这是{}页面!".format(url) return bytes(s, encoding="utf8") # 定义一个url和实际要执行的函数的对应关系 list1 = [ ("/index/", index), ("/home/", home), ] while 1: # 等待连接 conn, add = sk.accept() data = conn.recv(8096) # 接收客户端发来的消息 # 从data中取到路径 data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串 # 按\r\n分割 data1 = data.split("\r\n")[0] url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行 # 根据不同的路径返回不同内容 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!" # 返回具体的响应消息 conn.send(response) conn.close()
上述的优化知识又花了我们的服务端的代码,但是我们的网页显示出来的还是原来的一连串的字符,那么如何让我们的网页看起来特别的炫酷呢:
我们可以在 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') 直接跟上我们的HTML文件,那么就可以达到我们要求的页面效果了。
""" 根据URL中不同的路径返回不同的内容--函数进阶版 返回独立的HTML页面 """ import socket sk = socket.socket() sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口 sk.listen() # 监听 # 将返回不同的内容部分封装成函数 def index(url): # 读取index.html页面的内容 with open("index.html", "r", encoding="utf8") as f: s = f.read() # 返回字节数据 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), ] while 1: # 等待连接 conn, add = sk.accept() data = conn.recv(8096) # 接收客户端发来的消息 # 从data中取到路径 data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串 # 按\r\n分割 data1 = data.split("\r\n")[0] url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行 # 根据不同的路径返回不同内容 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!" # 返回具体的响应消息 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开发环境用的就是这个模块来做服务器。
wsgiref
我们通过wsgiref来对我们的服务端代码进行一个封装。
from wsgiref.simple_server import make_server def application(environ, start_response): # 按照HTTP协议解析数据:environ # 按照HTTP协议组装数据:start_response print(environ) print(type(environ)) start_response('200 OK', []) # 拿到当前请求路径 path = environ.get('PATH_INFO') if path == "/login": with open('login.html', 'rb')as f: data = f.read() elif path == "/index": with open('index.html', 'rb')as f: data = f.read() return [data] httped = make_server('127.0.0.1', 8080, application) # 封装socket # 等待用户连接 httped.make_server() from wsgiref.simple_server import make_server def application(environ,start_response): start_response('200 OK',[])#按照HTTP协议组装数据数据。 # 这一步其实就相当于我们的[b''] path=environ.get('PATH_INFO')#按照HTTP协议解析数据
那么既然有对socket代码进行封装的,肯定也有对页面渲染进行封装的模块了:
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>
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的语法规则写上,其内部就会按照指定的语法进行相应的替换,从而达到动态的返回内容。
静态文件配置:
STATIC_URL = '/static/' # HTML中使用的静态文件夹前缀 STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), # 静态文件存放位置 ]
看不明白?有图有真相:
刚开始学习时可在配置文件中暂时禁用csrf中间件,方便表单提交测试。
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
Django基础必备三件套:
from django.shortcuts import HttpResponse, render, redirect
HttpResponse
内部传入一个字符串参数,返回给浏览器。
例如:
def index(request): # 业务逻辑代码 return HttpResponse("OK")
render
除request参数外还接受一个待渲染的模板文件和一个保存具体数据的字典参数。
将数据填充进模板文件,最后把结果返回给浏览器。(类似于我们上面用到的jinja2)
例如:
def index(request): # 业务逻辑代码 return render(request, "index.html", {"name": "alex", "hobby": ["烫头", "泡吧"]})
redirect
接受一个URL参数,表示跳转到指定的URL。
例如:
def index(request): # 业务逻辑代码 return redirect("/home/")
重定向是怎么回事?
课后练习:
Django版登录
启动Django报错:
Django 启动时报错 “UnicodeEncodeError ...” 报这个错误通常是因为计算机名为中文,改成英文的计算机名重启下电脑就可以了。 Django 启动报错“SyntaxError: Generator expression must be parenthesized” 报这个错很大可能是因为使用了Python3.7.0,而目前(2018-06-12)Python3.7.0和Django还有点兼容性问题,换回Python3.6的环境即可。
也就是说是不安全的
这就是为什么get请求一般只是用来进行查询的原因。