WSGI及web服务器
WSGI协议
目前常用的服务器:nginx
浏览器动态请求时的三层:浏览器<-->web服务器<-->应用程序(定义了application处)
如何让nginx可以支持自己写的框架?
只要按照WSGI规则,就可以让其他的服务器支持自己写的框架
-
定义一个application函数
-
这个application函数接收两个参数:一个字典,一个函数引用
- environ:一个包含所有http请求信息的dict对象
- start_response:一个发送http响应的函数
eg:
def application(environ, start_response):
# 注意,形参名没有规定
start_response('200 OK',[('Content-Type', 'text/html')])
return "hello world"
整个application()函数本身没有涉及到任何解析http的部分,也就是说,把底层web服务器和应用程序逻辑部分进行了分离,这样开发者就可以专心做应该领域了
在application函数里面,必须调用传过去的函数(在调用时,就可以给该函数传header,然后服务器就能够收到该header),当application函数结束的时候,会return body。此时,服务器就可以将header和application返回的body返回给浏览器
WSGI实现了服务器和框架的解耦
在application函数中执行参数函数时,第一个参数为状态码,第二个参数中的每一个元组,就表示response中的一行数据(通常有多个元组),比如:set-Cookie:xxxx,元组中的第一个元素即为冒号前面的内容(键),第二个元素即为冒号后面的内容(值)
代码示例如下:
服务器:
import socket, re, multiprocessing, time
import mini_frame_02
class WSGIServer:
def __init__(self):
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.tcp_server_socket.bind(("", 8888))
self.tcp_server_socket.listen(128)
def set_response_header(self,status, headers):
# 当调用application时,由于在application中会调用此函数,所以就能够获得header的值
self.status = status
self.headers = headers
def service_client(self, new_socket):
request = new_socket.recv(1024).decode('utf-8')
request_lines = request.splitlines()
print()
print(">>" * 20)
print(request_lines)
file_name = ""
ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
print(ret)
print(ret.group())
if ret:
file_name = ret.group(1)
print(file_name)
if file_name == "/":
file_name = "index.html"
if not file_name.endswith(".py"):
# 如果请求的资源不是以.py结尾,就认为是静态资源
try:
f = open(file_name, "rb")
except:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "-----file not found-----"
new_socket.send(response.encode("utf-8"))
else:
html_content = f.read()
f.close()
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
new_socket.send(response.encode('utf-8'))
new_socket.send(html_content)
else:
# 如果是以.py结尾,认为是动态资源请求
# 但是此种写法不太好,通常,我们是把动态和静态请求的处理分开,比如,把动态请求交给另外一个py模块
env = dict()
body = mini_frame_02.application(env, self.set_response_header)
# 通过response的返回值,我们又能够获得body,将body的在application里面调用我们传过去的函数时获得的header走了拼接即可
header = "HTTP/1.1 %s\r\n"%(self.status)
for tmp in self.header:
header += "{}:{}\r\n".format(tmp[0],tmp[1])
header += "\r\n"
response = header + body
# 发送response相应给浏览器
new_socket.send(response.encode("utf-8"))
new_socket.close()
def run_forever(self):
while True:
new_socket, client_addr = self.tcp_server_socket.accept()
print(new_socket)
p = multiprocessing.Process(target=self.service_client, args=(new_socket,))
p.start()
new_socket.close()
self.tcp_server_socket.close()
def main():
wsgi_server = WSGIServer()
wsgi_server.run_forever()
if __name__ == "__main__":
main()
框架:mini_frame_02.py
def application(envron, start_response):
start_response("200 OK",[('Content_Type', 'text/html')])
return "hell world"
利用网络调试助手运行结果如下:
在这里插入图片描述
以上代码示例实现了让我们自己写的web服务器实现了WSGI协议的支持
ps:凡是在头部(header)中涉及到服务器的具体信息的,就需要自己在服务器端定义,比如,如果要在头响应的头部中加上服务器的版本信息,就自己在set_response_header方法中写上self.headers = [("server","mini_web v8.8")],然后self.headers += headers
如果要向框架中传传值,就利用字典,比如,浏览器请求的是index.html就在application执行相关语句使其返回index.html相关语句,如果浏览器请求的是login.html,就在application中执行相关语句,使服务器能够向浏览器返回login.html
在字典中,至少应该表明浏览器要请求的是什么内容
PS
:如果在要执行的python程序中导入了其他模块,而在其他模块中又导入或者打开了某文件,那么此时在其他模块中导入模块或打开文件的路径都是按照所执行的python程序去计算的,而不是该模块所在的路径。eg:执行a程序时,发现a中导入了b模块,而b中又用到了c,那么此时b中c的路径不应当是相对于b的,而应当是相对于a的
运行web服务器时指定端口以及框架
给程序传参
指定端口:采用在运行程序的时候给程序传参的方式
制定框架:仍然采用给程序传参的形式,eg:python3 web_server.py 端口名 框架名:application。运用此种方法,就不必在程序运行前就导入固定模块,而是在程序运行时,再import参数中的模块。ps
:但是通常模块名和其中的函数名都是存储在变量中,所以在import的时候,应该用__import__(存储模块名的变量)
,但是应当注意要sys.path.append(模块所在路径)
注意
:__import__()
的形式导入模块时,有一个返回值,是一个对象,这个对象标记着导入的模块。如何从这个对象中找到application函数?
getattr(模块对象,存储application函数名的变量)
让web服务器支持配置文件
通常,我们将静态文件的路径和框架加载的路径写在配置文件中
eg:配置文件web_server.conf中可以这么写
{
"static_path":"./static", #静态文件的路径
"dynamic_path":"./dynamic" #动态加载的文件(框架)的路径
}
然后在服务器程序中:
with open("./web_server.conf") as f:
conf_info = eval(f.read()) #把配置文件中的内容直接当作字典,然后就能够以字典的形式直接拿出文件的路径,从而和传递的参数结合起来以加载模块
用装饰器完成路由功能
注意:其中的异常处理部分省略
import re
URL_FUNC_DICT = dict()
def route(url):
def set_func(func):
URL_FUNC_DICT[url]=func #不同的请求链接,对应不同的函数(而不同的函数是返回不同的内容,所以就能够实现动态的请求)
def call_func(*args, **kwargs):
return func(*args, **kwargs)
return call_func
return set_func
# 利用装饰器,无论有多少个不同的动态请求,都能够快速的找到对应的内容并返回
@route("/index.html")
def index():
with open("./templates/index.html") as f:
content = f.read()
my_stock_info = "hahahahaha~"
content = re.sub(r"\{%content%\}", my_stock_info, content)
return content
def application(envron, start_response):
start_response("200 OK",[('Content_Type', 'text/html')])
file_name = env['PATH_INFO']
func = URL_FUNC_DICT[file_name]
# 获取不同请求对应的处理函数的引用
return func() # 将该函数返回的内容返回
# 利用此种方法,接口的application方法不用变
路由的功能
根据请求的不一样,最终调用的地方不一样,比如如上的func = URL_FUNC_DICT[file_name]
就是实现了这个功能
实现路由的核心原因:用到了映射
伪静态、静态和动态的区别
目前开发的网站其实真正意义上都是动态网站,只是URL上有些区别,一般URL分为静态URL、动态URL、伪静态URL
静态URL
静态URL类似域名/news/2013-4-34/110.html
,我们一般称为 真静态URL,每个网页有真实的物理路径,也就是真实存在服务器里的
静态URL对SEO(搜索引擎的优化)有加分的影响,因为打开速度快
动态URL
动态URL类似域名/newsMore.asp?id=5
或者/DaiKuan.php?id=14
,带有?的URL(?前面的是固定的,后面是动态的),我们一般称为动态网址,没一个URL只是一个逻辑地址,并不真实物理存在服务器硬盘里
目前搜索引擎已经能够很好的理解URL,所以对SEO没有什么减分的影响(特别复杂的URL结构除外)
伪静态URL
伪静态URL类似域名/course/43.html
,这个URL和真静态URL类似,他是通过伪静态规则把动态URL伪装成静态网址。也是逻辑地址,不存在物理地址
对SEO也没有什么减分影响
让web服务器支持伪静态
只需要将:浏览器 <--> 服务器 <--> 框架 中的服务器部分处理请求的部分进行修改即可,比如,原来是如果请求的为xxx.py则发送给框架某信息,现在则是如果请求的是xxx.html让框架处理该信息
具体代码略
在mini_frame框架中添加MySQL功能
from pymysql import connect
def application(envron, start_response):
start_response("200 OK",[('Content_Type', 'text/html')])
conn = connect(host='localhost', port = 3306, user = 'root', password = '', database = 'jing_dong', charset = 'utf8')
cr = conn.cursor()
cr.execute("select * from goods;")
content = cr.fetchall()
conn.close()
cr.close()
return str(content)
通常,在实际开发中,在框架中取出来的数据,是按照一定的模板(html模板,eg:<h1>xxxx</h1>
,在框架中只需要将xxx替换为从数据库中拿出来的数据即可,而这个模板常由前端人员提供)处理后,再返回。这就实现了 浏览器 <--> 服务器 <--> 框架(数据库)
之间的整个流程
ps:url编解码
对于特殊的字符(eg:空格)在浏览器(链接)中提交的时候,会自动进行编码,当服务器拿到数据,交给框架处理时,就应该解码
import urllib.parse
print(urllib.pase.quote(" ")) #输出%20 quote:编码
print(urllib.parse.unquote("%20")) #输出空格 unquote:解码