我们接下来学习的目的就是为了开发一个Web应用软件。那到底什么是Web应用软件呢?
对于传统的应用软件来说,基本上都是部署于单机使用的,而Web应用软件则不一样,Web应用软件是基于B/S架构的,B与S部署于不同的计算机上,并且基于网络通信,所以B与S的本质都是套接字,其中B指的是浏览器&无需开发,我们需要开发的是S端。
很明显我们在开发套接字服务端S时,思路应该是这样的
#1、接收套接字客户端B发来的请求信息并加以解析 #2、根据解析出的结果,加以判断,获取/生成用户想要的数据 #3、返回数据给套接字客户端B其中上述1和3属于套接字的底层通信,而2则属于应用程序的逻辑,所以我们通常说S端的开发由两大部分构成:server和application
# Sever:称之为服务器程序,指的是套接字的通信相关事宜,包含1和3 # application:称之为应用程序,指的是应用程序的逻辑,包含2综上所述,一个完整的Web应用如下图所示:
按照上述思路,开发S端如下
# S端 import socket def make_server(ip, port, app): # 代表server # 处理套接字通信相关事宜 sock = socket.socket() sock.bind((ip, port)) sock.listen(5) print(Starting development server at http://%s:%s/ %(ip,port)) while True: conn, addr = sock.accept() # 1、接收浏览器发来的请求信息 recv_data = conn.recv(1024) # print(recv_data.decode(utf-8)) # 2、将请求信息直接转交给application处理,得到返回值 res = app(recv_data) # 3、向浏览器返回消息(此处并没有按照http协议返回) conn.send(res) conn.close() def app(environ): # 代表application # 处理业务逻辑 return bhello world if __name__ == __main__: make_server(127.0.0.1, 8008, app) # 在客户端浏览器输入:http://127.0.0.1:8008 会报错(注意:请使用谷歌浏览器)目前S端已经可以正常接收浏览器发来的请求消息了,但是浏览器在接收到S端回复的响应消息bhello world时却无法正常解析 ,因为浏览器与S端之间收发消息默认使用的应用层协议是HTTP,浏览器默认会按照HTTP协议规定的格式发消息,而S端也必须按照HTTP协议的格式回消息才行,所以接下来我们详细介绍HTTP协议
HTTP协议详解链接地址:http://www.cnblogs.com/linhaifeng/articles/8243379.html
S端修订版本:处理HTTP协议的请求消息,并按照HTTP协议的格式回复消息
# S端 import socket def make_server(ip, port, app): # 代表server # 处理套接字通信相关事宜 sock = socket.socket() sock.bind((ip, port)) sock.listen(5) print(Starting development server at http://%s:%s/ %(ip,port)) while True: conn, addr = sock.accept() # 1、接收并处理浏览器发来的请求信息 # 1.1 接收浏览器发来的http协议的消息 recv_data = conn.recv(1024) # 1.2 对http协议的消息加以处理,简单示范如下 ll=recv_data.decode(utf-8).split(\r\n) head_ll=ll[0].split() environ={} environ[PATH_INFO]=head_ll[1] environ[method]=head_ll[0] # 2:将请求信息处理后的结果environ交给application,这样application便无需再关注请求信息的处理,可以更加专注于业务逻辑的处理 res = app(environ) # 3:按照http协议向浏览器返回消息 # 3.1 返回响应首行 conn.send(bHTTP/1.1 200 OK\r\n) # 3.2 返回响应头(可以省略) conn.send(bContent-Type: text/html\r\n\r\n) # 3.3 返回响应体 conn.send(res) conn.close() def app(environ): # 代表application # 处理业务逻辑 return bhello world if __name__ == __main__: make_server(127.0.0.1, 8008, app)此时,重启S端后,再在客户端浏览器输入:http://127.0.0.1:8008便可以看到正常结果hello world了。
我们不仅可以回复hello world这样的普通字符,还可以夹杂html标签,浏览器在接收到消息后会对解析出的html标签加以渲染
# S端 import socket def make_server(ip, port, app): sock = socket.socket() sock.bind((ip, port)) sock.listen(5) print(Starting development server at http://%s:%s/ %(ip,port)) while True: conn, addr = sock.accept() recv_data = conn.recv(1024) ll=recv_data.decode(utf-8).split(\r\n) head_ll=ll[0].split() environ={} environ[PATH_INFO]=head_ll[1] environ[method]=head_ll[0] res = app(environ) conn.send(bHTTP/1.1 200 OK\r\n) conn.send(bContent-Type: text/html\r\n\r\n) conn.send(res) conn.close() def app(environ): # 返回html标签 return b<h1>hello web</h1><img src="https://www.baidu.com/img/bd_logo1.png"></img> if __name__ == __main__: make_server(127.0.0.1, 8008, app)在上述函数app中,python代码与html代码耦合到一起,这是不合理的,我们应该将二者分离开,将html代码放入专门的文件中,于是我们新建timer.html文件,内容如下
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>当前时间为:2020-02-02 20:20:20</h2> </body> </html>S端程序如下
# S端 import socket def make_server(ip, port, app): # 代表server sock = socket.socket() sock.bind((ip, port)) sock.listen(5) print(Starting development server at http://%s:%s/ %(ip,port)) while True: conn, addr = sock.accept() recv_data = conn.recv(1024) ll=recv_data.decode(utf-8).split(\r\n) head_ll=ll[0].split() environ={} environ[PATH_INFO]=head_ll[1] environ[method]=head_ll[0] res = app(environ) conn.send(bHTTP/1.1 200 OK\r\n) conn.send(bContent-Type: text/html\r\n\r\n) conn.send(res) conn.close() def app(environ): # 处理业务逻辑:打开文件,读取文件内容并返回 with open(timer.html, r, encoding=utf-8) as f: data = f.read() return data.encode(utf-8) if __name__ == __main__: make_server(127.0.0.1, 8008, app)上述S端为浏览器返回的都是静态页面(内容都固定的),要想返回动态页面(内容是变化的),那timer.html中的内容就不能写死,可以定义一个特殊的符号(类似于变量名),然后每次用得到的值覆盖该符号即可(类似于为变量赋值),于是timer.html内容修改如下
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>当前时间为:{{ xxx }}</h2> </body> </html>S端修改如下
# S端 import socket def make_server(ip, port, app): # 代表server sock = socket.socket() sock.bind((ip, port)) sock.listen(5) print(Starting development server at http://%s:%s/ %(ip,port)) while True: conn, addr = sock.accept() recv_data = conn.recv(1024) ll=recv_data.decode(utf-8).split(\r\n) head_ll=ll[0].split() environ={} environ[PATH_INFO]=head_ll[1] environ[method]=head_ll[0] res = app(environ) conn.send(bHTTP/1.1 200 OK\r\n) conn.send(bContent-Type: text/html\r\n\r\n) conn.send(res) conn.close() def app(environ): # 处理业务逻辑 import time now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) with open(timer.html, r, encoding=utf-8) as f: data = f.read() data = data.replace({{ xxx }}, now) # 字符串替换 return data.encode(utf-8) if __name__ == __main__: make_server(127.0.0.1, 8008, app) # 在浏览器输入http://127.0.0.1:8008,每次刷新都会看到不同的时间承接上例我们返回动态页面的解决方案,思考一个问题,如果页面中需要引入特殊符号/“变量”过多,那么函数app中要写一大堆字符串替换代码,相当麻烦,有一个模块jinja2很好地帮我们解决了这个问题,本质原理也就是字符串的替换,我们用它就好
timer.html内容如下
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>当前时间为:{{ xxx }}</h2> <h2>当前用户为:{{ user }}</h2> <h2>当前角色:{{ role }}</h2> </body> </html>S端内容如下
# S端 import socket from jinja2 import Template # pip3 install jinja2 def make_server(ip, port, app): # 代表server sock = socket.socket() sock.bind((ip, port)) sock.listen(5) print(Starting development server at http://%s:%s/ %(ip,port)) while True: conn, addr = sock.accept() recv_data = conn.recv(1024) ll=recv_data.decode(utf-8).split(\r\n) head_ll=ll[0].split() environ={} environ[PATH_INFO]=head_ll[1] environ[method]=head_ll[0] res = app(environ) conn.send(bHTTP/1.1 200 OK\r\n) conn.send(bContent-Type: text/html\r\n\r\n) conn.send(res) conn.close() def app(environ): # 处理业务逻辑 import time now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) with open(timer.html, r, encoding=utf-8) as f: data = f.read() template=Template(data) # data = data.replace({{ xxx }}, now) # 字符串替换 data=template.render({xxx:now,user:egon,role:大总管}) return data.encode(utf-8) if __name__ == __main__: make_server(127.0.0.1, 8008, app) # 在浏览器输入http://127.0.0.1:8008,每次刷新都会看到不同的时间综上案例我们可以发现一个规律,在开发S端时,server的功能是复杂且固定的(处理socket消息的收发、解析http协议的数据),而app中的业务逻辑却各不相同(不同的软件就应该有不同的业务逻辑),重复开发复杂且固定的server是毫无意义的,有一个wsgiref模块帮我们写好了server的功能,这样我们便只需要专注于app功能的编写即可
# wsgiref实现了server,即make_server from wsgiref.simple_server import make_server from jinja2 import Template def app(environ, start_response): # 代表application # 1、返回http协议的响应首行和响应头信息 start_response(200 OK, [(Content-Type, text/html)]) # 2、处理业务逻辑:根据请求url的不同返回不同的页面内容 if environ.get(PATH_INFO) == /index: with open(index.html,r, encoding=utf-8) as f: data=f.read() elif environ.get(PATH_INFO) == /timer: import time now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) with open(timer.html, r, encoding=utf-8) as f: data = f.read() template=Template(data) data=template.render({xxx:now,user:egon,role:大总管}) else: data=<h1>Hello, web!</h1> # 3、返回http响应体信息,必须是bytes类型,必须放在列表中 return [data.encode(utf-8)] if __name__ == __main__: # 当接收到请求时,wsgiref模块会对该请求加以处理,然后后调用app函数,自动传入两个参数: # 1 environ是一个字典,存放了http的请求信息 # 2 start_response是一个功能,用于返回http协议的响应首行和响应头信息 s = make_server(, 8011, app) # 代表server print(监听8011) s.serve_forever() # 在浏览器输入http://127.0.0.1:8011/index和http://127.0.0.1:8011/timer会看到不同的页面内容timer.html已经存在了,新增的index.html页面内容如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>主页</h1> </body> </html>上述案例中app在处理业务逻辑时需要根据不同的url地址返回不同的页面内容,当url地址越来越多,需要写一堆if判断,代码不够清晰,耦合程度高,所以我们做出以下优化
# 处理业务逻辑的函数 from jinja2 import Template def index(environ): with open(index.html, r, encoding=utf-8) as f: data = f.read() return data.encode(utf-8) def timer(environ): import time now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) with open(timer.html, r, encoding=utf-8) as f: data = f.read() template=Template(data) data=template.render({xxx:now,user:egon,role:大总管}) return data.encode(utf-8) # 路径跟函数的映射关系 url_patterns = [ (/index, index), (/timer, timer), ] from wsgiref.simple_server import make_server def app(environ, start_response): start_response(200 OK, [(Content-Type, text/html)]) # 拿到请求的url并根据映射关系url_patters执行相应的函数 reuqest_url = environ.get(PATH_INFO) for url in url_patterns: if url[0] == reuqest_url: data = url[1](environ) break else: data = b404 return [data] if __name__ == __main__: s = make_server(, 8011, app) print(监听8011) s.serve_forever()随着业务逻辑复杂度的增加,处理业务逻辑的函数以及url_patterns中的映射关系都会不断地增多,此时仍然把所有代码都放到一个文件中,程序的可读性和可扩展性都会变得非常差,所以我们应该将现有的代码拆分到不同文件中
mysite # 文件夹 ├── app01 # 文件夹 │ └── views.py ├── mysite # 文件夹 │ └── urls.py └── templates # 文件夹 │ ├── index.html │ └── timer.html ├── main.pyviews.py 内容如下:
# 处理业务逻辑的函数 from jinja2 import Template def index(environ): with open(index.html, r, encoding=utf-8) as f: data = f.read() return data.encode(utf-8) def timer(environ): import time now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) with open(timer.html, r, encoding=utf-8) as f: data = f.read() template=Template(data) data=template.render({xxx:now,user:egon,role:大总管}) return data.encode(utf-8)urls.py内容如下:
# 路径跟函数的映射关系 from app01.views import * # 需要导入views中的函数 url_patterns = [ (/index, index), (/timer, timer), ]main.py 内容如下:
from wsgiref.simple_server import make_server from mysite.urls import url_patterns # 需要导入urls中的url_patterns def app(environ, start_response): start_response(200 OK, [(Content-Type, text/html)]) # 拿到请求的url并根据映射关系url_patters执行相应的函数 reuqest_url = environ.get(PATH_INFO) for url in url_patterns: if url[0] == reuqest_url: data = url[1](environ) break else: data = b404 return [data] if __name__ == __main__: s = make_server(, 8011, app) print(监听8011) s.serve_forever()至此,我们就针对application的开发自定义了一个框架,所以说框架的本质就是一系列功能的集合体、不同的功能放到不同的文件中。有了该框架,可以让我们专注于业务逻辑的编写,极大的提高了开发web应用的效率(开发web应用的框架可以简称为web框架),比如我们新增一个业务逻辑,要求为:浏览器输入http://127.0.0.1:8011/home 就能访问到home.html页面
在框架的基础上具体开发步骤如下:
步骤一:在templates文件夹下新增home.html
步骤二:在urls.py的url_patterns中新增一条映射关系
url_patterns = [ (/index, index), (/timer, timer), (/home, home), # 新增的映射关系 ]步骤三:在views.py中新增一个名为home的函数
def home(environ): with open(templates/home.html, r,encoding=utf-8) as f: data = f.read() return data.encode(utf-8)我们自定义的框架功能有限,总结下来大致有这么几个功能
功能1、socket收发消息,指的是server 功能2、根据不同的路径执行不同的处理逻辑/功能 功能3、返回动态页面(字符串的替换),如jinja2在Python中我们可以使用别人开发的、功能更强大的Web框架,如django、tornado、flask等,三种区别如下
#1、django框架 实现了上述功能1、2、3 django框架自定义了server(基于wsgiref+socketserver模块来实现),但这只是提供给开发测试使用的server,并不能在生产环境应用,生产环境部署django的server通常采用uwsgi django自定义了视图系统,即实现了功能2 django自定义了模块系统,即实现了功能2 #2、tornado框架 实现了上述功能1、2、3 tornado框架自定义了server,是异步非阻塞的,效率很高,生产环境也可使用,考虑到高并发,通常选择该框架 #3、flask框架 只实现了上述功能2wsgi协议
综上,我们得知一个Web应用的S端由server和application构成,服务器程序server负责接受HTTP请求、解析HTTP请求、发送HTTP响应等底层套接字通信的处理,都是苦力活,如果我们自己来写这些底层代码,还没开始写应用程序逻辑application呢,就得花个把月去读HTTP规范,所以我们通常直接使用别人开发好的server程序,比如wsgiref、uwsgi、或者框架自带的等等,我们则只需要把精力放在开发应用程序逻辑application上即可。因为我们在开发application时不希望接触到诸如TCP连接、HTTP原始请求和响应格式等底层套接字通信,所以需要在server与application之间建立一套统一的规范/接口,让我们专心用Python编写Web业务。 这个接口就是WSGI:Web Server Gateway Interface。 详见(了解即可):https://www.liaoxuefeng.com/wiki/897692888725344/923057027806560 其实wsgiref、uwsgi等服务器程序server都是遵循wsgi协议的,有了这套协议/标准,server与application的开发就完全解开了耦合,一批程序员可以专注于开发不同的server,一批程序员(就是我们自己)则专注于开发不同的application,只要二者都遵循wsgi协议,则开发的程序可以完美整合,这跟谈恋爱是一个道理,定好你对老婆的要求/标准,只要符合这个标准的女人都可以做你的老婆,反之也一样,所以,找到单身的原因没有 web框架的出现是为了让我们把精力更多地放在开发application上,有的web框架自己实现了高性能的server(比如tornado),有的web框架需要借助别人开发的高性能server=>uwsgi(比如django),之所以可以这么灵活,都要归功于wsgi协议 在开发过程中我们也无需关系wsgi协议,web框架都会按照wsgi协议为我们定制好application的基本功能,所以我们只需要关心最上层的应用程序的开发即可,有了web框架真是省了我们不少事,下面我们就来详细介绍一下django框架吧