精析Python3实现动态web服务(附服务端源码)

实现一个简单的静态web网站,只需将写好的html页面上传到特定的web服务器软件即可,但静态网页其实和图片没什么区别,每次更新网站内容,都需要重新制作html页面,然后上传给提供web服务的软件,替换原来的html页面,也就完成了更新,以一个正常人的思维方式,每次更新内容都要重新生成html的工作实在太蛋疼了!那么能不能让程序自己生成html呢?当然可以,程序就是为将人类从重复繁杂的工作中解放出来而生的!

如果我们提供一个动态网站服务,至少应考虑以下四点:

1.要有稳定的web服务程序(可以使用知名的apache,nginx,这里为了探究原理,我们自己用多进程写一个简单的web服务);

2.要有可用的web网页模板(网络上web模版的数量堪比ppt模板,当然我们可以自己画一张, 10分钟后...算了-_-///,作为后端人员,我们这里用朴素的信息显示就好)

3.要有可填充html模板的内容(内容一般从自己的数据库里取,篇幅所限,我们这里用time.ctime()函数,模拟数据库动态数据);

4.要有处理填写内容的逻辑(这个就是我们要今天主要研究的按照wsgi标准实现的简单的web框架);


一个优秀的动态web框架应该是这样的:

1.web框架要和 web服务器软件 分离开;如果把大量的逻辑处理语句,和html放到一个文件中,后期会难以维护(这也是现在多人开发推崇MVC的原因);

2.一个优秀的web框架要和web服务器软件 有良好的交互通信;这时就需要一个数据交互的标准WSGI(python为自身web框架制定了WSGI标准);

3.一个优秀的web框架要实现,和数据库有良好的读写通信方法;

关于WSGI标准

为了方便表述,先举一个栗子:

import time

def app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-Type', 'text/plain')]
    start_response(status, response_headers)
    return str(environ) + '==Hello world from a simple WSGI application!--->%s\n' % time.ctime()

WIGS模型的要点:

1.在web框架模块,以上面的栗子为例,web服务器软件会向web框架传递一个列表(environ)和一个函数(函数体在web服务器软件中实现)的引用(start_response),然后web框架要实现一个app函数,并将 "一个列表"和"一个函数的引用",作为两个参数;

2.传递过来的列表内部存储了N个元组,这些元组包含了web服务器接收到的客户端浏览器的请求信息, 传递过来的函数参数的引用,可以用来返回请求资源的状态反馈(如果请求的资源可以访问,就会返回200,如果资源无法访问,就返回404或502之类的错误;

3.传递过来的函数引用的调用比return更靠前,这样可以在返回正式的网页之前的这段时间,让web服务器软件做好接收数据的准备;(其实可以将函数的引用作为web框架与web服务器软件传递数据的的一种快捷方式);

扩展:

其实双重返回的设计思路很常见,比如在tcp四次挥手的过程中,第二次和第三次挥手都是服务器发送数据,客户端接收数据;

第二次服务端向客户端说("客户端,我收到你主动关闭本次连接的消息了!"),第三次服务端向客户端说("客户端,我已经关闭了这次的发送连接,不会给你发数据了,收到了记得回我个消息哈!");

也许有人会认为,既然第二次和第三次都是服务端向客户端发送数据,那应该可以将两条消息一起发送,但实际上,服务器关闭发送数据的通道是需要一定的时间的,如果第二次和第三次一起发送,客户端浏览器就不能在发送第一次消息后,及时确认消息是否送达,而在tcp连接中,及时"确认送达"是一件非常重要的事情!

在web服务器软件模块,至少要实现三个功能:

1.创建 包含客户端请求头消息的列表(作为第一个参数传递);

2.创建一个可以解析返回状态信息的函数(作为第二个参数传递);

3.接收web框架内app函数返回的body,并将body与作为第二个参数的引用的函数的返回状态值组合,一同发送给客户端浏览器;

实现源码

1.作者自己编写小型web服务器(以上篇 gevent实现静态web服务器为基础改写)

web_server.py


import socket
from sys import argv
import gevent
from gevent import monkey
import time
import random
import re

# 服务器类
class WISG(object):
    def __init__(self, port, app):
        self.port = port
        self.root_dir = "./HTML"        
        # 创建主套接字
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 允许端口重用
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # 主套接字绑定端口
        self.server_socket.bind(("", self.port))
        # 主套接字转为被动模式
        self.server_socket.listen(128)

        # 获取web框架中的函数引用
        self.app = app
        pass

    # 启动服务器对象的入口函数
    def run_forever(self):
        self.create_new_socket()
        pass
    
    # 创建新的套接字,使用gevent,使新的套接字以消耗少量资源的协程方式运行
    def create_new_socket(self):
        while True:
            new_client_socket, new_client_socket_addr = self.server_socket.accept()
            gevent.spawn(self.deal_accept_data, new_client_socket)

    # 处理接收到的数据
    def deal_accept_data(self, new_client_socket):
        recv_data = new_client_socket.recv(1024)
        # 接收到的请求为utf-8格式,解析数据
        recv_data = recv_data.decode("utf-8")
        # 如果收到客户端发送的空字符,则关闭连接
        if not recv_data:
            return
        # 将接收到的数据转换为列表
        recv_data_list = recv_data.splitlines()

        # 获取请求头信息
        the_request_header = recv_data_list[0]
        
        file_name = self.get_file_name(the_request_header)
        
        if file_name.endswith(".html"):


            print("请求的文件名为%s"%(file_name)) 
            

            # 向客户端发送文件
            self.send_html(file_name, new_client_socket)
            new_client_socket.close()

        else:
            print("进入动态选择模块...")
            print("请求的文件名为%s"%(file_name))
            # 得到web框架返回的数据

            # 创建一个字典
            environ = dict()
            environ["PATH_INFO"] = file_name

            #获得内容
            dynamic_content = self.app(environ, self.set_response_header)
            new_client_socket.send(self.dynamic_response_headers_info + dynamic_content.encode("utf-8"))
            new_client_socket.close()

    # 根据请求头信息,获得本地对应以.html或.py尾缀的文件名
    def get_file_name(self, the_request_header):
        """GET /index.html HTTP/1.1"""
        file_name = re.match(r"[^/]+([^ ]+).*", the_request_header).group(1)
        if file_name == "/":
            file_name = "/index.html"
        return file_name
        pass      

    # 发送静态文件的html到客户端
    def send_html(self, file_name, new_client_socket):
        try:
            f = open(self.root_dir+file_name, "rb")
        except Exception as res:
            print(res)
            print("无法找到网页404")
        else:
            content = f.read()

        respond_body = content
        respond_header = "HTTP/1.1 200 OK \r\n"
        respond_header += "Content-Type: text/html; charset=utf-8\r\n"
        respond_header = respond_header + "\r\n"
        
        # 发送回应
        new_client_socket.send(respond_header.encode("utf-8"))
        new_client_socket.send(respond_body)
        print("内容发送成功!")
        pass

    def set_response_header(self, status, headers):


        #将从web框架收到的状态码,和返回的头信息存储到一个列表里面
        self.dynamic_respond_header = [status, headers]
        # 组建返回头信息
        dynamic_respond_header = "HTTP/1.1 %s \r\n"
        dynamic_respond_header += "%s:%s\r\n"%(headers[0][0], headers[0][1])
        dynamic_respond_header += "\r\n"
        # 将列表中的数据进行整理,转为可直接使用的"返回头"信息,然后存到类变量dynamic_response_headers_info
        self.dynamic_response_headers_info =  dynamic_respond_header.encode("utf-8")

        pass


def main():
    monkey.patch_all()
    # 创建web服务器
    if len(argv) == 3:
        port = int(argv[1])
        # web框架名称
        frame_name = re.match(r"([^:]+):(.+)", argv[2]).group(1)
        # web框架中主调函数的名称
        app_name = re.match(r"([^:]+):(.+)", argv[2]).group(2)

        # 动态导入框架函数app
        web_frame_module = __import__(frame_name)
        # 获得框架中的主调函数
        app = getattr(web_frame_module, app_name)

    # 传入端口号,和来自web框架的函数app
    web_server = WISG(port, app)
    print("app的名字为%s,框架的名字为%s,端口号为%s"%(frame_name, app_name, port))
    print("请在地址栏访问 127.0.0.1:%d"%(port))

    # 启动web服务器
    web_server.run_forever()
    pass

if __name__ == "__main__":
    main()



2.按照wsgi标准实现的web框架

web_frame.py



import time
import re
import codecs

template_root = "./HTML"


file_name = None


def read_file(file_name):
    try:
        file_name = template_root+file_name
        f = codecs.open(file_name, "r", "utf-8")
    except Exception as e:
        print(e)
        print("无法打开%s"%file_name)
    else:
        content = f.read()
        # 这里我们假装从mysql获得了数据
        data_from_mysql = "我是来自数据库的动态数据......"+'当前的时间为--->%s\n' % time.ctime()
        # 将模板中的{content}换成我们的"动态数据"
        content = str(re.sub(r"{content}", data_from_mysql, content))

        f.close()
        return content

# web框架入口 
def app(environ, start_response):

    """environ包含需要访问的.py文件(模板)的名称, start_response代表来自web框架的函数的引用"""
    # 设置返回的状态码信息
    status = "200 OK"
    # 设置返回的网页类型
    response_headers = [('Content-Type', 'text/html;charset=utf-8')]

    # 向web框架中定义的函数start_response中传入头信息(状态码,网页类型) 
    start_response(status, response_headers)
    
    file_name = environ["PATH_INFO"]
    content = read_file(file_name)
    # 将动态的数据返回给服务器框架
    return content


README

终端运行:
python3 web_server.py 8888 web_frame:app


目录样式

小结:

生成动态网页的本质,其实是让程序去替换html中特定部分的内容,换句话说,就是把html页面当成一个没有实际内容的模板,而当用户通过网址访问网页的时候,web框架就把动态的内容填到html模板里面,这样动态生成了带有内容的html网页,web服务器把带有内容的html网页发送给用户浏览器,最后用户收到了含有完整内容的网页,搞定!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • web静态服务器 服务端: 1.1.1显示固定的页面 参考代码: import socket from multi...
    chen_000阅读 2,065评论 0 1
  • 在Web应用中,服务器把网页传给浏览器,实际上就是把网页的HTML代码发送给浏览器,让浏览器显示出来。而浏览器和服...
    壁花烧年阅读 614评论 0 0
  • #简介 深入学习Flask作为RestFul服务端的架构思路。了解Flask设计哲学、应用场景。包含从开发环境搭建...
    爱睡觉的树阅读 2,446评论 0 1
  • 一、Web开发 Browser/Server模式目前最流行,简称BS架构。在BS架构下,客户端只需要浏览器,应用程...
    时间之友阅读 848评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,523评论 28 53