Python 18 MiniWEB项目

2018-12-09  本文已影响0人  Cestine

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. 服务器和框架应用的功能分离,使服务器的迁移,维护更加简单

上一篇下一篇

猜你喜欢

热点阅读