前面很多内容都是在介绍 Python 的知识点。这些知识点可以说涵盖了 Python 80% 的方面。在接着往下分享的话,那就是 Python 高阶的知识点。比如语言本身的自身能力(反射)、描述器、元编程等内容了。再接着就是 WEB 相关的内容了。
接下来会花很多篇章来介绍 WEB 相关的知识点。然后再介绍如何从零开始实现一个 WEB 框架。内容还是比较多的,请有需要的同学多多关注。
这里从零开始写一个框架出来,目的是为了更好地使用现有的 WEB 框架,了解现有 WEB 框架是如何工作的,这才是我们最终的目的。
WEB发展历程
最早的时候,WEB就是一些静态文件。由开发人员通过编辑器直接编辑生成静态的HTML页面,如果要修改WEB页面的内容或布局,就得编辑这些静态的HTML文件。早期的时候就是这么来玩的,是不是让人着实不爽?
由于静态WEB页面无法与用户进行交互(提交表单等操作)。为了解决这一问题,人们开发出了一种称为CGI(Common Gateway Interface)的技术,使用 C/C++
编写。
由于WEB应用的特点是频繁更新,用低级语言非常不适合高效地开发。而脚本语言(JSP、PHP)由于开发效率高,与HTML结合紧密,因此,迅速取代了CGI模式。JSP使用Java编写脚本,而PHP则是开源的脚本语言。
最早的动态页面是CGI实现的,CGI是怎么工作的?当请求某个地址的时候,http服务器会启动一个外部程序,并且把外部程序的输出返回给用户。CGI规范了http和外部程序的通信方式。
- 输入:通过环境变量
- 输出:通过标准输出
CGI的缺点:
- 并发模式是多进程
- 进程由http服务器管理
基于CGI以上的缺点,由此衍生出了FastCGI技术,把进程管理交给其他服务来处理。http服务器把http协议转化成fastcgi协议,通过socket发送给FastCGI守护进程。这样并发模型变得多种多样,进程管理由FastCGI实现,单一原则。
另一个分支就是由cgi发展的wsgi技术。接下来就看看wsgi。
Web框架的主要关键点:
- 路由
- Request解析
- Response封装
WSGI
Web服务器网关接口(WSGI)是Python Web应用程序框架和Web服务器之间标准接口的规范。
在Python Web应用程序的早期,由于没有统一的标准,将Web应用程序框架连接到Web服务器是不容易的。Python Web应用程序是用于工作的,具体是通过运用现有的CGI、FastCGI或mod_python(Apache)标准之一。这意味着在一个Web服务器上工作的应用程序可能无法在另一个Web服务器上工作。换言之,统一的应用程序与Web服务器之间的互操作性没有了。
WSGI通过在服务器和Web应用框架之间指定一个简单但统一的接口来解决这个问题,从而允许移植Web应用程序。
WSGI指定了两个方面:服务器(或网关)端,以及应用程序或框架端。WSGI请求得到了以下处理:
- 服务器端执行应用程序,给应用程序提供一个环境和一个回调函数。
- 应用程序处理请求,并使用所提供的回调函数返回响应给服务器。
流程如下:
wsgi两部分组成:容器,应用。也可以把容器称为网关或服务器。容器实现了协议转换,由http协议到wsgi协议的转换;应用用来处理业务逻辑。
容器和应用怎么通信呢?FastCGI通过socket;wsgi的容器和应用通过函数调用进行通信。什么情况下可以通过函数调用实现通信?同一进程内可以实现函数调用,wsgi的容器和应用是运行在同一进程之中。
常用的wsgi容器:gunicorn,uWSGI。
一个可工作的HelloWorld
说了这么多,还是看个简单的例子吧,看看wsgi是如何工作的。接下来实现一个helloworld的程序。
wsgi application应该是一个callable对象。函数是可调用的,如果一个类有__call__
方法,也是可调用的。
wsgi函数的书写有一定的要求,它的函数签名一般如下,比较固定:
def application(environ, start_response):
‘‘‘
# 函数传入的参数是有规定的:
# environ:environ变量是从服务器传递到应用程序的环境变量的字典,如请求内容等;
# 由公共网关接口(CGI)规范定义。WSGI在其规范中规定了一些环境变量受托者
# start_response:一个可调用对象,其提供一个从服务器端到应用程序端的回调,从而开启服务器端的
# 响应处理。它必须有两个位置参数。第一个应该是具有整型状态吗的状态字符串,第二个是一个(header_name, header_value)列表,其是描述HTTP头响应的元组。
‘‘‘
pass
上述函数传入的参数是有规定的:
environ
:environ
变量是从服务器传递到应用程序的环境变量的字典,如请求内容等;由公共网关接口(CGI)规范定义。WSGI在其规范中规定了一些环境变量受托者start_response
:一个可调用对象,其提供一个从服务器端到应用程序端的回调,从而开启服务器端的响应处理。它必须有两个位置参数。第一个应该是具有整型状态吗的状态字符串,第二个是一个(header_name, header_value
)列表,其是描述HTTP头响应的元组。
要了解更多的WSGI规范,可以看看PEP 3333提案。
开始写:
def application(environ, start_response):
body = ‘hello world‘
status = ‘200 OK‘
headers = [
(‘content-type‘, ‘text/plain‘),
(‘content-length‘, str(len(body)))
]
start_response(status, headers)
return [body.encode()]
# 以上代码只有应用,没有容器;
if __name__ == ‘__main__‘:
# 引入wsgi容器
from wsgiref.simple_server import make_server
server = make_server(‘0.0.0.0‘, 8000, application)
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
运行以上代码,并在浏览器上进行访问:
[[email protected]_113_230_centos 12-web]$ python wsgi01.py
183.192.25.121 - - [13/Oct/2018 18:48:08] "GET / HTTP/1.1" 200 11
183.192.25.121 - - [13/Oct/2018 18:48:09] "GET /favicon.ico HTTP/1.1" 200 11
183.192.25.121 - - [13/Oct/2018 18:48:09] "GET /favicon.ico HTTP/1.1" 200 11
也可以通过curl命令行工具进行访问:
curl -XPOST http://127.0.0.1:8000 -d ‘{"a": 1}‘
接下来使用gunicorn容器来运行上述代码:
# 首先安装gunicorn
pip install gunicorn
# 接着运行wsgi01.py的application应用
[[email protected]_113_230_centos 12-web]$ gunicorn -b 0.0.0.0 wsgi01:application
[2018-10-13 18:55:38 +0800] [18359] [INFO] Starting gunicorn 19.6.0
[2018-10-13 18:55:38 +0800] [18359] [INFO] Listening at: http://0.0.0.0:8000 (18359)
[2018-10-13 18:55:38 +0800] [18359] [INFO] Using worker: sync
[2018-10-13 18:55:38 +0800] [18397] [INFO] Booting worker with pid: 18397
通常在开发的时候,使用wsgiref就行了,如果在生产中,可以使用第三方的wsgi容器。
上面的代码中,如果想看environ中的内容,可以在上述代码中添加如下的代码:
for k, v in environ.items():
print(‘{} => {}‘.format(k, v))
# 完整的代码如下:
# coding: utf-8
def application(environ, start_response):
for k, v in environ.items():
print(‘{} => {}‘.format(k, v))
body = ‘hello world‘
status = ‘200 OK‘
headers = [
(‘content-type‘, ‘text/plain‘),
(‘content-length‘, str(len(body)))
]
start_response(status, headers)
return [body.encode()]
# 以上只有应用代码,没有容器
if __name__ == ‘__main__‘:
from wsgiref.simple_server import make_server
server = make_server(‘0.0.0.0‘, 8000, application)
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
如果再次运行代码,会有如下的输出:
wsgi.input => <gunicorn.http.body.Body object at 0x7fe20286c198>
HTTP_ACCEPT_ENCODING => gzip, deflate
HTTP_ACCEPT_LANGUAGE => zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
SERVER_PORT => 8000
RAW_URI => /
REMOTE_PORT => 16252
HTTP_USER_AGENT => Mozilla/5.0 (Windows NT 6.3; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0
wsgi.file_wrapper => <class ‘gunicorn.http.wsgi.FileWrapper‘>
REMOTE_ADDR => 183.192.25.121
wsgi.version => (1, 0)
wsgi.url_scheme => http
HTTP_HOST => 115.159.125.96:8000
wsgi.multithread => False
HTTP_ACCEPT => text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
wsgi.multiprocess => False
QUERY_STRING =>
SERVER_SOFTWARE => gunicorn/19.6.0
SERVER_PROTOCOL => HTTP/1.1
PATH_INFO => /
HTTP_CONNECTION => keep-alive
REQUEST_METHOD => GET
wsgi.run_once => False
wsgi.errors => <gunicorn.http.wsgi.WSGIErrorsWrapper object at 0x7fe20286c1d0>
SERVER_NAME => 0.0.0.0
HTTP_CACHE_CONTROL => max-age=0
SCRIPT_NAME =>
gunicorn.socket => <socket.socket fd=10, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=(‘10.105.113.230‘, 8000), raddr=(‘183.192.25.121‘, 16252)>
environ中有一个QUERY_STRING的key,我们可以打印该信息:
[[email protected]_113_230_centos 12-web]$ cat wsgi01.py
# coding: utf-8
def application(environ, start_response):
print(environ.get(‘QUERY_STRING‘))
body = ‘hello world‘
status = ‘200 OK‘
headers = [
(‘content-type‘, ‘text/plain‘),
(‘content-length‘, str(len(body)))
]
start_response(status, headers)
return [body.encode()]
# 以上只有应用代码,没有容器
if __name__ == ‘__main__‘:
from wsgiref.simple_server import make_server
server = make_server(‘0.0.0.0‘, 8000, application)
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
在浏览器的URL地址栏里输入:
http://115.159.125.96:8000?name=lavenliu&age=25
运行效果如下:
[[email protected]_113_230_centos 12-web]$ gunicorn -b 0.0.0.0 wsgi01:application
[2018-10-13 19:15:36 +0800] [20609] [INFO] Starting gunicorn 19.6.0
[2018-10-13 19:15:36 +0800] [20609] [INFO] Listening at: http://0.0.0.0:8000 (20609)
[2018-10-13 19:15:36 +0800] [20609] [INFO] Using worker: sync
[2018-10-13 19:15:36 +0800] [20647] [INFO] Booting worker with pid: 20647
name=lavenliu&age=25 # 获得的来自客户端的输入
上面的输出中,
name=lavenliu&age=25 # 获得的来自客户端的输入
接下来就是解析来自客户端的输入,解析这件事也不是很困难,不过有标准库来处理这个解析的工作,引入标准库:
from urllib.parse import parse_qs
from html import escape
parse_qs(‘name=lavenliu&age=25‘)
# 返回的value是列表,因为有可能key有同名的
执行结果如下:
{‘age‘: [‘25‘], ‘name‘: [‘lavenliu‘]}
接下来的代码如下:
[[email protected]_113_230_centos 12-web]$ cat wsgi02.py
# coding: utf-8
from urllib.parse import parse_qs
from html import escape
def application(environ, start_response):
params = parse_qs(environ.get(‘QUERY_STRING‘))
name = params.get(‘name‘, [‘anon‘])[0]
body = ‘hello {}‘.format(name) # 如果用户在浏览器里输入非法的东东时,可能会有问题
status = ‘200 OK‘
headers = [
(‘content-type‘, ‘text/html‘),
(‘content-length‘, str(len(body)))
]
start_response(status, headers)
return [body.encode()]
# 以上只有应用代码,没有容器
if __name__ == ‘__main__‘:
from wsgiref.simple_server import make_server
server = make_server(‘0.0.0.0‘, 8001, application)
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
如果用户在浏览器这样输入,就有可能有问题:
http://115.159.125.96:8001?name=<script type=‘text/javascript‘>alert(‘f**k it‘)</script>
# chrome浏览器则没有问题,它会检测到这个输入有问题;如果在IE6上可能就会执行上述代码。
Navigated to http://115.159.125.96:8001/?%3Cscript%20type=%27text/javascript%27%3Ealert(%27f**k%20it%27)%3C/script%3E
?name=<script type=‘text/javascript‘>alert(‘f**k it‘)</script>:1 The XSS Auditor refused to execute a script in ‘http://115.159.125.96:8001/?name=%3Cscript%20type=%27text/javascript%27%3Ealert(%27f**k%20it%27)%3C/script%3E‘ because its source code was found within the request. The auditor was enabled as the server sent neither an ‘X-XSS-Protection‘ nor ‘Content-Security-Policy‘ header.
Navigated to http://115.159.125.96:8001/?name=%3Cscript%20type=%27text/javascript%27%3Ealert(%27f**k%20it%27)%3C/script%3E
# 在火狐浏览器就能通过,不会报错
# IE11版本则没有这个问题
好了,今天就到这里。不知你看了这些内容有什么想法,随时欢迎留言进行讨论、一起学习。
后面会逐步介绍如何在此基础上进行封装,进而做出一个小小的框架。有任何问题欢迎留言交流。
原文地址:https://blog.51cto.com/lavenliu/2417870