美文网首页
Python 18 MiniWEB项目

Python 18 MiniWEB项目

作者: Cestine | 来源:发表于2018-12-09 10:59 被阅读0次

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

    相关文章

      网友评论

          本文标题:Python 18 MiniWEB项目

          本文链接:https://www.haomeiwen.com/subject/uouhhqtx.html