web服务器运行原理

3.1.1 HTTP 服务器运行原理

web服务器运行原理_第1张图片
之前在实现的程序中,主要代码都实现在上图的左半部分。

服务器的运行和 WEB 应用的处理,都是在一个文件中实现的。

这几天的工作,就是把程序解耦,将功能分离,服务器只用来提供WEB服务,WEB应用用来实现数据处理。

大家可以了解一下开发中比较常用的WEB框架,比如 Apache ,Nigix,Tomcat等。

没有一个服务器框架安装完成后,就完成了WEB应用的开发的。

因为服务器根本不知道你要完成的功能是什么,所以只提供给你服务,而应用的功能按照服务的接口来完成。然后让服务器响应处理。

3.1.2 原始服务器回顾分析

在前面的课程中,我们实现过一个 HTTP 服务器,我们就在这个服务器的基础上,来实现这阶段的 MiniWEB 框架。

首先,先来回顾一下这个HTTP服务器的代码

注意:将代码复制到工程文件中之后,还需要将资源文件复制到工程目录中

资源文件所在路径
在这里插入图片描述
web服务器运行原理_第2张图片
原始服务器 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.初始化方法中创建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

WSGI是什么? WSGI,全称 Web Server Gateway Interface,是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。

用来描述web server如何与web application通信的规范。

实际上,WSGI协议中,定义的接口函数就是 application ,定义如下:


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

Hello, web!

'
]

这个函数有两个参数: 参数一: web服务器向数据处理文件中传递请求相关的信息,一般为请求地址,请求方式等,传入类型约定使用字典 参数二: 传入一个函数,使用函数回调的形式,将数据处理的状态结果返回给服务器,服务器的函数一般用来存储返回的信息,用来组合响应头信息 返回值: 用来返回具体的响应体数据.

服务器和框架应用程序在共同遵守了这个协议后,就可以通过 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  # 响应头
            #拼接返回的响应头信息
            #因是返回是以列表装元组的形式返回,所以遍历列表,然后拼接元组里的信息
            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 总结:

代码在开发过程中,应该遵循高内聚低耦合的思想
静态数据是指在访问时不会发生变化的数据
动态数据是指在访问时会服务的状态,条件等发生不同的变化,得到的数据不同
通过WSGI接口,实现了服务器和框架的功能分离
服务器和框架应用的功能分离,使服务器的迁移,维护更加简单

你可能感兴趣的:(web,服务器)