Python web框架开发 - WSGI协议

仅供学习,转载请注明出处

前情介绍

前面我利用TCP协议,返回HTTP数据的方法,实现了web静态页面返回的服务端功能。
但是这样并不能满足大部分的功能需求。

首先需要知道,浏览器进行http请求的时候,不单单会请求静态资源,还可能需要请求动态页面。
那么什么是静态资源,什么是动态页面呢?

静态资源 : 例如html文件、图片文件、css、js文件等,都可以算是静态资源
动态页面:当请求例如登陆页面、查询页面、注册页面等可能会变化的页面,则是动态页面。

哦,好像很厉害

浏览器请求动态页面过程

通过下图来了解一下页面HTTP请求的过程,如下:

前面我开发的web静态服务器就是只做了中间部分,只用来返回静态资源。那么后面的应用程序框架则是处理动态请求的页面。

例如:浏览器发送一个 http://172.16.5.81:7788/login.py 的请求,则返回浏览器一个关于登陆的页面,其中包含了服务端的当前时间等。

还可以看到web服务器是用wsgi协议调用应用程序框架的,这里我们先不讲什么是wsgi协议,先看看我之前写的静态web服务端。

可以通过以下访问前面开发的web静态服务器:
Python 开发web服务器,多进程优化
Python 开发web服务器,多线程

那么,我先来取这两个代码中的一个来进行优化开发,就采用多进程的版本吧。

下一步的优化目的,首先就是要将原来面向过程的代码,修改为面向对象,做好封装。

查看多进程web服务端代码 - 面向过程

#coding=utf-8
from socket import *
import re
import multiprocessing

def handle_client(client_socket):
    """为一个客户端服务"""
    # 接收对方发送的数据
    recv_data = client_socket.recv(1024).decode("utf-8") #  1024表示本次接收的最大字节数
    # 打印从客户端发送过来的数据内容
    #print("client_recv:",recv_data)
    request_header_lines = recv_data.splitlines()
    for line in request_header_lines:
        print(line)
     
    # 返回浏览器数据
    # 设置内容body
    # 使用正则匹配出文件路径
    print("------>",request_header_lines[0])
    print("file_path---->","./html/" + re.match(r"[^/]+/([^\s]*)",request_header_lines[0]).group(1))
    ret = re.match(r"[^/]+/([^\s]*)",request_header_lines[0])
    if ret:
       file_path = "./html/" + ret.group(1)
       if file_path == "./html/":
          file_path = "./html/index.html"
       print("file_path *******",file_path)

    try:
       # 设置返回的头信息 header
       response_headers = "HTTP/1.1 200 OK\r\n" # 200 表示找到这个资源
       response_headers += "\r\n" # 空一行与body隔开
       # 读取html文件内容
       file_name = file_path # 设置读取的文件路径
       f = open(file_name,"rb") # 以二进制读取文件内容
       response_body = f.read()
       f.close()   
       # 返回数据给浏览器
       client_socket.send(response_headers.encode("utf-8"))   #转码utf-8并send数据到浏览器
       client_socket.send(response_body)   #转码utf-8并send数据到浏览器
    except:
       # 如果没有找到文件,那么就打印404 not found
       # 设置返回的头信息 header
       response_headers = "HTTP/1.1 404 not found\r\n" # 200 表示找到这个资源
       response_headers += "\r\n" # 空一行与body隔开
       response_body = "<h1>sorry,file not found</h1>"
       response = response_headers + response_body
       client_socket.send(response.encode("utf-8"))

    #client_socket.close()

def main():
   # 创建套接字
   server_socket = socket(AF_INET, SOCK_STREAM)
   # 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7788端口
   server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
   # 设置服务端提供服务的端口号
   server_socket.bind(('', 7788))
   # 使用socket创建的套接字默认的属性是主动的,使用listen将其改为被动,用来监听连接
   server_socket.listen(128) #最多可以监听128个连接
   # 开启while循环处理访问过来的请求 
   while True:
      # 如果有新的客户端来链接服务端,那么就产生一个新的套接字专门为这个客户端服务
      # client_socket用来为这个客户端服务
      # server_socket就可以省下来专门等待其他新的客户端连接while True:
      client_socket, clientAddr = server_socket.accept()
      # handle_client(client_socket)
      # 设置子进程
      new_process = multiprocessing.Process(target=handle_client,args=(client_socket,))
      new_process.start() # 开启子进程

      # 因为子进程已经复制了父进程的套接字等资源,所以父进程调用close不会将他们对应的这个链接关闭的
      client_socket.close()


