每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。

    HTTP请求格式:

    GET:

    POST:

    1. Header1: Value1
    2. Header2: Value2
    3. Header3: Value3
    4. body data goes here...

    Header部分每行用\r\n换行,每行里键名和键值之间以: 分割,注意冒号后有个空格。

    当遇到\r\n\r\n时,Header部分结束,后面的数据全部是Body。

    HTTP响应格式:

    1. 200 OK
    2. Header1: Value1
    3. Header2: Value2
    4. Header3: Value3
    5. body data goes here...

    HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。

    Body数据是可以被压缩的,如果看到Content-Encoding,说明网站使用了压缩。最常见的压缩方式是gzip。

    了解了HTTP协议的格式后,我们可以理解一个Web应用的本质:
    1、浏览器发送HTTP请求给服务器;
    2、服务器接收请求后,生成HTML;
    3、服务器把生成的HTML作为HTTP响应的body返回给浏览器;
    4、浏览器接收到HTTP响应后,解析HTTP里body并显示。

    接受HTTP请求、解析HTTP请求、发送HTTP响应实现起来比较复杂,有专门的服务器软件来实现,例如Nginx,Apache。我们要做的就是专注于生成HTML文档。

    Python里也提供了一个比较底层的WSGI(Web Server Gateway Interface)接口来实现TCP连接、HTTP原始请求和响应格式。实现了该接口定义的内容,就可以实现类似Nginx、Apache等服务器的功能。

    WSGI接口定义要求Web开发者实现一个函数,就可以响应HTTP请求,示例:

    1. def application(environ, start_response):
    2. start_response('200 OK', [('Content-Type', 'text/html')])
    3. return [b'<h1>Hello, web!</h1>']

    这是一个简单的文本版本的Hello, web!

    上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:

    1. environ:一个包含所有HTTP请求信息的dict对象;
    2. start_response:一个发送HTTP响应的函数。

    有了WSGI,我们关心的就是如何从environ这个dict对象拿到HTTP请求信息,然后构造HTML,通过start_response()发送Header,最后返回Body。

    整个application()函数本身没有涉及到任何解析HTTP的部分,即底层代码不需要自己编写,只负责在更高层次上考虑如何响应请求就可以了。

    但是,application()函数由谁来调用呢?因为这里的参数environstart_response我们没法提供,返回的bytes也没法发给浏览器。

    有很多符合WSGI规范的服务器,Python提供了一个最简单的WSGI服务器,可以把我们的Web应用程序跑起来。这个模块叫wsgiref,它是用纯Python编写的WSGI服务器的参考实现。所谓“参考实现”是指该实现完全符合WSGI标准,但是不考虑任何运行效率,仅供开发和测试使用。

    有了wsgiref,我们可以非常快的实现一个简单的web服务器:

    1. # coding: utf-8
    2. from wsgiref.simple_server import make_server
    3. def application(environ, start_response):
    4. print(environ)
    5. start_response('200 OK', [('Content-Type', 'text/html')])
    6. return [b'<h1>Hello web!</h1>']
    7. print('HTTP server is running on http://127.0.0.1:9999')
    8. # 创建一个服务器,IP地址可以为空,端口是9999,处理函数是application:
    9. httpd = make_server('', 9999, application)
    10. httpd.serve_forever()

    运行后访问http://127.0.0.1:9999/,会看到:

    1. Hello web!

    通过Chrome浏览器的控制台,我们可以查看到浏览器请求和服务器响应信息:

    1. # 请求信息:
    2. GET / HTTP/1.1
    3. Host: 127.0.0.1:9999
    4. Connection: keep-alive
    5. Cache-Control: max-age=0
    6. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    7. Upgrade-Insecure-Requests: 1
    8. User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
    9. Accept-Encoding: gzip, deflate, sdch
    10. Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
    11. Cookie: _ga=GA1.1.948200530.1463673425
    12. # 响应信息:
    13. HTTP/1.0 200 OK
    14. Date: Sun, 12 Feb 2017 05:20:31 GMT
    15. Server: WSGIServer/0.2 CPython/3.4.3
    16. Content-Length: 19
    17. <h1>Hello web!</h1>

    我们再看终端的输出信息:

    如果我们打印environ参数信息,会看到如下值:

    1. {
    2. "SERVER_SOFTWARE": "WSGIServer/0.1 Python/2.7.5",
    3. "SCRIPT_NAME": "",
    4. "SERVER_PROTOCOL": "HTTP/1.1",
    5. "HOME": "/root",
    6. "LANG": "en_US.UTF-8",
    7. "SHELL": "/bin/bash",
    8. "SERVER_PORT": "9999",
    9. "HTTP_HOST": "dev.banyar.cn:9999",
    10. "HTTP_UPGRADE_INSECURE_REQUESTS": "1",
    11. "XDG_SESSION_ID": "64266",
    12. "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    13. "wsgi.version": "0",
    14. "wsgi.errors": "",
    15. "HOSTNAME": "localhost",
    16. "HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.8,en;q=0.6",
    17. "PATH_INFO": "/",
    18. "USER": "root",
    19. "QUERY_STRING": "",
    20. "PATH": "/usr/local/php/bin:/usr/local/php/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin",
    21. "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",
    22. "HTTP_CONNECTION": "keep-alive",
    23. "SERVER_NAME": "localhost",
    24. "REMOTE_ADDR": "192.168.0.101",
    25. "wsgi.url_scheme": "http",
    26. "CONTENT_LENGTH": "",
    27. "GATEWAY_INTERFACE": "CGI/1.1",
    28. "CONTENT_TYPE": "text/plain",
    29. "REMOTE_HOST": "",
    30. "HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch"
    31. }

    为显示方便,已精简部分信息。有了环境变量信息,我们可以对程序做些修改,可以动态显示内容:

    1. def application(environ, start_response):
    2. print(environ['PATH_INFO'])
    3. start_response('200 OK', [('Content-Type', 'text/html')])
    4. body = '<h1>Hello %s!</h1>' % (environ['PATH_INFO'][1:] or 'web' )
    5. return [body.encode('utf-8')]

    以上使用了environ里的PATH_INFO的值。我们在浏览器输入http://127.0.0.1:9999/python,浏览器会显示:

    1. Hello python!

    终端的输出信息:

    1. $ python user_wsgiref_server.py
    2. HTTP server is running on http://127.0.0.1:9999
    3. /python
    4. 127.0.0.1 - - [12/Feb/2017 13:54:57] "GET /python HTTP/1.1" 200 22
    5. /favicon.ico
    6. 127.0.0.1 - - [12/Feb/2017 13:54:58] "GET /favicon.ico HTTP/1.1" 200 27

    实际项目开发中,我们不可能使用swgiref来实现服务器,因为WSGI提供的接口虽然比HTTP接口高级了不少,但和Web App的处理逻辑比,还是比较低级。我们需要使用成熟的web框架。

    由于用Python开发一个Web框架十分容易,所以Python有上百个开源的Web框架。部分流行框架:

    1. Flask:轻量级Web应用框架;
    2. Django:全能型Web框架;
    3. web.py:一个小巧的Web框架;
    4. Bottle:和Flask类似的Web框架;
    5. TornadoFacebook的开源异步Web框架

    Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。

    安装非常简单:

    1. pip install flask

    控制台输出:

    1. Collecting flask
    2. Downloading Flask-0.12-py2.py3-none-any.whl (82kB)
    3. 100% |████████████████████████████████| 92kB 163kB/s
    4. Collecting itsdangerous>=0.21 (from flask)
    5. Downloading itsdangerous-0.24.tar.gz (46kB)
    6. 100% |████████████████████████████████| 51kB 365kB/s
    7. Collecting click>=2.0 (from flask)
    8. Downloading click-6.7-py2.py3-none-any.whl (71kB)
    9. 100% |████████████████████████████████| 71kB 349kB/s
    10. Collecting Jinja2>=2.4 (from flask)
    11. Downloading Jinja2-2.9.5-py2.py3-none-any.whl (340kB)
    12. 100% |████████████████████████████████| 348kB 342kB/s
    13. Collecting Werkzeug>=0.7 (from flask)
    14. Downloading Werkzeug-0.11.15-py2.py3-none-any.whl (307kB)
    15. 100% |████████████████████████████████| 317kB 194kB/s
    16. Downloading MarkupSafe-0.23.tar.gz
    17. Running setup.py bdist_wheel for itsdangerous ... done
    18. Successfully built itsdangerous MarkupSafe
    19. Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, flask
    20. 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:

    • GET /:首页,返回Home;
    • GET /login:登录页,显示登录表单;
    • POST /login:处理登录表单,显示登录结果。

    user_flask_app.py

    通过代码我们可以发现,Flask通过Python的装饰器在内部自动地把URL和函数给关联起来。

    注意代码里同一个URL/login分别有GETPOST两种请求,可以映射到两个处理函数中。

    使用模板

    Web框架让我们从编写底层WSGI接口拯救出来了,极大的提高了我们编写程序的效率。

    但代码里嵌套太多的html让整个代码易读性变差,使程序变得复杂。我们需要将后端代码逻辑与前端html分离出来。这就是传说中的MVC:Model-View-Controller,中文名“模型-视图-控制器”。

    Controller负责业务逻辑,比如检查用户名是否存在,取出用户信息等等;

    View负责显示逻辑,通过简单地替换一些变量,View最终输出的就是用户看到的HTML。

    ‘Model’负责数据的获取,如从数据库查询用户信息等。Model简单可以理解为数据。

    那么就是:Model获取数据,Controlle处理业务逻辑,View显示数据。

    现在,我们把上次直接输出字符串作为HTML的例子用MVC模式改写一下:

    1. # coding: utf-8
    2. from flask import Flask,request,render_template
    3. app = Flask(__name__)
    4. # 首页
    5. @app.route('/', methods=['GET', 'POST'])
    6. def home():
    7. return render_template('home.html')
    8. # 登录页
    9. @app.route('/login', methods=['get'])
    10. def login():
    11. return render_template('login.html', param = [])
    12. # 登录页处理
    13. @app.route('/login', methods=['post'])
    14. def do_login():
    15. param = request.form
    16. if(param['username'] == 'yjc' and param['password'] == 'yjc'):
    17. return render_template('welcome.html', username = param['username'])
    18. else:
    19. return render_template('login.html', msg = '用户名或密码不正确。', param = param)
    20. pass
    21. if __name__ == '__main__':
    22. app.run('', 5000)

    Flask通过render_template()函数来实现模板的渲染。和Web框架类似,Python的模板也有很多种。Flask默认支持的模板是jinja2

    模板页面:
    home.html

    1. <h1>Home</h1><p><a href="/login">去登录</a></p>

    login.html

    1. {% if msg %}
    2. <p style="color:red;">{{ msg }}</p>
    3. {% endif %}
    4. <form action="/login" method="post">
    5. <p>用户名:<input name="username" value="{{ param.username }}"></p>
    6. <p>密码:<input name="password" type="password"></p>
    7. <p><button type="submit">登录</button></p>
    8. </form>

    welcome.html

    1. <p>欢迎您, {{ username }} !</p>

    项目目录:

    1. user_flask_app
    2. |-- templates
    3. |-- home.html
    4. |-- login.html
    5. |-- welcome.html
    6. |-- user_flask_app.py

    render_template()函数第一个参数是模板名,默认是templates目录下。后面的参数是传给模板的变量。变量的值可以是数字、字符串、列表等等。

    在Jinja2模板中,我们用{{ name }}表示一个需要替换的变量。很多时候,还需要循环、条件判断等指令语句,在Jinja2中,用{% ... %}表示指令。

    比如循环输出页码:

    1. {% for i in page_list %}
    2. <a href="/page/{{ i }}">{{ i }}</a>
    3. {% endfor %}

    除了Jinja2,常见的模板还有:

    1. Mako:用<% ... %>和${xxx}的一个模板;