Python 18 MiniWEB项目
MiniWEB项目、程序解耦和耦合关系、区分动态数据和静态数据、WSGI、WSGI接口中的接口函数中参数的函数回调
3.1 MiniWEB项目
学习目标
1. 能够说出WEB服务器在访问时的执行过程
2. 能够说出实现框架的意义
3. 能够说出为什么要进行程序的解耦
总结:
1. 代码在开发过程中,应该遵循高内聚低耦合的思想
2. 静态数据是指在访问时不会发生变化的数据
3. 动态数据是指在访问时会服务的状态,条件等发生不同的变化,得到的数据不同
4. 通过WSGI接口,实现了服务器和框架的功能分离
5. 服务器和框架应用的功能分离,使服务器的迁移,维护更加简单
--------------------------------------------------------------------------------
3.1.1 HTTP 服务器运行原理
之前在实现的程序中,主要代码都实现在上图的左半部分。服务器的运行和 WEB 应用的处理,都是在一个文件中实现的。
这几天的工作,就是把程序解耦,将功能分离,服务器只用来提供WEB服务,WEB应用用来实现数据处理。
大家可以了解一下开发中比较常用的WEB框架,比如 Apache ,Nigix,Tomcat等。
没有一个服务器框架安装完成后,就完成了WEB应用的开发的。
因为服务器根本不知道你要完成的功能是什么,所以只提供给你服务,而应用的功能按照服务的接口来完成。然后让服务器响应处理。
3.1.2 原始服务器回顾分析
在前面的课程中,我们实现过一个 HTTP 服务器,我们就在这个服务器的基础上,来实现这阶段的 MiniWEB 框架。
首先,先来回顾一下这个HTTP服务器的代码
注意:将代码复制到工程文件中之后,还需要将资源文件复制到工程目录中
原始服务器 WebServer.py
# 代码实现:
import socket
import re
import multiprocessing
def service_client(new_socket):
"""为客户端返回数据"""
# 1. 接收浏览器发送过来的请求 ,即http请求相关信息
# GET / HTTP/1.1
# .....
request = new_socket.recv(1024).decode("utf-8")
#将请求头信息进行按行分解存到列表中
request_lines = request.splitlines()
# GET /index.html HTTP/1.1
file_name = ""
#正则: [^/]+ 不以/开头的至少一个字符 匹配到/之前
# (/[^ ]*) 以分组来匹配第一个字符是/,然后不以空格开始的0到多个字符,也就是空格之前
# 最后通过匹配可以拿到 请求的路径名 比如:index.html
ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
#如果匹配结果 不为none,说明请求地址正确
if ret:
#利用分组得到请求地址的文件名,正则的分组从索引1开始
file_name = ret.group(1)
print('FileName: ' + file_name)
#如果请求地址为 / 将文件名设置为index.html,也就是默认访问首页
if file_name == "/":
file_name = "/index.html"
# 2. 返回http格式的数据,给浏览器
try:
#拼接路径,在当前的html目录下找访问的路径对应的文件进行读取
f = open("./html" + 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()
# 2.1 准备发送给浏览器的数据---header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
#如果想在响应体中直接发送文件内的信息,那么在上面读取文件时就不能用rb模式,只能使用r模式,所以下面将响应头和响应体分开发送
#response += html_content
# 2.2 准备发送给浏览器的数据
# 将response header发送给浏览器
new_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器
new_socket.send(html_content)
# 关闭套接
new_socket.close()
def main():
"""用来完成整体的控制"""
# 1. 创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#用来重新启用占用的端口
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定IP和端口号
tcp_server_socket.bind(("", 7890))
# 3. 设置套接字监听连接数(最大连接数)
tcp_server_socket.listen(128)
while True:
# 4. 等待新客户端的链接
new_socket, client_addr = tcp_server_socket.accept()
# 5. 为连接上来的客户端去创建一个新的进程去运行
p = multiprocessing.Process(target=service_client, args=(new_socket,))
p.start()
#因为新进程在创建过程中会完全复制父进程的运行环境,所以父线程中关闭的只是自己环境中的套接字对象
#而新进程中因为被复制的环境中是独立存在的,所以不会受到影响
new_socket.close()
# 关闭监听套接字
tcp_server_socket.close()
if __name__ == "__main__":
main()
3.1.3 程序解耦
<1>概念理解 什么是耦合关系?
耦合关系是指某两个事物之间如果存在一种相互作用、相互影响的关系,那么这种关系就称"耦合关系"。
在软件工程中的耦合就是代码之间的依赖性。
代码之间的耦合度越高,维护成本越高。
<2>代码开发原则之一:高内聚,低耦合。
这句话的意思就是程序的每一个功能都要单独内聚在一个函数中,让代码之间的耦合度达到最小。也就是相互之间的依赖性达到最小。
实现面向对象的思想的代码重构
以面向对象的思想来完成服务器的代码实现 实现过程:
■ 1.封装类
■ 2.初始化方法中创建socket对象
■ 3.启动服务器的方法中进行服务监听
■ 4.实现数据处理的方法
■ 5.对象属性的相应修改
■ 6.重新实现main方法,创建WEBServer类对象并启动服务
WebServer.py
# 面向对象修改数据
import socket
import re
import multiprocessing
class WEBServer(object):
#在初始化方法中完成服务器Socket对象的创建
def __init__(self):
"""用来完成整体的控制"""
# 1. 创建套接字
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 用来重新启用占用的端口
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定IP和端口号
self.tcp_server_socket.bind(("", 7890))
# 3. 设置套接字监听连接数(最大连接数)
self.tcp_server_socket.listen(128)
def service_client(self,new_socket):
"""为这个客户端返回数据"""
# 1. 接收浏览器发送过来的请求 ,即http请求相关信息
# GET / HTTP/1.1
# .....
request = new_socket.recv(1024).decode("utf-8")
#将请求头信息进行按行分解存到列表中
request_lines = request.splitlines()
# GET /index.html HTTP/1.1
# get post put del
file_name = ""
#正则: [^/]+ 不以/开头的至少一个字符 匹配到/之前
# (/[^ ]*) 以分组来匹配第一个字符是/,然后不以空格开始的0到多个字符,也就是空格之前
# 最后通过匹配可以拿到 请求的路径名 比如:index.html
ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
#如果匹配结果 不为none,说明请求地址正确
if ret:
#利用分组得到请求地址的文件名,正则的分组从索引1开始
file_name = ret.group(1)
print('FileName: ' + file_name)
#如果请求地址为 / 将文件名设置为index.html,也就是默认访问首页
if file_name == "/":
file_name = "/index.html"
# 2. 返回http格式的数据,给浏览器
try:
#拼接路径,在当前的html目录下找访问的路径对应的文件进行读取
f = open("./html" + 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()
# 2.1 准备发送给浏览器的数据---header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
#如果想在响应体中直接发送文件内的信息,那么在上面读取文件时就不能用rb模式,只能使用r模式,所以下面将响应头和响应体分开发送
#response += html_content
# 2.2 准备发送给浏览器的数据
# 将response header发送给浏览器
new_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器
new_socket.send(html_content)
# 关闭套接
new_socket.close()
def run(self):
while True:
# 4. 等待新客户端的链接
new_socket, client_addr = self.tcp_server_socket.accept()
# 5. 为这个客户端服务
p = multiprocessing.Process(target=self.service_client, args=(new_socket,))
p.start()
#因为新线程在创建过程中会完全复制父线程的运行环境,所以父线程中关闭的只是自己环境中的套接字对象
#而新线程中因为被复制的环境中是独立存在的,所以不会受到影响
new_socket.close()
# 关闭监听套接字
self.tcp_server_socket.close()
def main():
webServer = WEBServer()
webServer.run()
if __name__ == "__main__":
main()
通过使用面向对象的思想,将代码重构后,耦合性降低,但还没有完全实现功能的分离。 目前还是在一个文件中实现所有的程序功能,也就是说,目前只是完成了在原理图中,左半侧的功能。后面会继续改进。
3.1.4 区分动态数据和静态数据
静态数据:是指在页面进行访问时,无论何时访问,得到的内容都是同样的,不会发生任意变化
(比如我们现在实现的API网页的访问效果,这些API文件都是保存在本地(或服务器上)的一些固定的文档说明,无论在何时何地访问这些数据,都是相同的,不会发生变化)
动态数据:是指在页面进行访问时,得到的数据是经过服务器进行计算,加工,处理过后的数据,称为动态数据,哪怕只是加了一个空格
比如:实时新闻,股票信息,购物网站显示的商品信息等等都动态数据
在这部分代码实现中,先来实现不同形式的页面访问,服务器返回不同的数据(数据暂时还是静态的,假的数据,真正的动态数据会在完成框架后,在数据库中读取返回)
这里设定: xxx.html 访问时,返回的是静态数据 API 文档中的内容, xxx.py 访问时,返回的是动态数据(数据先以静态数据代替)
实现过程:
1.先根据访问页面地址判断访问数据的类型,是py的动态还是html的静态
2.根据动态请求的路径名的不同来返回不同的数据,不在使用html获取数据,而使用py来获取
WebServer.py
# ...
# 前面的代码不需要修改
if ret:
#利用分组得到请求地址的文件名,正则的分组从索引1开始
file_name = ret.group(1)
print('FileName: ' + file_name)
#如果请求地址为 / 将文件名设置为index.html,也就是默认访问首页
if file_name == "/":
file_name = "/index.html"
# ------------- 这里开始修改代码------------
#判断访问路径的类型
if file_name.endswith('.py'):
#根据不同的文件名来确定返回的响应信息
if file_name == '/index.py': #首页
header = "HTTP/1.1 200 OK\r\n" #响应头
body = 'Index Page ...' #响应体
data = header + '\r\n' + body #拼接响应信息
new_socket.send(data.encode('utf-8')) #返回响应信息
elif file_name == '/center.py': #个人中心页面
header = "HTTP/1.1 200 OK\r\n"
body = 'Center Page ...'
data = header + '\r\n' + body
new_socket.send(data.encode('utf-8'))
else: #其它页面
header = "HTTP/1.1 200 OK\r\n"
body = 'Other Page ...'
data = header + '\r\n' + body
new_socket.send(data.encode('utf-8'))
else:
# 2. 返回http格式的数据,给浏览器
try:
#拼接路径,在当前的html目录下找访问的路径对应的文件进行读取
f = open("./html" + 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()
# 2.1 准备发送给浏览器的数据---header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
#如果想在响应体中直接发送文件内的信息,那么在上面读取文件时就不能用rb模式,只能使用r模式,所以下面将响应头和响应体分开发送
#response += html_content
# 2.2 准备发送给浏览器的数据
# 将response header发送给浏览器
new_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器
new_socket.send(html_content)
3.1.5 实现动态数据的响应优化
虽然前面的代码实现了设计需求,但是实现过程太过冗余,不符合代码开发原则。 一个服务器中提供可以访问的页面肯定不止这么几个,如果每一个都实现一次响应信息的编写,那冗余代码就太多了,不符合代码的开发规范 通过分析我们可以看出,代码中大部分内容都是相同的,只有在响应信息的响应体部分不同,那么就可以将代码优化一下。
实现过程: 因为所有页面的响应信息都是相同的,所以让这些页面共用一块代码
1. 将响应头和空行代码放到判断页面之前
2. 将发拼接和发送代码放到判断之后
3. 页面判断中,只根据不同的页面设计不同的响应体信息
实现代码: WebServer.py
# ...
# 前面的代码不需要修改
#判断访问路径的类型
# ------------- 这里开始修改代码------------
if file_name.endswith('.py'):
header = "HTTP/1.1 200 OK\r\n" # 响应头
#根本不同的文件名来确定返回的响应信息
if file_name == '/index.py':
body = 'Index Page ...' #响应体
elif file_name == '/center.py':
body = 'Center Page ...'
else:
body = 'Other Page ...'
data = header + '\r\n' + body # 拼接响应信息
new_socket.send(data.encode('utf-8')) # 返回响应信息
# ------------- 这里开始修改代码结束------------
else:
# 后面的代码不需要修改
3.1.6 实现功能的分离
代码被进一步优化,但是还是存在问题。网络请求和数据处理还是没有分开,还是在同一个文件中实现的。
实际开发中WEB服务器有很多种,比如Apache,Nigix等等。
如果在开发过程中,需要对 WEB 服务器进行更换。那么我们现在的做法就要花费很大的精力,因为 WEB 服务和数据处理都在一起。
如果能将程序的功能进行进行分离,提供 WEB 请求响应的服务器只管请求的响应,而响应返回的数据由另外的程序来进行处理。
这样的话,WEB 服务和数据处理之间的耦合性就降低了,这样更便于功能的扩展和维护
■ 比如:
■ 一台电脑,如果要是所有的更件都是集成在主板上的,那么只要有一个地方坏了。那整个主板都要换掉。成本很高
■ 如果所有的硬件都是以卡槽接口的形式插在主板上,那么如果哪一个硬件坏了或要进行升级扩展都会很方便,降低了成本。
在实际开发过程中,代码的模块化思想就是来源于生活,让每个功能各司其职。
实现思想: 将原来的服务器文件拆分成两个文件,一个负责请求响应,一个负责数据处理。 那么这里出现一个新的问题,两个文件中如何进行通信呢?负责数据处理的文件怎么知道客户端要请求什么数据呢?
想一下主板和内存之间是如何连接的?
实现过程:
1.WebServer 文件只用来提供请求的接收和响应
2.WebFrame 文件只用来提供请求数据的处理和返回
3.文件之间利用一个函数来传递请求数据和返回的信息
实现代码 WebServer.py
# ------------- 这里需要修改代码------------
# 因为在这里需要使用框架文件来处理数据,所以需要进行模块导入
import WebFrame
#...
# 前面的代码不需要修改
# ------------- 这里开始修改代码------------
#判断访问路径的类型
if file_name.endswith('.py'):
header = "HTTP/1.1 200 OK\r\n" # 响应头
# 根本不同的访问路径名来向框架文件获取对应的数据
# 通过框架文件中定义的函数将访问路径传递给框架文件
body = WebFrame.application(file_name)
#将返回的数据进行拼接
data = header + '\r\n' + body # 拼接响应信息
new_socket.send(data.encode('utf-8')) # 返回响应信息
# ------------- 这里开始修改代码结束------------
else:
# 后面的代码不需要修改
# ...
WebFrame.py
# 在框架文件中,实现一个函数,做为 Web 服务器和框架文件之间的通信接口
# 在这个接口函数中,根据 Web 服务器传递过来的访问路径,判断返回的数据
def application(url_path):
if url_path == '/index.py':
body = 'Index Page ...' #响应体
elif url_path == '/center.py':
body = 'Center Page ...'
else:
body = 'Other Page ...'
return body
代码实现到这里,基本将功能进行了分离,初步完成了前面原理图中的功能分离。
但是还没有真正的完成框架,到这里只是完成了框架中的一小步。
3.1.7 WSGI
<1>WSGI是什么?
WSGI,全称 Web Server Gateway Interface,
是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。
是用来描述web server如何与web application通信的规范。
<2>WSGI协议中,定义的接口函数就是 application ,定义如下:
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/html')])
return [b'<h1>Hello, web!</h1>']
这个函数有两个参数:
参数一: web服务器向数据处理文件中传递请求相关的信息,一般为请求地址,请求方式等,传入类型约定使用字典
参数二: 传入一个函数,使用函数回调的形式,将数据处理的状态结果返回给服务器
■ 服务器的函数一般用来存储返回的信息,用来组合响应头信息,这里只是在框架中调用这个函数的时候传入定义时的参数(此处参数包括2个,第一个是响应状态和状态描述,第二个是响应头信息),其中描述响应头信息的参数是以列表装元组的形式返回,列表中的每一个元素都是以元组形式存放的一条响应头的信息,元组中有两个数据,分别对应着响应头信息中:前后的部分,所以要得到里面的数据应该先遍历列表,得到的是列表里的数据元组,'%s:%s\r\n' %t是对元组的拆包然后拼接响应头信息
返回值: 用来返回具体的响应体数据。
服务器和框架应用程序在共同遵守了这个协议后,就可以通过 application 函数进行通信。完成请求的转发和响应数据的处理返回。
实现过程:
1.在服务器中调用application函数
2.定义用来储存返回的响应头信息的回调函数,函数有两个参数,一个是状态,一个是其它信息,以字典形式传入
3.以字典传入请求地址名,传入回调的函数名
4.当处理完数据后,调用传入的函数并返回数据 5
.服务器收到返回的信息后进行响应信息的拼接处理.
代码实现: WebServer.py
import WebFrame
#...
# 前面的代码不需要修改
# ------------- 这里开始修改代码------------
#判断访问路径的类型
if file_name.endswith('.py'):
#要先调用这个函数,如果不调用,那么回调函数不能执行,下面拼接数据就会出错
#根本不同的文件名来向数据处理文件获取对应的数据
#并将回调函数传入进去
env = {'PATH_INFO':file_name}
body = WEBFrame.application(env,self.start_response)
#拼接返回的状态信息
header = "HTTP/1.1 %s\r\n"%self.status # 响应头
#拼接返回的响应头信息
#因为是返回是以列表装元组的形式返回,所以遍历列表,得到的是列表里的数据元组,
#'%s:%s\r\n'%t是对元组的拆包,然后拼接元组里的信息
for t in self.params:
header += '%s:%s\r\n'%t
data = header + '\r\n' + body # 拼接响应信息
new_socket.send(data.encode('utf-8')) # 返回响应信息
# ------------- 这里开始修改代码结束------------
else:
# 后面的代码不需要修改
# ...
# ------------- 这里需要修改代码------------
#定义一个成员函数 ,用来回调保存数据使用
def start_response(self,status,params):
#保存返回回来的响应状态和其它响应信息
self.status = status
self.params = params
WebFrame.py
# 实现 WSGI 协议中的 application 接口方法
def application(environ, start_response):
# 从服务器传过来的字典中将访问路径取出来
url_path = environ['PATH_INFO']
# 判断访问路径,确定响应数据内容,保存到body中
if url_path == '/index.py':
body = 'Index Page ...' #响应体
elif url_path == '/center.py':
body = 'Center Page ...'
else:
body = 'Other Page ...'
# 回调 start_response 函数,将响应状态信息回传给服务器
start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8')])
# 返回响应数据内容
return body
通过代码的优化,到这里,基本已经将服务器和框架应用的功能分离。
3.1.8 总结:
1. 代码在开发过程中,应该遵循高内聚低耦合的思想
2. 静态数据是指在访问时不会发生变化的数据
3. 动态数据是指在访问时会服务的状态,条件等发生不同的变化,得到的数据不同
4. 通过WSGI接口,实现了服务器和框架的功能分离
5. 服务器和框架应用的功能分离,使服务器的迁移,维护更加简单