if __name__ == "__main__":
   main()

先来回顾一下运行的情况:

好了,看到运行也是正常的,那么下面就要来分析一下,如何将代码封装为对象。

封装对象分析

首先我需要定义一个webServer类,然后将访问静态资源的功能都封装进去。

#coding=utf-8
from socket import *
import re
import multiprocessing

class WebServer:

   def __init__(self):
       # 创建套接字
       self.server_socket = socket(AF_INET, SOCK_STREAM)
       # 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7788端口
       self.server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
       # 设置服务端提供服务的端口号
       self.server_socket.bind(('', 7788))
       # 使用socket创建的套接字默认的属性是主动的,使用listen将其改为被动,用来监听连接
       self.server_socket.listen(128) #最多可以监听128个连接

   def start_http_service(self):
       # 开启while循环处理访问过来的请求
       while True:
           # 如果有新的客户端来链接服务端,那么就产生一个新的套接字专门为这个客户端服务
           # client_socket用来为这个客户端服务
           # self.server_socket就可以省下来专门等待其他新的客户端连接while True:
           client_socket, clientAddr = self.server_socket.accept()
           # handle_client(client_socket)
           # 设置子进程
           new_process = multiprocessing.Process(target=self.handle_client,args=(client_socket,))
           new_process.start() # 开启子进程
           # 因为子进程已经复制了父进程的套接字等资源,所以父进程调用close不会将他们对应的这个链接关闭的
           client_socket.close()

   def handle_client(self,client_socket):
       """为一个客户端服务"""
       # 接收对方发送的数据
       recv_data = client_socket.recv(1024).decode("utf-8") #  1024表示本次接收的最大字节数
       # 打印从客户端发送过来的数据内容
       #print("client_recv:",recv_data)
       request_header_lines = recv_data.splitlines()
       for line in request_header_lines:
           print(line)

       # 返回浏览器数据
       # 设置内容body
       # 使用正则匹配出文件路径
       print("------>",request_header_lines[0])
       print("file_path---->","./html/" + re.match(r"[^/]+/([^\s]*)",request_header_lines[0]).group(1))
       ret = re.match(r"[^/]+/([^\s]*)",request_header_lines[0])
       if ret:
          file_path = "./html/" + ret.group(1)
          if file_path == "./html/":
             file_path = "./html/index.html"
          print("file_path *******",file_path)

       try:
          # 设置返回的头信息 header
          response_headers = "HTTP/1.1 200 OK\r\n" # 200 表示找到这个资源
          response_headers += "\r\n" # 空一行与body隔开
          # 读取html文件内容
          file_name = file_path # 设置读取的文件路径
          f = open(file_name,"rb") # 以二进制读取文件内容
          response_body = f.read()
          f.close()
          # 返回数据给浏览器
          client_socket.send(response_headers.encode("utf-8"))   #转码utf-8并send数据到浏览器
          client_socket.send(response_body)   #转码utf-8并send数据到浏览器
       except:
          # 如果没有找到文件,那么就打印404 not found
          # 设置返回的头信息 header
          response_headers = "HTTP/1.1 404 not found\r\n" # 200 表示找到这个资源
          response_headers += "\r\n" # 空一行与body隔开
          response_body = "<h1>sorry,file not found</h1>"
          response = response_headers + response_body
          client_socket.send(response.encode("utf-8"))


def main():
    webserver = WebServer()
    webserver.start_http_service()

if __name__ == "__main__":
    main()

好了,从上面的代码来看,我已经将前面面向过程的代码修改为面向对象了。

运行一下看看有没有错误:

坐看淡定正常请求成功

