HTTP 协议是基于 TCP 和 IP 协议的。HTTP 协议是一种文本协议。
每个 HTTP 请求和响应都遵循相同的格式,一个 HTTP 包含 Header 和 Body 两部分,其中 Body 是可选的。
HTTP 请求格式:
GET:
- GET /path HTTP/1.1
- Header1: Value1
- Header2: Value2
- Header3: Value3
POST:
- POST /path HTTP/1.1
- Header1: Value1
- Header2: Value2
- Header3: Value3
- body data goes here...
Header 部分每行用
换行,每行里键名和键值之间以
- \r\n
分割,注意冒号后有个空格。
- :
当遇到
时,Header 部分结束,后面的数据全部是 Body。
- \r\n\r\n
HTTP 响应格式:
- 200 OK
- Header1: Value1
- Header2: Value2
- Header3: Value3
- body data goes here...
HTTP 响应如果包含 body,也是通过
来分隔的。
- \r\n\r\n
Body 数据是可以被压缩的,如果看到
,说明网站使用了压缩。最常见的压缩方式是 gzip。
- Content-Encoding
了解了 HTTP 协议的格式后,我们可以理解一个 Web 应用的本质:
1、浏览器发送 HTTP 请求给服务器;
2、服务器接收请求后,生成 HTML;
3、服务器把生成的 HTML 作为 HTTP 响应的 body 返回给浏览器;
4、浏览器接收到 HTTP 响应后,解析 HTTP 里 body 并显示。
接受 HTTP 请求、解析 HTTP 请求、发送 HTTP 响应实现起来比较复杂,有专门的服务器软件来实现,例如 Nginx,Apache。我们要做的就是专注于生成 HTML 文档。
Python 里也提供了一个比较底层的
(Web Server Gateway Interface)接口来实现 TCP 连接、HTTP 原始请求和响应格式。实现了该接口定义的内容,就可以实现类似 Nginx、Apache 等服务器的功能。
- WSGI
接口定义要求 Web 开发者实现一个函数,就可以响应 HTTP 请求,示例:
- WSGI
- def application(environ, start_response):
- start_response('200 OK', [('Content-Type', 'text/html')])
- return [b'<h1>Hello, web!</h1>']
这是一个简单的文本版本的
。
- Hello, web!
上面的
函数就是符合
- application()
标准的一个 HTTP 处理函数,它接收两个参数:
- WSGI
- environ:一个包含所有HTTP请求信息的dict对象;
- start_response:一个发送HTTP响应的函数。
有了 WSGI,我们关心的就是如何从
这个 dict 对象拿到 HTTP 请求信息,然后构造 HTML,通过
- environ
发送 Header,最后返回 Body。
- start_response()
整个
函数本身没有涉及到任何解析 HTTP 的部分,即底层代码不需要自己编写,只负责在更高层次上考虑如何响应请求就可以了。
- application()
但是,
函数由谁来调用呢?因为这里的参数
- application()
、
- environ
我们没法提供,返回的 bytes 也没法发给浏览器。
- start_response
函数必须由
- application()
服务器来调用。
- WSGI
有很多符合 WSGI 规范的服务器,Python 提供了一个最简单的
服务器,可以把我们的 Web 应用程序跑起来。这个模块叫
- WSGI
,它是用纯 Python 编写的
- wsgiref
服务器的参考实现。所谓 "参考实现" 是指该实现完全符合
- WSGI
标准,但是不考虑任何运行效率,仅供开发和测试使用。
- WSGI
有了
,我们可以非常快的实现一个简单的 web 服务器:
- wsgiref
- # coding: utf-8
- from wsgiref.simple_server import make_server
- def application(environ, start_response):
- print(environ)
- start_response('200 OK', [('Content-Type', 'text/html')])
- return [b'<h1>Hello web!</h1>']
- print('HTTP server is running on http://127.0.0.1:9999')
- # 创建一个服务器,IP地址可以为空,端口是9999,处理函数是application:
- httpd = make_server('', 9999, application)
- httpd.serve_forever()
运行后访问
,会看到:
- http://127.0.0.1:9999/
- Hello web!
通过 Chrome 浏览器的控制台,我们可以查看到浏览器请求和服务器响应信息:
- #请求信息:GET / HTTP / 1.1 Host: 127.0.0.1 : 9999 Connection: keep - alive Cache - Control: max - age = 0 Accept: text / html,
- application / xhtml + xml,
- application / xml;
- q = 0.9,
- image / webp,
- *
- /*;q=0.8
- Upgrade-Insecure-Requests: 1
- User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
- Accept-Encoding: gzip, deflate, sdch
- Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
- Cookie: _ga=GA1.1.948200530.1463673425
- # 响应信息:
- HTTP/1.0 200 OK
- Date: Sun, 12 Feb 2017 05:20:31 GMT
- Server: WSGIServer/0.2 CPython/3.4.3
- Content-Type: text/html
- Content-Length: 19
- <h1>Hello web!</h1>*/
我们再看终端的输出信息:
- $ python user_wsgiref_server.py
- HTTP server is running on http://127.0.0.1:9999
- 127.0.0.1 - - [12/Feb/2017 13:18:38] "GET / HTTP/1.1" 200 19
- 127.0.0.1 - - [12/Feb/2017 13:18:39] "GET /favicon.ico HTTP/1.1" 200 19
如果我们打印
参数信息,会看到如下值:
- environ
- {
- "SERVER_SOFTWARE": "WSGIServer/0.1 Python/2.7.5",
- "SCRIPT_NAME": "",
- "REQUEST_METHOD": "GET",
- "SERVER_PROTOCOL": "HTTP/1.1",
- "HOME": "/root",
- "LANG": "en_US.UTF-8",
- "SHELL": "/bin/bash",
- "SERVER_PORT": "9999",
- "HTTP_HOST": "dev.banyar.cn:9999",
- "HTTP_UPGRADE_INSECURE_REQUESTS": "1",
- "XDG_SESSION_ID": "64266",
- "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
- "wsgi.version": "0",
- "wsgi.errors": "",
- "HOSTNAME": "localhost",
- "HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.8,en;q=0.6",
- "PATH_INFO": "/",
- "USER": "root",
- "QUERY_STRING": "",
- "PATH": "/usr/local/php/bin:/usr/local/php/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin",
- "HTTP_USER_AGENT": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36",
- "HTTP_CONNECTION": "keep-alive",
- "SERVER_NAME": "localhost",
- "REMOTE_ADDR": "192.168.0.101",
- "wsgi.url_scheme": "http",
- "CONTENT_LENGTH": "",
- "GATEWAY_INTERFACE": "CGI/1.1",
- "CONTENT_TYPE": "text/plain",
- "REMOTE_HOST": "",
- "HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch"
- }
为显示方便,已精简部分信息。有了环境变量信息,我们可以对程序做些修改,可以动态显示内容:
- def application(environ, start_response):
- print(environ['PATH_INFO'])
- start_response('200 OK', [('Content-Type', 'text/html')])
- body = '<h1>Hello %s!</h1>' % (environ['PATH_INFO'][1:] or 'web' )
- return [body.encode('utf-8')]
以上使用了
里的
- environ
的值。我们在浏览器输入
- PATH_INFO
,浏览器会显示:
- http://127.0.0.1:9999/python
- Hello python!
终端的输出信息:
- $ python user_wsgiref_server.py
- HTTP server is running on http://127.0.0.1:9999
- /python
- 127.0.0.1 - - [12/Feb/2017 13:54:57] "GET /python HTTP/1.1" 200 22
- /favicon.ico
- 127.0.0.1 - - [12/Feb/2017 13:54:58] "GET /favicon.ico HTTP/1.1" 200 27
实际项目开发中,我们不可能使用
来实现服务器,因为 WSGI 提供的接口虽然比 HTTP 接口高级了不少,但和 Web App 的处理逻辑比,还是比较低级。我们需要使用成熟的 web 框架。
- swgiref
由于用 Python 开发一个 Web 框架十分容易,所以 Python 有上百个开源的 Web 框架。部分流行框架:
- Flask:轻量级Web应用框架;
- Django:全能型Web框架;
- web.py:一个小巧的Web框架;
- Bottle:和Flask类似的Web框架;
- Tornado:Facebook的开源异步Web框架
Flask 是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。
安装非常简单:
- pip install flask
控制台输出:
- Collecting flask
- Downloading Flask-0.12-py2.py3-none-any.whl (82kB)
- 100% |████████████████████████████████| 92kB 163kB/s
- Collecting itsdangerous>=0.21 (from flask)
- Downloading itsdangerous-0.24.tar.gz (46kB)
- 100% |████████████████████████████████| 51kB 365kB/s
- Collecting click>=2.0 (from flask)
- Downloading click-6.7-py2.py3-none-any.whl (71kB)
- 100% |████████████████████████████████| 71kB 349kB/s
- Collecting Jinja2>=2.4 (from flask)
- Downloading Jinja2-2.9.5-py2.py3-none-any.whl (340kB)
- 100% |████████████████████████████████| 348kB 342kB/s
- Collecting Werkzeug>=0.7 (from flask)
- Downloading Werkzeug-0.11.15-py2.py3-none-any.whl (307kB)
- 100% |████████████████████████████████| 317kB 194kB/s
- Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->flask)
- Downloading MarkupSafe-0.23.tar.gz
- Building wheels for collected packages: itsdangerous, MarkupSafe
- Running setup.py bdist_wheel for itsdangerous ... done
- Successfully built itsdangerous MarkupSafe
- Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, flask
- Successfully installed Jinja2-2.9.5 MarkupSafe-0.23 Werkzeug-0.11.15 click-6.7 flask-0.12 itsdangerous-0.24
安装完 flask 会同时安装依赖模块:
,
- itsdangerous
,
- click
,
- MarkupSafe
,
- Jinja2
。
- Werkzeug
现在我们来写个简单的登录功能,主要是三个页面:
字样;
- home
,有登录表单;
- /login
那么一共有 3 个 URL:
user_flask_app.py
- # coding: utf-8
- from flask import Flask
- from flask import request
- app = Flask(__name__)
- # 首页
- @app.route('/', methods=['GET', 'POST'])
- def home():
- return '<h1>Home</h1><p><a href="/login">去登录</a></p>'
- # 登录页
- @app.route('/login', methods=['get'])
- def login():
- return '''<form action="/login" method="post">
- <p>用户名:<input name="username"></p>
- <p>密码:<input name="password" type="password"></p>
- <p><button type="submit">登录</button></p>
- </form>'''
- # 登录页处理
- @app.route('/login', methods=['post'])
- def do_login():
- # 从request对象读取表单内容:
- param = request.form
- if(param['username'] == 'yjc' and param['password'] == 'yjc'):
- return '欢迎您 %s !' % param['username']
- else:
- return '用户名或密码不正确。'
- pass
- if __name__ == '__main__':
- # run()方法参数可以都为空,使用默认值
- app.run('', 5000)
我们可以打开: 看效果。实际的 Web App 应该拿到用户名和口令后,去数据库查询再比对,来判断用户是否能登录成功。
通过代码我们可以发现,Flask 通过 Python 的装饰器在内部自动地把 URL 和函数给关联起来。
注意代码里同一个
分别有
- URL/login
和
- GET
两种请求,可以映射到两个处理函数中。
- POST
Web 框架让我们从编写底层 WSGI 接口拯救出来了,极大的提高了我们编写程序的效率。
但代码里嵌套太多的 html 让整个代码易读性变差,使程序变得复杂。我们需要将后端代码逻辑与前端 html 分离出来。这就是传说中的
:Model-View-Controller,中文名 "模型 - 视图 - 控制器"。
- MVC
r 负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;
- Controlle
负责显示逻辑,通过简单地替换一些变量,View 最终输出的就是用户看到的 HTML。
- View
'Model'负责数据的获取,如从数据库查询用户信息等。Model 简单可以理解为数据。
那么就是:
获取数据,
- Model
处理业务逻辑,
- Controlle
显示数据。
- View
现在,我们把上次直接输出字符串作为 HTML 的例子用 MVC 模式改写一下:
- # coding: utf-8
- from flask import Flask,request,render_template
- app = Flask(__name__)
- # 首页
- @app.route('/', methods=['GET', 'POST'])
- def home():
- return render_template('home.html')
- # 登录页
- @app.route('/login', methods=['get'])
- def login():
- return render_template('login.html', param = [])
- # 登录页处理
- @app.route('/login', methods=['post'])
- def do_login():
- param = request.form
- if(param['username'] == 'yjc' and param['password'] == 'yjc'):
- return render_template('welcome.html', username = param['username'])
- else:
- return render_template('login.html', msg = '用户名或密码不正确。', param = param)
- pass
- if __name__ == '__main__':
- app.run('', 5000)
Flask 通过
函数来实现模板的渲染。和 Web 框架类似,Python 的模板也有很多种。Flask 默认支持的模板是
- render_template()
。
- jinja2
模板页面:
home.html
- <h1>
- Home
- </h1>
- <p>
- <a href="/login">
- 去登录
- </a>
- </p>
login.html
- { %
- if msg %
- } < p style = "color:red;" > {
- {
- msg
- }
- } < /p>
- {% endif %}
- <form action="/login " method="post ">
- <p>用户名:<input name="username " value=" {
- {
- param.username
- }
- }
- "></p>
- <p>密码:<input name="password " type="password "></p>
- <p><button type="submit ">登录</button></p>
- </form>"
welcome.html
- <p>
- 欢迎您, {{ username }} !
- </p>
项目目录:
- user_flask_app
- |-- templates
- |-- home.html
- |-- login.html
- |-- welcome.html
- |-- user_flask_app.py
函数第一个参数是模板名,默认是
- render_template()
目录下。后面的参数是传给模板的变量。变量的值可以是数字、字符串、列表等等。
- templates
在 Jinja2 模板中,我们用
表示一个需要替换的变量。很多时候,还需要循环、条件判断等指令语句,在 Jinja2 中,用
- {{ name }}
表示指令。
- {% ... %}
比如循环输出页码:
- { %
- for i in page_list %
- } < a href = "/page/{{ i }}" > {
- {
- i
- }
- } < /a>
- {% endfor %}/
除了
,常见的模板还有:
- Jinja2
- Mako:用 < %... % >和$ {
- xxx
- }的一个模板;Cheetah:也是用 < %... % >和$ {
- xxx
- }的一个模板;Django:Django是一站式框架,内置一个用 { % ... %
- }和 {
- {
- xxx
- }
- }的模板。
来源: