一 WSGI
今天要实现的是一个简易的web框架,写博客的目的也是为了让初学者少走弯路,所以这里会循序渐进的讲解。首先我们要明白,web框架,服务器,前端究竟有什么样的关系。难道不能在服务器之中把功能全部实现吗?当然可以,但是这违反了软件开发的原则,叫做单一职责原则。设计模式有23种,一定要有了解。也就是说,所有的设计模式最终的目的就是为了让工程可扩展,可重用。如果把一切东西都放在服务器里,首先损耗性能不说,也显得不易维护。为了解决这种问题,python里面提供了一种协议,叫做WSGI,首先明白一点,他既不是包,也不是模块,只是一种协议。用这种协议来进行开发,首先来说就是科学,再者易于维护。
WSGI协议其实就是定义了一个接口,def application(env, start_response):。也就是说,WSGI协议最终的体现是什么呢,就是让服务器来处理高并发,连接等一系列的通信问题,至于数据处理,以及业务逻辑,全放到了application这个方法里面。只要我们的web框架实现application方法来实现业务逻辑,那么服务器也就只剩下转发的作用了。这里面再缕一缕思路。首先,对于一个网站而言,我们最直观的就是在搜索引擎里写入URL来访问页面。此时浏览器把请求给了服务器。浏览器的职责就是与用户进行信息交互,并显示信息。而服务器接收到了这个请求之后,通过WSGI协议把请求进行转发给web框架。这时的web框架就根据不同的请求,来实现不同的业务逻辑,得到的数据通过服务器转发给前端显示。这就是整个流程。大家想想,这样不就解决了程序耦合的问题了吗,面向对象的思想就是在不断地解耦合。每一个类尽量职责单一,每一个模块职责也要单一,要做到低耦合,高聚合。
二 代码第一部分 通信
import socket
import re
import multiprocessing
import time
import socketserver
import sys
import re
class WSGIServer(socketserver.BaseRequestHandler):
def handle(self):
self.data = self.request.recv(1024).decode('utf-8')
data = self.data.splitlines()
resource = re.findall(".*?(/.*?) HTTP/.*?",data[0],re.S)[0]
self.getConfinfo()
if not resource.endswith(".py"):
try:
f = open("{0}{1}".format(self.conf_info["static_path"], resource), "rb")
except:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "------file not found-----"
self.request.send(response.encode("utf-8"))
else:
html_content = f.read()
f.close()
# 2.1 准备发送给浏览器的数据---header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
self.request.send(response.encode("utf-8"))
self.request.send(html_content)
else:
env = dict()
env['PATH_INFO'] = resource
body = self.application(env, self.set_response_header)
header = "HTTP/1.1 %s\r\n" % self.status
for temp in self.headers:
header += "%s:%s\r\n" % (temp[0], temp[1])
header += "\r\n"
response = header + body
self.request.send(response.encode('utf-8'))
def getConfinfo(self):
with open('web_server.conf','r') as f:
self.conf_info = eval(f.read())
sys.path.append(self.conf_info['dynamic_path'])
self.Frame_name= __import__(self.conf_info["frame_name"])
self.application = getattr(self.Frame_name,self.conf_info["FunctionName"])
def set_response_header(self, status, headers):
self.status = status
self.headers = [("server", "mini_web v8.8")]
self.headers += headers
def main():
Soc = socketserver.TCPServer(('localhost', 7840), WSGIServer)
Soc.serve_forever()
if __name__ == "__main__":
main()
先不看代码,先从思路上来分析。浏览器与服务器之间的通信是基于HTTP协议,而HTTP协议是架设在TCP/IP协议之上。所以想要实现HTTP协议,要先进行socket,那么在这里博主用的是python里面本身自带的模块socketsever模块,这个模块是对socket,select,epoll模块的封装。先看main()函数,这个函数就是socketsever的用法体现。socketsever类里面有一个TCPServer类,这个类里面封装了TCP服务器的所有方法,它的初始化首先要传入一个元组,这个元组里面分别是IP地址,端口号,接下来的参数,就是回调函数。在python里面一切都是对象,那么函数名就是此函数的引用,也就是C语言的函数指针,所有在python实现回调不难理解,直接当做参数传递。只不过这里的参数是类,而不是函数。这个类必须继承于socketserver.BaseRequestHandler这个类,并重写里面的handle方法,就可以实现多线程。至于怎么实现,感兴趣的可以去看源码,博主也稍微看了一下。通过第一步之后,调用Soc.server_forever就可以实现开启服务器了。
紧接着,HTTP协议有基础的都该知道最基本的就是一个请求,一个响应。那么在我们输入URL的时候,如果是静态的URL,可以看到有一些是带有.html后缀的。那么也就是说,我们的这个请求发送之后,浏览器将这个请求发给了服务器,服务器进行转发,转发给web框架,web框架根据url指定的地址来找html文件,此时根据不同的业务逻辑进行不同的处理,最终再把这个html返回给浏览器,浏览器进行渲染。这就是一个最简单的过程,我们不考虑服务器的框架也不考虑ajax,web最简单最直接的原理,就是浏览器给了http请求的url,根据不同的情况,返回不同的头,不同的报文体。
代码的思路也是这样。
1 首先我们接收完数据之后,进行了正则表达式的解析。此步解析是为了提取url里面的文件名,此时我们先假定好,该html就存放在了固定目录里。
2 解析完文件名之后,进行了判断,就是当文件名不是以.py结尾的时候,我们认为这是一个静态文件,所以直接从配置文件中读出路径,去路径中去寻找,如果找到了,返回html的内容以及响应头,注意,响应头与报文体有一个空行。如果没找到,则返回前端错误信息。
3 那么当文件是以.py结尾的时候,我们调用了WSGI协议里的application函数,这里讲解一下参数,此函数第一个参数为字典,在这里这个字典要向web框架传递的是文件名。第二个参数,是一个回调函数,此函数的意义在于,接收从框架返回的响应头,为什么?因为这部分的逻辑处理交给了框架,服务器并不知道这个页面是否存在以及其它的异常,所以,通过这个回调函数,把框架处理完的头接收过来。
代码的通信部分大体框架就是这样。
三 框架部分
import re
import pymysql
"""
URL_FUNC_DICT = {
"/index.py": index,
"/center.py": center
}
"""
URL_FUNC_DICT = dict()
def route(url):
def set_func(func):
# URL_FUNC_DICT["/index.py"] = index
URL_FUNC_DICT[url] = func
def call_func(*args, **kwargs):
return func(*args, **kwargs)
return call_func
return set_func
@route("/index.py") # 相当于 @set_func # index = set_func(index)
def index():
with open("./templates/index.html",encoding="utf-8") as f:
content = f.read()
my_stock_info = "哈哈哈哈 这是你的本月名称....."
content = re.sub(r"\{%content%\}", my_stock_info, content)
return content
@route("/center.py")
def center():
with open("./templates/center.html",encoding="utf-8") as f:
content = f.read()
db = pymysql.connect(host = 'localhost',port = 3306,user='root',password = 'mysql',database='stock_db',charset='utf8')
cursor = db.cursor()
sql = """select * from info;"""
cursor.execute(sql)
data_from_mysql = cursor.fetchall()
cursor.close()
db.close()
html_template = """
<tr>
<td>%d</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td>
<input type="button" value="添加" id="toAdd" name="toAdd" systemidvaule="%s">
</td>
</tr>"""
html = ""
for info in data_from_mysql:
html += html_template % (info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7], info[1])
content = re.sub(r"\{%content%\}", html, content)
return content
@route('/time.py')
def time():
with open("./templates/time.html",encoding="utf-8") as f:
content = f.read()
return content
def application(env, start_response):
start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
file_name = env['PATH_INFO']
try:
return URL_FUNC_DICT[file_name]()
except Exception as ret:
return "产生了异常:%s" % str(ret)
框架部分代码还是有点含金量的,首先,服务器给我们的字典里包含的是一个文件名,这个文件名就是我们要处理逻辑业务的开始,我们根据不同的文件名,来调用不同的处理逻辑。这里划上重点!在C语言的项目里,我们怎么样进行函数与字符串的对应呢?也就是说我们怎么实现接收过来特定的字符串,来调用特定的函数呢。答案是通过轮询结构体数组,当找到与接收过来的字符串相同的时候,就调用对应的函数指针。那么在python里我们可以通过字典键值对的方式来实现,但是有一点要注意,对于一个web框架而言,逻辑部分是相当多的,如果提前写好,那是不可能的。所以我们用了带参数的装饰器。
带参数的装饰器里面有三层,因为python的执行是从上到下,当我们用装饰器修饰函数时自动的把函数名和装饰器的参数写到了字典里面。在application里 我们只需要调用字典里的函数就OK了
四 总结
代码里对于配置文件的读写,这里就先不表示 ,自己看。但是总体框架就是这样。一定要掌握装饰器,框架最精髓的地方就是在于对于多个请求我们怎么用最优雅的方式来解决冗余代码的问题。带参数的装饰器,要重点理解。