思考:那么,已经封装为对象了,下一步还要优化什么呢?

好了,请求静态资源的页面已经可以了,那么如果请求动态的页面呢?
如果web服务端是java写的话,通常http请求就是http:xxxx/xxx.jsp
如果web服务端是php写的话,通常http请求就是http:xxxx/xxx.php
那么,既然这次我使用python来写,就可以定义动态资源的请求为http:xxxx/xxx.py

那么如果来识别并执行 http:xxxx/xxx.py 的请求呢?

增加识别动态资源请求的功能

需求:识别并返回http:xxxx/xxx.py 的请求
那么让我想一下,先做个简单的,例如:我请求一个http的请求 http:xxxx/time.py 则返回一个当前服务端的时间给浏览器。

那么如果http请求了一个py结尾的请求,我需要在哪里处理呢?

还有我可以用什么方法来判断 .py 后缀的文件呢?
用正则匹配?
其实可以使用endswith("文件后缀")的方法来判断处理。

识别文件名后缀的方法 file_name.endswith(".py")
测试使用如下:

In [1]: file_name = "time.py"

# 匹配后缀为 .html ,直接报False
In [3]: file_name.endswith(".html")
Out[3]: False

# 匹配后缀为 .py ,则报True
In [4]: file_name.endswith(".py")
Out[4]: True

那么下面就可以来写写这里判断的处理分支了。

测试执行一下:

  • 首先请求HTML等静态资源页面
  • 请求动态资源页面

那么下面编写一下当接受到动态资源请求时候,返回浏览器的数据

先简单地写一串HTML+当前服务器时间的内容吧。

       if file_path.endswith(".py"):
           # 请求动态资源
           print("这个是请求动态资源的!")
           # 设置返回的头信息 head
           response_headers = "HTTP/1.1 200 OK\r\n" # 200 表示找到这个资源
           response_headers += "\r\n" # 空一行与body隔开
           # 设置返回浏览器的body内容
           response_body = "<h1>hello this is xxx.py</h1><br>"
           response_body += time.ctime()
           response = response_headers + response_body
           # 返回数据给浏览器
           client_socket.send(response.encode("utf-8"))

运行一下测试看看:

从这里已经可以正常返回动态页面的内容的了。

思考 :那么如果将动态处理页面的代码在web服务端不断地写,代码就会很庞大。是否可以拆分出来,放到另一个模块进行编写呢?

这里就涉及到 web服务端 与 业务处理服务端 之间的一个协议了,这个业界内通用的协议就是 WSGI协议

为什么需要 WSGI协议

在讲WSGI协议之前,我先把处理动态页面的功能拆分到另一个模块文件中。

创建一个处理业务的框架模块,并讲刚才处理返回浏览器的代码复制进去:

import time

def application(client_socket):
    # 请求动态资源
    print("这个是请求动态资源的!")
    # 设置返回的头信息 head
    response_headers = "HTTP/1.1 200 OK\r\n"  # 200 表示找到这个资源
    response_headers += "\r\n"  # 空一行与body隔开
    # 设置返回浏览器的body内容
    response_body = "<h1>hello this is xxx.py</h1><br>"
    response_body += time.ctime()
    response = response_headers + response_body
    # 返回数据给浏览器
    client_socket.send(response.encode("utf-8"))

那么在原来的webserver.py模块只要import该模块文件,使用application()方法就可以处理刚才的业务了。

webserver.py 模块操作如下

好了,做了这个解耦的操作之后,下面来运行测试一下:

从上面的调用结果来看,的确是调用成功啦,理解大概如下图:

但是可以看出来,webserver想要调用 framework处理业务的话,就要这样去写,如下:

framework.application(client_socket)

这种方式虽然可行,但是在业界中是不通用的。也就是说这种调用方法扔给别人写的框架,就无法兼容了。

例如:假设我后面改用Django、Flask框架来处理业务,此时一定就不是用这种方式来通讯调用的。

那么该用什么方式呢?

是否可以修改服务器和架构代码而确保可以在多个架构下,保证与web服务器之间的通讯调用呢?

