在使用sys模块的argv属性实现用python3命令运行.py脚本的同时向脚本中传递参数中,实现了web服务器和web框架之间一定程度上的解耦,但还不够彻底,原因如下:
1. 解耦需求
通常在实际开发中,因为web后端需要同时支持浏览器发来的静态资源和动态资源请求,而且由于web服务器、静态资源、web框架(用以提供动态资源)一般不是同一批人开发。
所以,在实际部署这三者的时候,如何确保能够不修改任何一方的源码就可以实现三者之间协同为浏览器提供服务就至关重要。
以使用sys模块的argv属性实现用python3命令运行.py脚本的同时向脚本中传递参数中的web服务器程序为例:
首先,在项目部署时,就会希望按照下图所示方式部署web服务器、web框架以及静态资源以实现三者逻辑存储的分离,即:
- 所有的静态资源放在文件夹static下;
- 处理动态资源请求的web框架程序放在文件夹dynamic下;
- web服务器程序与上述两个文件夹处于同一级目录。
为实现程序可以根据依赖关系正常运行,对应的web服务器程序需首先修改后如下:
import socket
import re
import multiprocessing
import sys
class WSGIServer(object):
def __init__(self, port, app):
# 1.创建套接字
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 通过设定套接字选项解决[Errno 98]错误
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2.绑定端口
self.tcp_server_socket.bind(("", port))
# 3.变为监听套接字
self.tcp_server_socket.listen(128)
self.app = app
# 定义两个实例属性,用于存储response_header信息
self.status = None
self.headers = None
def set_response_header(self, status, headers):
self.status = status
# 在服务器程序中指定web服务器的信息
self.headers = [("Server", "user-defined web server v1.0")]
self.headers += headers
def serve_client(self, new_client_socket):
"""为这个客户端返回数据"""
# 6.接收浏览器发送过来的http请求
request_data = new_client_socket.recv(1024)
# 判断客户端是否已经断开连接,
# 客户端断开连接的表现是:
# recv()方法解除阻塞,但是返回值为空
if not request_data:
print("客户端已经断开连接")
new_client_socket.close()
return
request_str = request_data.decode("utf-8")
# 7.将请求报文分割成字符串列表
request_lines = request_str.splitlines()
print(request_lines)
# 8.通过正则表达式提取浏览器请求的文件名
file_name = None
ret = re.match(r"^[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
print("file_name:", file_name)
if file_name == "/":
file_name = "/index.html"
# 9.返回http格式的应答数据给浏览器
# 9.1 如果请求的资源不是以.py结尾,那么就认为浏览器请求的是静态资源(HTML、CSS、JPG、PNG等)
if not file_name.endswith(".py"):
# 静态资源处理逻辑
# 9.返回http格式的应答数据给浏览器
try:
f = open("./static" + file_name, "rb")
except Exception:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "-----file not found-----"
new_client_socket.send(response.encode("utf-8"))
else:
# 9.1 读取发送给浏览器的数据-->body
html_content = f.read()
f.close()
# 9.2 准备发送给浏览器的数据-->header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 将response header发送给浏览器--先以utf-8格式编码
new_client_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器--直接是以字节形式发送
new_client_socket.send(html_content)
else:
# 9.2 如果浏览器请求的资源是以.py结尾,则表示浏览器请求的是动态资源
env = dict()
env["PATH_INFO"] = file_name
body = self.app(env, self.set_response_header)
header = "HTTP/1.1 %s\r\n" % self.status
for each in self.headers:
header += "%s:%s\r\n" % (each[0], each[1])
header += "\r\n"
response = header + body
new_client_socket.send(response.encode("utf-8"))
# 10. 关闭此次服务的套接字
new_client_socket.close()
def run_forever(self):
"""用来完成程序整体控制"""
while True:
# 4.等待新客户端连接
new_client_socket, client_addr = self.tcp_server_socket.accept()
# 5.为连接上的客户端服务
process = multiprocessing.Process(target=self.serve_client, args=(new_client_socket,))
process.start()
new_client_socket.close()
# 关闭监听套接字
self.tcp_server_socket.close()
def main():
"""
控制整体,创建一个web服务器对象,然后调用这个对象的run_forever()方法运行
:return:
"""
if len(sys.argv) == 3:
try:
port = int(sys.argv[1])
frame_application_str = sys.argv[2]
except Exception as ret:
print("请输入正确格式的端口...")
return
else:
print("请按照以下方式运行服务器程序:")
print("python3 web_server.py 8888 mini_web_framework:application")
return
# 使用正则表达式分别提取出web框架和符合WSGI规范的函数名称
match_obj = re.match(r"([^:]+):(.*)", frame_application_str)
if match_obj:
frame_name = match_obj.group(1)
application_name = match_obj.group(2)
else:
print("请按照以下方式运行服务器程序:")
print("python3 web_server.py 8888 mini_web_framework:application")
return
sys.path.append("./dynamic")
# 通过变量frame_name导入mini_web_framework模块
web_frame = __import__(frame_name)
print(web_frame)
# 使用getattr函数获取mini_web_framework模块中application函数引用
app = getattr(web_frame, application_name)
wsgi_server = WSGIServer(port, app)
wsgi_server.run_forever()
if __name__ == "__main__":
main()
2. 解耦分析
分析上述源码,web服务器程序有如下几处需要进一步实现和web框架以及静态资源之间的解耦:
- 第68行显式指定了静态资源的路径和名称;
- 第151行显式添加了web框架的路径名称:需要说明的是,由于此时web框架与服务器程序在同一级路径,如果不将web框架的路径添加至系统路径,则因为解释器导包时的向上查找机制,解释器将找不到web框架的模块。
3. 解耦实现
上述web服务器、web框架、静态资源之间已经实现了逻辑存储的解耦隔离,但是程序之间仍存在上述提及的两处耦合,进一步解耦则需要使用到配置文件。
具体地,在和web服务器、web框架、静态资源同一级路径下新建一个配置文件server_configuration.conf,内容为:
{
"static_path":"./static",
"dynamic_path":"./dynamic"
}
服务器程序将通过读取配置文件的方式来:
- 指定静态资源的路径和名称;
- 添加web框架所在路径。
具体代码实现如下:
import socket
import re
import multiprocessing
import sys
class WSGIServer(object):
def __init__(self, port, app, static_path):
# 1.创建套接字
self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 通过设定套接字选项解决[Errno 98]错误
self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2.绑定端口
self.tcp_server_socket.bind(("", port))
# 3.变为监听套接字
self.tcp_server_socket.listen(128)
self.app = app
self.static_path = static_path
# 定义两个实例属性,用于存储response_header信息
self.status = None
self.headers = None
def set_response_header(self, status, headers):
self.status = status
# 在服务器程序中指定web服务器的信息
self.headers = [("Server", "user-defined web server v1.0")]
self.headers += headers
def serve_client(self, new_client_socket):
"""为这个客户端返回数据"""
# 6.接收浏览器发送过来的http请求
request_data = new_client_socket.recv(1024)
# 判断客户端是否已经断开连接,
# 客户端断开连接的表现是:
# recv()方法解除阻塞,但是返回值为空
if not request_data:
print("客户端已经断开连接")
new_client_socket.close()
return
request_str = request_data.decode("utf-8")
# 7.将请求报文分割成字符串列表
request_lines = request_str.splitlines()
print(request_lines)
# 8.通过正则表达式提取浏览器请求的文件名
file_name = None
ret = re.match(r"^[^/]+(/[^ ]*)", request_lines[0])
if ret:
file_name = ret.group(1)
print("requested_file_name:", file_name)
if file_name == "/":
file_name = "/index.html"
# 9.返回http格式的应答数据给浏览器
# 9.1 如果请求的资源不是以.py结尾,那么就认为浏览器请求的是静态资源(HTML、CSS、JPG、PNG等)
if not file_name.endswith(".py"):
# 静态资源处理逻辑
# 9.返回http格式的应答数据给浏览器
try:
f = open(self.static_path + file_name, "rb")
except Exception:
response = "HTTP/1.1 404 NOT FOUND\r\n"
response += "\r\n"
response += "-----file not found-----"
new_client_socket.send(response.encode("utf-8"))
else:
# 9.1 读取发送给浏览器的数据-->body
html_content = f.read()
f.close()
# 9.2 准备发送给浏览器的数据-->header
response = "HTTP/1.1 200 OK\r\n"
response += "\r\n"
# 将response header发送给浏览器--先以utf-8格式编码
new_client_socket.send(response.encode("utf-8"))
# 将response body发送给浏览器--直接是以字节形式发送
new_client_socket.send(html_content)
else:
# 9.2 如果浏览器请求的资源是以.py结尾,则表示浏览器请求的是动态资源
env = dict()
env["PATH_INFO"] = file_name
body = self.app(env, self.set_response_header)
header = "HTTP/1.1 %s\r\n" % self.status
for each in self.headers:
header += "%s:%s\r\n" % (each[0], each[1])
header += "\r\n"
response = header + body
new_client_socket.send(response.encode("utf-8"))
# 10. 关闭此次服务的套接字
new_client_socket.close()
def run_forever(self):
"""用来完成程序整体控制"""
while True:
# 4.等待新客户端连接
new_client_socket, client_addr = self.tcp_server_socket.accept()
# 5.为连接上的客户端服务
process = multiprocessing.Process(target=self.serve_client, args=(new_client_socket,))
process.start()
new_client_socket.close()
# 关闭监听套接字
self.tcp_server_socket.close()
def main():
"""
控制整体,创建一个web服务器对象,然后调用这个对象的run_forever()方法运行
:return:
"""
if len(sys.argv) == 3:
try:
port = int(sys.argv[1])
frame_application_str = sys.argv[2]
except Exception as ret:
print("请输入正确格式的端口...")
return
else:
print("请按照以下方式运行服务器程序:")
print("python3 web_server.py 8888 mini_web_framework:application")
return
# 使用正则表达式分别提取出web框架和符合WSGI规范的函数名称
match_obj = re.match(r"([^:]+):(.*)", frame_application_str)
if match_obj:
frame_name = match_obj.group(1)
application_name = match_obj.group(2)
else:
print("请按照以下方式运行服务器程序:")
print("python3 web_server.py 8888 mini_web_framework:application")
return
# 读取配置文件
with open("server_configuration.conf") as file:
conf_info = eval(file.read())
sys.path.append(conf_info["dynamic_path"])
# 通过变量frame_name导入mini_web_framework模块
web_frame = __import__(frame_name)
print(web_frame)
# 使用getattr函数获取mini_web_framework模块中application函数引用
app = getattr(web_frame, application_name)
wsgi_server = WSGIServer(port, app, conf_info["static_path"])
wsgi_server.run_forever()
if __name__ == "__main__":
main()
上述代码在第154行出打开并读取了配置文件,而后将读取的内容通过内置的eval()函数转换成了字典,接着通过以键取值方式在对应位置替换写死的参数。
4. 总结
上述代码实现了web服务器、web框架、静态资源之间的完全解耦,实现开发时,只需要将三者按照文章开头的路径关系进行部署,而后在配置文件中指定对应的web框架和静态资源路径名称,则可以实现不同的web服务器、web框架、静态资源之间的任意配合,而无需修改任意一个的源码。