答案就是 Python Web Server Gateway Interface (或简称 WSGI,读作“wizgy”)。

WSGI我来啦

WSGI协议的介绍

WSGI允许开发者将选择web框架和web服务器分开。可以混合匹配web服务器和web框架,选择一个适合的配对。比如,可以在Gunicorn 或者 Nginx/uWSGI 或者 Waitress上运行 Django, Flask, 或 Pyramid。真正的混合匹配,得益于WSGI同时支持服务器和架构:

web服务器必须具备WSGI接口,所有的现代Python Web框架都已具备WSGI接口,它让你不对代码作修改就能使服务器和特点的web框架协同工作。

WSGI由web服务器支持,而web框架允许你选择适合自己的配对,但它同样对于服务器和框架开发者提供便利使他们可以专注于自己偏爱的领域和专长而不至于相互牵制。其他语言也有类似接口:java有Servlet API,Ruby 有 Rack。

好像很厉害!!

定义WSGI接口

WSGI接口定义非常简单,它只要求Web开发者实现一个函数,就可以响应HTTP请求。

我们来看一个最简单的Web版本的“Hello World!”:

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return 'Hello World!'

上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:

  • environ:一个包含所有HTTP请求信息的dict对象;
  • start_response:一个发送HTTP响应的函数。

整个application()函数本身没有涉及到任何解析HTTP的部分,也就是说,把底层web服务器解析部分和应用程序逻辑部分进行了分离,这样开发者就可以专心做一个领域了

不过,等等,这个application()函数怎么调用?如果我们自己调用,两个参数environ和start_response我们没法提供,返回的str也没法发给浏览器。

所以application()函数必须由WSGI服务器来调用。有很多符合WSGI规范的服务器。而我们此时的web服务器项目的目的就是做一个既能解析静态网页还可以解析动态网页的WSGI服务器。

说了那么多,敢不敢秀一波代码操作

编写framwork支持WSGI协议,实现浏览器显示 hello world

framwork.py:

直接协议规范代码复制进去。

那么在webserver.py的部分,就需要接受application返回的信息。
首先,start_response 就是在framwork设置http请求header信息的。而return 就是返回http请求body信息的。

那么知道了这两点之后,下一步要做的。就是想办法来接受这个application的设置header以及body信息。

那么怎么处理呢?

webserver.py

为了方便对比查看这两个文件的代码,使用pycharm同时打开两个视图窗口来查看。

好了,下面来继续看看。

下面来创建这两个形参:

  • environ:一个包含所有HTTP请求信息的dict对象;
  • start_response:一个发送HTTP响应的函数。

先随便写个空的,来填入WSGI规范所需要的参数。
其中response_body通过return的返回值就可以接受到了。
那么response_header该怎么处理呢?

可以从代码中看出start_response 在webserver.py 传入到 framwork.py 的application中调用。
其中在application中就直接设置header信息到start_response的参数中。然后我在webserver.py能否直接将其取出来,拼接成header信息呢?

编写start_response 接收 header 信息

那么首先编写一个类变量来保存信息,然后测试打印一下看看。

运行测试一下看看:

那么只要讲其保存到self.application_header中,我就可以在类方法的任意一个地方进行拆分或者拼接成所需要的http header返回值了。

编写如下:

运行测试看看。

好啦,这样就得到了完成的header内容啦,那么下面将其拼接body内容,然后返回浏览器中显示。

运行测试如下:

哦,就这样成功了么?

肯定没有啦,我如果请求返回一个页面,怎么可能就返回一个hello world呢?

下一步肯定是要能够正常返回index.py这样的正常页面啦。但是这个篇章也写得比较长了,

下个篇章继续。

本次开发的完整代码如下:

webserver.py

#coding=utf-8
from socket import *
import re
import multiprocessing
import time
import framework


class WebServer:

   def __init__(self):
       # 创建套接字
       self.server_socket = socket(AF_INET, SOCK_STREAM)
       # 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7788端口
       self.server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
       # 设置服务端提供服务的端口号
       self.server_socket.bind(('', 7788))
       # 使用socket创建的套接字默认的属性是主动的,使用listen将其改为被动,用来监听连接
       self.server_socket.listen(128) #最多可以监听128个连接

   def start_http_service(self):
       # 开启while循环处理访问过来的请求
       while True:
           # 如果有新的客户端来链接服务端,那么就产生一个新的套接字专门为这个客户端服务
           # client_socket用来为这个客户端服务
           # self.server_socket就可以省下来专门等待其他新的客户端连接while True:
           client_socket, clientAddr = self.server_socket.accept()
           # handle_client(client_socket)
           # 设置子进程
           new_process = multiprocessing.Process(target=self.handle_client,args=(client_socket,))
           new_process.start() # 开启子进程
           # 因为子进程已经复制了父进程的套接字等资源,所以父进程调用close不会将他们对应的这个链接关闭的
           client_socket.close()

   def handle_client(self,client_socket):
       """为一个客户端服务"""
       # 接收对方发送的数据
       recv_data = client_socket.recv(1024).decode("utf-8") #  1024表示本次接收的最大字节数
       # 打印从客户端发送过来的数据内容
       #print("client_recv:",recv_data)
       request_header_lines = recv_data.splitlines()
       for line in request_header_lines:
           print(line)

       # 返回浏览器数据
       # 设置内容body
       # 使用正则匹配出文件路径
       print("------>",request_header_lines[0])
       print("file_path---->","./html/" + re.match(r"[^/]+/([^\s]*)",request_header_lines[0]).group(1))
       ret = re.match(r"[^/]+/([^\s]*)",request_header_lines[0])
       if ret:
          file_path = "./html/" + ret.group(1)
          if file_path == "./html/":
             file_path = "./html/index.html"
          print("file_path *******",file_path)

       # 判断file_path是否py文件后缀,如果是则请求动态资源,否则请求静态资源
       if file_path.endswith(".py"):

           # framework.application(client_socket)
           # 支撑WGSI协议的调用方式
           environ = {}
           response_body = framework.application(environ, self.start_response)
           # 设置返回的头信息header
           # 1.拼接第一行HTTP/1.1 200 OK + 换行符内容
           response_headers = "HTTP/1.1 " + self.application_header[0] + "\r\n"
           # 2.循环拼接第二行或者多行元组内容:Content-Type:text/html
           for var in self.application_header[1]:
               response_headers += var[0]+":"+var[1] + "\r\n"
           # 3.空一行与body隔开
           response_headers += "\r\n"
           # 4.打印看看header的内容信息
           print("response_header=")
           print(response_headers)

           # 设置返回的浏览器的内容
           response = response_headers + response_body
           client_socket.send(response.encode("utf-8"))

       else:
           # 请求静态资源
           try:
              # 设置返回的头信息 header
              response_headers = "HTTP/1.1 200 OK\r\n" # 200 表示找到这个资源
              response_headers += "\r\n" # 空一行与body隔开
              # 读取html文件内容
              file_name = file_path # 设置读取的文件路径
              f = open(file_name,"rb") # 以二进制读取文件内容
              response_body = f.read()
              f.close()
              # 返回数据给浏览器
              client_socket.send(response_headers.encode("utf-8"))   #转码utf-8并send数据到浏览器
              client_socket.send(response_body)   #转码utf-8并send数据到浏览器
           except:
              # 如果没有找到文件,那么就打印404 not found
              # 设置返回的头信息 header
              response_headers = "HTTP/1.1 404 not found\r\n" # 200 表示找到这个资源
              response_headers += "\r\n" # 空一行与body隔开
              response_body = "<h1>sorry,file not found</h1>"
              response = response_headers + response_body
              client_socket.send(response.encode("utf-8"))

   def start_response(self,status,header):
       self.application_header = [status,header]
       print("application_header=",self.application_header)

def main():
    webserver = WebServer()
    webserver.start_http_service()

if __name__ == "__main__":
    main()

framework.py

# 支撑WGSI协议
def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return 'Hello World!'

关注微信公众号,回复【资料】、Python、PHP、JAVA、web,则可获得Python、PHP、JAVA、前端等视频资料。