10. HTTP服务器
10.0. 怎样使Python程序作为Serv来运行,并对HTTP-req进行res?
HTTP协议的广为流行,使许多现成的解决方案实现了可能需要的所有,主要的Serv模式。在使用HTTP时,几乎不太可能编写任何底层的代码。
标准库提供了一个内置的HTTP-Serv思想。可从命令行启动该Serv
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
这个Serv遵循了90年代,用于文件OS-Serv的陈旧的设计惯例。HTTP-req中的路径会被转换为,用于本地系统中进行文件搜索的路径。只支持获取当前工作目录及子目录下的文件。当要获取的是文件时,Serv可正常返回该文件。但当req获取的是一个目录时,Serv就会返回index.html文件的内容(如果存在)/动态生成该目录包含的文件列表。
在安装了Python的环境中,构建小型的Web-Serv时,需要在不同机器间,传输文件,但没有更详细的文件传输协议可供使用。部署自己的软件,对HTTP-req进行req,需要遵循哪些步骤?
10.1. WSGI
早期HTTP编程中,许多Py服务会被编写成简单的CGI脚本。每次收到req就会触发CGI脚本。Serv对HTTP-req进行分割,将相应参数,以环境变量的形式传入CGI脚本中。Py程序员可直接将这些输入参数和HTTP-req打印到STD-OUT,也可借助标准库的cgi模块来查看。但以上方式会为每个接收到的HTTP-req启动一个新的prc,大大影响了Serv的性能。
10.1.1. 各种语言实现自己的HTTP-Serv。Py在标准库中加入了http.server模块。该模块实现服务时,需要编写自己的子类继承BaseHTTPRequestHandler,并添加do_GET()和do_POST()方法。
10.1.2. 其他程序员想在支持返回动态页面的Web-Serv上,同时支持返回静态内容(图片和样式表),出现了mod_python,是一个Apache模块,允许成功注册的Py函数,提供自定义的Apache处理函数,且在自定义处理函数中,提供认证、日志、返回内容等功能。
mod_python的API是Apache独有的,使用Py编写的处理函数,接收一个特殊的Apache-request对象作为参数。在处理函数内部,可调用apache模块的特殊函数,来与Web-Serv进行交互。使用mod_python的应用程序与使用CGI/http.server编写的程序无相似之处。
10.1.3. 在Py中使用上述不同方式编写的HTTP应用程序,在设计与Web-Serv交互的API时,都采用了某种特定的机制。使用CGI编写的服务,至少需要重写一部分代码,才能应用http.server。无论是使用CGI,还是http.server编写的Serv,都需要进过修改,才能在Apache下运行。使得Py-web服务的可移植性很差。
为解决可移植性差的问题,Py社区在PEP 333中,提出了web-Serv网关接口(WSGI, Web Server Gateway Interface)
计算机科学中的任何问题,都可通过加上另一层间接的中间层来解决。
WSGI标准就是添加了一层中间层。通过这一中间层,Py编写的HTTP服务就能与任何Web-Serv进行交互了。
10.1.4. WSGI标准指定了一个调用惯例,所有主流的Web-Serv的实现,都遵循这一惯例,那么就能直接在Serv中应用底层服务及功能完整的Web框架,无需修改原来的代码。各大Web-Serv很快遵循WSGI进行了实现。WSGI已成为了使用Py进行HTTP操作的标准方法。
10.1.5. WSGI程序是可被调用的,且有两个输入参数。
10-1展示了一个例子,使用一个简单的Py函数来表示可调用的WSGI程序。(也可用其他可调用类型,如Py类/包含__call__()方法的类实例)
第一个参数是environ,用于接收一个字典,字典中提供的键值对是旧式的CGI环境变量集合的拓展。
第二个参数本身也是可被调用的,将其命名为start_response(),WSGI通过start_response()来声明res-head-mes。
被调用后,app可以开始生成byte-str(如果app本身是一个生成器),可返回一个可迭代对象。该对象可在迭代过程中,生成byte-str(可返回一个简单的Py列表)
# 10-1 以WSGI应用程序形式编写的简单HTTP服务 wsgi_env.py
from pprint import pformat
from wsgiref.simple_server import make_server
def app(environ, start_response):
headers = {'Content-Type': 'text/plain; charset=utf-8'}
start_response('200 OK', list(headers.items()))
yield 'Here is the WSGI environment:\r\n\r\n'.encode('utf-8')
yield pformat(environ).encode('utf-8')
if __name__ == '__main__':
httpd = make_server('', 8000, app)
host, port = httpd.socket.getsockname()
print('Serving on', host, 'port', port)
httpd.serve_forever()
当编写Serv程序时,复杂度大大提升,因为需要完全考虑标准中描述的许多注意点和边界情况。相关细节,阅读PEP3333提出的Py3版本的WSGI
10.1.6. WSGI出现后,WSGI中间件的思想开始流行。大多数Py使用WSGI的主要原因是,提供了程序/框架与监听HTTP-req的Web-Serv间的可插拔性。
10.2. 异步Serv与框架
有一种程序的WSGI尚未支持,支持协程/绿色thd的异步Serv
WSGI可调用对象的设计,是面向传统的多thd/多prc-Serv的,在需要进行I/O操作时,可调用对象会被阻塞。WSIG没有提供任何,能使可调用对象,将控制权交还给主Serv-prc的机制,来让主prc轮流调度不同的可调用对象。
10.2.1. 任何异步Serv框架,都需各自给出用于编写Web服务的惯例,一般都会负责解析收到的HTTP-req,有时也会提供URL分发和自动连接DB的功能。
处理异步问题的Py项目,必须在其各自的引擎上,提供一个HTTP-Web-Serv,同时必须自行指定一种调用惯例,通过这种调用惯例,将解析得到的req-mes传递给处理req的代码。与WSGI生态系统不同的是,无法单独选择异步HTTP-Serv和Web框架,两者可能来自同一个包。
10.2.2. Tornado引擎没有把重点放在支持协议的数量上,而是着重提升了HTTP-Serv的性能。Tornado支持的回调函数惯例,与Twisted不尽相同。Eventlet的绿色thd的异步性,是隐式提供的,没有在进行I/O操作时,将控制权显式交还给主thd。使我们可以编写类似普通WSGI的可调用对象,它们进行阻塞操作时,会隐式交还控制权。
10.2.3. Py3.4已经有asyncio引擎,提供一个统一的接口,使不同的异步协议框架,可直接应用不同的事件循环实现。
可能有助于统一底层事件循环的混乱,但asynico没有指定专用于HTTP-req和res的API,对想要编写异步HTTP服务的程序员来说,并不会立刻受其影响。
10.2.4. 如果准备使用asyncio、Tornado、Twisted这样的异步引擎,来编写HTTP服务,要精心选择HTTP-Serv和框架(用于解析req和构造res)的组合,不能将Serv和框架搞混。
10.3. 前向代理与反向代理
无论是前向代理,还是反向代理,HTTP代理就是一个HTTP-Serv,用于接收req,然后对接收到的req(至少部分req)进行转发。转发res时,agent会扮演Cli的角色,将转发的HTTP-req发送至真正的Serv,最后将从Serv接收到的res发回给最初的Cli。RFC 7290的2.3关于agent的介绍,了解HTTP的设计是如何支持agent的:https://tools.ietf.org/html/rfc7230#section-2.3
10.3.1. 早期关于Web的描述,所阐明的观点认为前向代理(forward proxies)会成为最常见的代理模式。
如,一家公司会为所有职员,通过web浏览器发出的req提供一个HTTP-agent,而不是直接将这些req发送到远程Serv上。如果早上有100个员工的浏览器,要req获取google的logo,就可直接将保存在缓存中的logo返回给员工。如果google提供的Expires和Conche-Control-head-mes允许,那么该公司就能大大节省带宽,员工也能体验到更快的网速
10.3.2. 随着作为用户隐私及身份保护最佳实践的TLS的出现,fwd-prx开始难以实施。对于无法读取的req,agent-Serv没有办法查看和缓存。
10.3.3. 反向代理(reverse proxy)恰恰相反,已经广泛应用于大型HTTP服务之中,rev-prx是Web服务的一部分,
对于HTTP-CLi不可见。当CLi认为连接到python.org时,实际上正在与一个rev-prx进行交互。真正运行python.org的核心Serv,提供了合适的Expires/Cache-Control-head-mes,那么agent-Serv就可直接将缓存中的许多资源(静态+动态页面)返回给Cli。只有在资源无法进行缓存/已经超过agent-Serv的缓存有效期时,才需将HTTP-req转发给核心Serv,rev-prx通常能处理服务运行过程中的大多数负载。
10.3.4. rev-prx-Serv必须进行TLS截止,且rev-prx-Serv上运行的服务,必须拥有原始Serv的证书和私钥。只有prx-Serv审核了HTTP-req的合法性,才能进行缓存/转发。
如果采用的是rev-prx(无论是Apache/nginx这样的前端Web-Serv,还是Varnish专用守护进程),像Expires和Cache-Control这样,与缓存相关的HTTP-head都变得异乎寻常的重要。不仅与终端user的浏览器相关,更是Serv架构各层间的重要通信信号
10.3.5. 对于觉得不应该缓存的数据(每秒都会更新的标题页面/事件log),rev-prx也能帮上忙,前提是能容忍显示的结果是至少1s前的脏数据。Cli获取资源通常也需花费0.ns的时间。
显示1s前的脏数据,真的会影响到user-experience?
以每秒会收到100条req的重要feed/事件log为例。将Cache-Control-head的max-age设置为1s,应用rev-prx后,可能会将Serv的负载减小到原来的0.01倍。rev-prx只需在每秒的开始去获取资源,然后将该资源的缓存版本,发送给req资源的所有其他Cli即可。
10.3.6. 要在agent-Serv后端设计,并部署大型HTTP服务,需要查阅RFC 7234及关于HTTP缓存设计的讨论。从中了解到特定于中间层缓存(Varnish)而不是终端用户的HTTP-Cli缓存的选项与设置,如proxy-revalidate和s-maxage。在设计服务时,应考虑使用这些选项与设置。
10.4. 4种架构
架构师可使用很多种复杂的机制,来将多个子模块,组合构建成一个HTTP服务。
在Py社区中,已形成4中最基本的设计模式。已经编写了用于生成动态内容的Py代码,且已选择了某个支持WSGI的API/框架,如何将HTTP服务,部署到线上?
10.4.0.1. 运行一个Py编写的Serv,Serv代码中可以直接调用WSGI接口。最流行的是Green Unicorn(Gunicorn)-Serv,也有其他可用于Pro-env的纯Py-Serv。
如,ChrryPy-Serv,Flup也仍然吸引不少user使用。(除非编写的服务负载很小/仅对内部开放,不要使用wsgiref原型Serv)使用异步Serv引擎,Serv和框架必须运行在同一prc中。
10.4.0.2. 配置mod——wsgi并运行Apache,在一个独立的WSGIDaemonProcess中运行Py代码,由mod_wsgi启动Daemon。这是一种复合型的方法,在同一个Serv中使用了两种不同的语言。
如果Cli-req的是静态资源,那么Apache的C引擎可直接返回该资源;对动态资源的req,则提交给mod_wsgi,由mod_wsgi调用Py解释器,运行Daemon中的程序代码。(WSGI不支持程序暂时放弃控制权,并于之后再完成操作,这种方法不使用于异步Web框架)
10.4.0.3. 在后端运行一个类似于Gunicorn的Py-HTTP-Serv(支持所选异步框架的任何Serv),然后再前端运行一个既能返回静态文件,又能对Py编写的动态资源服务,进行rev-prx的Web-Serv。
Apache和Nginx是适用于这一任务的两个流行的前端Serv。此外,编写的Py程序运行在多台后端Serv,Apache和nginx也可对Cli的req进行负载均衡。
10.4.0.4. 最前端运行一个纯粹的反向代理(Varnish),在该rev-prx后端,运行Apache/nginx,在最后端运行Py编写的HTTP-Serv。这是一个三层的架构。这些rev-prx可分布在不同的地理位置,就能将离Cli最近的rev-prx上的缓存资源,返回给发送req的Cli。
像Fastly这样的内容分发网络(content delivery networks)就将大量的Varnish-Serv部署在了世界各地的机房中,为Cli提供了完整并可立即使用的服务。能终止面向Cli的TLS证书,并将req转发给后端的Serv。
Cli-------HTTP-------->Gunicorn代码
Cli-------HTTP-------->Apache(mod_wsgi)-------->daemon代码 mod_wsgi启动守护进程
Cli-------HTTP-------->Apache/nginx----HTTP--->Gunicorn代码
Cli-------HTTP-------->Varnish proxy---HTTP--->Apache/nginx----HTTP----->Gunicorn代码
单独部署Py代码/将Py代码部署在反向HTTP代理后端的4中常用技术
这4个架构的选择,主要基于Cpy的3个运行时特征,节解释器占用内存大、解释器运行慢、全局解释器(GIL)禁止多个thd同时运行Py字节码。
GIL的限制,鼓励使用多个独立的Py-prc而非共享prc空间的多个Py-thd。但是解释器占用的内存大小,又带来了另一个问题:内存中只能载入一定数量的Py实例,限制了能同时运行的prc数量。
10.4.1. 在Apache下运行Python
使用旧式mod_python在Apache下运行Py-web-site时,非常有可能遇到上面描述的问题。一个典型的web-site收到的大多数req,都是对静态资源的请求。每个req,Py动态生成一个页面的req都伴随着大量对CSS、JS及图片的req。但mod_python在每个Apache工作thd中,都维护了一个Py解释器运行时实例,其中大多数都处于空闲状态。某个特定时刻,只有一个工作thd可运行Py,其余所有thd都使用Apache的C核心程序输出静态文件。
10.4.1.1. 如果将Py解释器保存在Web-Serv的独立prc中,上面的情形就可避免。维护Py解释器实例的Web-Serv工作prc也负责从磁盘中,取出静态内容,交给等待中的socket,准备发送给Cli。产生了两种不同的方法:
1)避免在每个Apache-thd中,都保存一个Py解释器的方法是,使用现代mod_wsgi模块,并激活“Daemon”特征。这种模式下,Apache工作thd/prc无需负责Py的载入与执行,只需动态连接到mod_wsgi即可。
2)与mod_python不同,mod_wsgi创建并管理了一个独立的Py工作prc-pool,负责实际调用WSGI程序。Cli的req也会被转发到这个prc-pool中。每个占用内存较大的Py解释器,耗费较长时间构建动态页面时,大量小型的Apache工作prc/thd也能快速向Cli返回静态文件。
10.4.2. 纯粹的Python HTTP 服务器的兴起
Py并没有运行在主Serv-prc中,Apache-prc会负责将HTTP-req序列化并转发到Py-prc。
产生一个疑问:为什么不直接使用HTTP?为什么不配置Apache的rev-prx功能,将动态req转发给运行服务的Gunicorn?
这样的话,需要启动并管理两个不同的Daemon:Apache和Gunicorn。
方法1)只需启动Apache一个daemon,由mod_msgi负责管理Py解释器。相应带来了很大的灵活性。
10.4.2.1 Apache和Gunicorn不需运行在同一台机器上。可在一台针对高并发req和无需文件读取做了优化的Serv上运行Apache,在另一台针对使用动态语言,在运行时,向后端DB发送req进行了优化的Serv上运行Gunicorn.
此时Apache已经不再是应用程序的容器,而只是带有rev-prx功能的静态文件Serv,可选择使用别的Web-Serv代替它。nginx也可作为文件服务器,并带有rev-prx功能。
10.4.2.2. mod_wsgi采用的反向代理是专有的,限制较大,不同的prc必须运行在同一台机器上,prc间的交互使用的是mod_wsgi所独有的内部协议。对于纯粹的Python-HTTP-Serv,使用的则是真正的HTTP协议,既可在同一台机器上运行Python和Web-Serv,也可根据需要,在不同机器上运行Py和Web-Serv
10.4.3. 反向代理的劣势
如果HTTP程序,只需支持Py代码生成的动态内容,不需处理任何对静态资源的req。这种情况下,Apache和nginx似乎没有用,DEV可能想无视Apache/nginx,直接使用Gunicorn/其他纯粹的Python-Web-Serv向Cli提供服务。
这种情况下,需要考虑rev-prx能提供的安全性。如果有人想停止所编写的Web服务,只需通过n个socket连接到服务中的n个工作prc,每个req中都先发送一些随机字节,然后暂停发送即可。此时,所有工作prc都被占用,等待接收完整的req。然而完整的req永远都不会到达。如果在服务的前端,使用了Apache/nginx,rev-prx就会负责将长时间没有完整到达的req(可能是恶意攻击/某些Cli运行在移动设备等带宽较低的场合)存储在缓冲区,直到接收到完整的req后,再将req转发给后端的服务。
10.4.3.1. 仅靠一个先收集完整req,再转发给后端服务的agent,是无法保证OS免受拒绝服务攻击的,但可以防止后端的Ajax在运行时,由于没有接收到Cli的完整数据而停止运行。此外,还能防止py服务接收一些畸形的输入,如超长的HTTP-headname/完全畸形的req。因为Apache/nginx会直接拒绝这些req,并返回4xx-err。这过程对后端的程序完全不可见。
以上框架中,常用的是其中3种:
1)默认情况下,使用nginx加上Gunicorn的架构,如果OS-MA要求,也会使用Apache
2)如果编写的服务是一个单纯的API,不涉及任何静态组件,也会试着直接使用Gunicorn/直接在Gunicorn前端部署一个Varnish,来为动态资源提供一级缓存。
3)只有在涉及大型Web服务时,才会考虑完整的三层家口,即在Gunicorn中运行Py代码,在Gunicorn前端部署nginx/Apache,然后在最前端使用本地/分布式的Varnish集群。
10.4.3.2. 出现了一些Py运行环境,可将运行速度提升到机器级,如PyPy。
引出了重要的问题,既然Py代码可以和Apac运行得一样快,为什么不直接在Py代码中同时处理,对静态内容和动态内容的req?
Apache和nginx这些工业界的首选方案,提供了完善的文档,其机制也为大量OS-MA熟知并喜爱,没有动力驱使他们将服务全部移植到Py-Serv上去。
10.4.3.3. 上面模式的变种。
1)直接在Varnish后端运行Gunicorn。如不需要支持静态文件/DEV乐于使用Py从磁盘中获取静态文件,就是一个可选的方案。
2)打开nginx/Apache的rev-prx缓存功能,直接提供类似于Varnish的缓存功能,不使用三层架构。
有些网站在前端Serv和Py程序间,尝试使用了一些别的协议,如Flup和uwsgi中采用的协议。
10.5. 平台即服务(PaaS, Platform as a Service)
既然已经选择使用Py来构建网络服务,推荐阅读关于自动化部署、持续集成、高性能大规模服务的相关技术。
如何打包自己的应用程序,以便将应用部署到这些服务上?
10.5.1. 有了PaaS后,构建和运行HTTP服务过程中的许多麻烦事自动消失了,至少不用DEV自己担心,PaaS提供商会解决这些问题。
不需自己去租赁Serv、提供存储设备和IP地址、配置管理和重启Serv所需的root权限、安装正确版本的Python。Serv重启/断电后,也不需使用系统脚本,将应用程序复制到所有Serv上,然后自动运行服务。
都交给PaaS提供商来解决,
1)会负责安装/租赁成千上万的host和DB-Serv,
2)会根据客户规模,提供许多负载均衡器。
3)只需向PaaS提供商提供一个配置文件,就会自动完成。
4)PaaS会将域名信息加到它的DNS中,并将其指向某台负载均衡器,
5)在OS镜像内,安装正确版本的Py及所有Py依赖项,启动运行应用程序。此过程中,很容易提交新的源代码/在新版本应用程序,在user使用过程中,产生err时进行回滚。无需创建单独的/etc/init.d文件,无需重启某台特定的机器。
10.5.2. Heroku是目前PaaS领域最受DEV喜爱的项目,为Py应用作为其生态环境的一部分,提供了极佳的支持。Heroku对于缺乏专业经验/时间,来自己构建/管理负载均衡等工具的小公司很有意义。
10.5.3. Docker生态环境是Heroku的潜在竞争对手。Docker支持用户直接在本地的Linux机器上,运行Heroku风格的容器,大大简化了测试和调试的过程。使用Heroku,只修改一行配置,也需要经历漫长的提交和重新build过程。
即使使用了Heroku/Docker,仍然需要选择一个Web-Serv。
原因在于,尽管PaaS提供商提供了负载均衡、容器化、支持版本控制的配置、容器镜像缓存、DB-MA的功能,他们仍虚妄应用程序能提供标准的HTTP互操作性,即一个打开的端口,来供PaaS负载均衡器连接,并发送HTTP-req。
要为WSGI-app/框架提供一个监听网络port,还是需要一个Web-Serv的。
10.5.4. 许多DEV会选择使用Gunicorn/某个同类产品,这样就能在每个容器内,同时运行多个work-prc,从而使得单个容器能接收多个req。
由于服务有时需要好几秒才能生成某些资源,如果一开始req了这样的资源,且单个容器不能接收多个req的话,后续的req都要排队等待之前的req的完成。会造成一个问题。由于PaaS负载均衡器采用了轮询,如果所有容器都处于繁忙状态,那么负载均衡器对所有容器轮询一遍后,仍然无法找到合适的容器。
10.5.5. 大多数PaaS提供商不支持静态内容,除非在Py-app中,实现了对静态内容的更多支持/向容器中,加入了Apache/nginx。
尽管可以将静态资源与动态页面的路径,放在两个完全不同的URL内,许多架构师还是倾向于将两者放在同一名字空间内。
10.6. GET与POST模式和REST的问题
表述性状态转移(REST, Representational State Transfer),用来表示随着HTTP这样的超文本系统的所有特性,都得到充分利用,而出现的一种软件架构风格。
Roy Fielding博士的论文总结出了REST的概念:http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.html
"REST由4个对接口的约束条件定义",在论文的最后,简要罗列了这4个约束条件。
1)使用URI来标识资源
2)通过操作资源的表述形式来操作资源
3)消息具备自描述性
4)超媒体即应用状态引擎
服务设计者渴望赢得RESTful的美誉,但大多数服务都不是RESTful的设计,问题出在哪?
10.6.1. 第一个约束条件"使用URI来标识资源“排除了几乎所有传统形式的RPC。无论是JSON-RPC,还是XML-RPC,都没有在HTTP协议级别上,暴露资源实体。
假如一个user想获取一篇博文,然后更新博文的标题,再获取该博文,以检查更新是否成功。如果使用RPC方法调用来实现这一过程,暴露给HTTP的方法和路径为:
POST /rpc-endpoint/ -> 200 OK
POST /rpc-endpoint/ -> 200 OK
POST /rpc-endpoint/ -> 200 OK
每个req都可能会在POST-mes-body中的某处,有类似于post 1022的mes,用来指定Cli想要获取/编辑的特定资源。但,RPC使这一mes对HTTP协议是不可见的。想要符合REST的接口,会使用资源路径来指定要操作的博文,如将其命名为/post/1022
10.6.2. 第二个约束"通过操作资源的表现形式,来操作资源"。可防止设计者使用一些,特定于自己的服务的临时解决方案。
仍以更新博文标题为例,如果设计者使用了特定于服务的解决方案,那么每当Cli作者,想要了解更新方法时,都需要吃力地阅读服务指定的文档。如果使用了REST,修改一篇博文的标题时,就不需了解特定的东西了。
因为,一篇博文的表现形式可能是HTML、JSON、XML/其他格式,只能针对这些标书形式进行读写操作。要更新一篇博文的标题,Cli只需获取该博文当前的表述形式,然后修改标题,最后将新的表述形式提交给服务即可。
GET /post/1022/ -> 200 OK
PUT /post/1022/ -> 200 OK
GET /post/1022/ -> 200 OK
获取/更新资源,都至少要与Serv进行一次往返,对许多设计者来说是一个痛点。他们会有很强的意愿,在实际设计时,不去完全遵循REST的架构风格。
遵循REST的风格,就能发挥出它的优势。此时对资源的读写操作是对称的,且能将有意义的语义,暴露给HTTP协议。
使用REST后,HTTP协议就能确定哪些req是read-req、哪些是writer-req。
如果GET的res中,包含了正确的HTTP-head-mes,即使程序间没有通过浏览器来交互,也可使用缓存及条件请求(condition-req)。
10.6.3. 第三个约束"消息具备自描述性"提出的原因在于,表示进行显示缓存的HTTP-head,这些head-mes对mes提供了文档描述。
如,编写Cli的程序员,不需查询API文档,就能获悉/post/1022/的格式是JSOn,只有在使用了con-req确保缓存尚未过期时,才能使用缓存,/post/?q=news这样的req可以在上次获取同一资源60s内,直接获取缓存中的副本。
Serv返回的每个HTTP-res中,都会重新声明这些head-mes
10.6.4. 如果一个服务,满足了前3个约束条件,那么它对于HTTP协议来说,就是完全透明的了,而代理、缓存及Cli也能充分利用REST-req的语义,因此也就不需提前获取关于该服务的先验知识。满足最后一个约束条件的服务少得多。“超媒体即应用程序状态引擎(Hypermedia as the engine of application state-HATEOAS)这一表述颇具争议,“REST API必须由超文本驱动”博文指出,很多所谓的REST-API没有满足最后一个约束条件。
此外,该服务可以是为user设计的,用来返回包含form及JS的HTML页面;
也可以是为机器设计的,用来返回指向JSON/XML格式文件的简介的URL
这篇博文中,将HATEOAS这一约束条件,分为了至少6个独立的要点,最后一点是最为根本的。博客开篇提到:“除了原始的URI和适用于目标user的标准多媒体类型外,REST-API不应该依赖于任何先验知识"
对于这一约束,几乎所有熟悉的HTTP-API都无法满足,无论是Google还是GitHub提供的API,它们的文档一开始,基本上都会先讨论一下支持的资源类型,如“每篇博文的URL都是用类似/post/1022/的形式,为每篇博文指定一个独有的IP”
对于这种类型的API,都不能称之为真正的符合REST条件的API。它们都在文档中,包含了一些特殊的规则,Cli并不能仅通过超链接,就获取到正确的资源。
10.6.5. 一个彻底的符合REST条件的API,应该只有一个入口。Serv返回的媒体信息中,可能会包含一系列的form,Cli可以向其中某个form提交博客文章的ID,来获取要访问该文章所需的URL。
服务本身会负责将“IP为1022的博客文章”的概念和与之对应的特定路径,动态链接起来,而不是通过人类可读的文档,由Cli手动建立两者之间的链接。
10.6.6. “超媒体”作为“超文本”的泛化,对于旨在长期运行的服务来说,是一个非常重要的约束条件。如果想要支持好几代HTTP-CLi-user,且在第一代user已经离开不再使用服务时,使得后续user仍然能轻松寻找到需要的数据,那么这一约束条件是很重要的。
但是,因为只需要满足前3个约束条件,就能利用HTTP的大多数优势了(无状态、冗余性、缓存加速),所以没有出现太多尝试满足完整的REST约束的的服务。
10.7. 不使用Web框架编写WSGI可调用对象
很少需要自己编写底层的socket代码来实现HTTP协议,许多协议的细节,都可以交给Web-Serv来处理。如果选择使用Web框架,也可处理协议的细节。
那么Web-Serv和Web框架间,有什么区别?
10.7.1. Web-Serv负责建立、管理,监听socket,运行accept()来接收新连接,解析所有收到的HTTP-req。
1)Web-Serv甚至不需要调用应用程序代码,就能处理连接的Cli不发送完整请求,和Cli-req无法解析为HTTP-req的情况。
2)有些Web-Serv也会设置超时参数,关闭空闲的Cli-socket,并拒绝路径/head-mes超长的req。
3)Web-Serv只会将符合规则的完整req,传递给Web框架/app代码。
这一过程通过调用在Web-Serv注册的WSGI可调用对象实现。然后,Web-Serv通常会生成类似下面的HTTP响应码:
10.7.1.1. 400 Bad Request: HTTP-req不符合规则/超出了指定的大小限制
10.7.1.2. 500 Server Error: WSGI可调用对象没有成功运行,而是抛出了一个异常
成功接收并解析了HTTP-req后,有两种方法可以构建WSGI可调用对象,供Web-Serv调用。
1)直接自己创建可调用对象;
2)使用提供了WSGI可调用对象的Web框架,然后将自己的代码,嵌入到Web框架中
两者有何区别?
Web框架的基本任务是:负责URL分发。HTTP-req中的方法、hostname和路径,构成了一个坐标空间,每个req都是这个空间中的一个坐标点。
我们编写的服务可能会运行在一些hostname下,但不可能运行与所有hostname下。
对于方法也是一样,服务可能会支持GET/POST-req,但HTTP-req可以指定任何方法(甚至虚构的方法)。
对于许多req路径,Serv都能生成有效的res,但是对于另一些req路径,Serv则可能无法生成有效的res。
可在Web框架中,声明支持的路径和方法,对于不支持的HTTP-req,Web框架将自动生成,并返回类似下面的状态码:
10.7.1.3. 404 Not Found
10.7.1.4. 405 Method Not Allowed
10.7.1.5. 501 Not Implemented
如果不使用Web框架,应如何编写代码?如何在自己的代码中,直接提供WSGI接口,并进行URL分发呢?
10.7.2. 要构建这样的app,有两种方法:
第一种方法:阅读WSGI的具体说明,了解环境变量字典中的属性;
第二种方法:使用WebOb和Werkzeug工具集提供的包装函数,这两个工具互为竞争产品,可从Py包索引中获取这些工具。
10-2给出了,在原始WSGI环境中,编写app的冗长的代码风格
# 10-2 用于返回当前时间的原始WSGI可调用对象 timeapp_raw.py
import time
def app(environ, start_response):
host = environ.get('HTTP_HOST', '127.0.0.1')
path = environ.get('PATH_INFO', '/')
if ':' in host:
host, port = host.split(':', 1)
if '?' in path:
path, query = path.split('?', 1)
headers = [('Content-Type', 'text/plain; charset=utf-8')]
if environ['REQUEST_METHOD'] != 'GET':
start_response('501 Not Implemented', headers)
yield b'501 Not Implemented'
elif host != '127.0.0.1' or path != '/':
start_response('404 Not Found', headers)
yield b'404 Not Found'
else:
start_response('200 OK', headers)
yield time.ctime().encode('ascii')
如果没有使用Web框架,就需要在代码中,过滤掉与服务不符的hostname、路径、方法。这一过程无趣又麻烦。
如果服务只能对路径为/,hostname为127.0.0.1的GET-req做出res,一旦检测到了不符的hostnmae、路径、方法,就需要向Cli返回一个err。
需注意的是,在代码中将hostname与port进行了分离,这是为了处理Cli提供类似127.0.0.1:8000这样的Host-head的情况。此外,还必须基于?字符,对req路径进行分割,用于处理以/?name=value这样的查询str结尾的req(10-2中假设通常忽略多余的查询str,而没有返回404)
10.7.3. 接下来两个代码清单,展示了第三方库简化原始WSGI模式的方法,可使用标准的pip安装工具来安装WebOb和Werkzeug
$ pip install Webob
$ pip install Werkzeug
WebOb(Web Object)是一个轻量级的对象接口,封装了标准WSGI-dir,简化了对WSGI-dic-mes的存取。
10-3展示了使用WebOb,对代码10-2的程序进行简化的例子。
10-3 使用WebOb编写的可调用对象返回当前时间
import time, webob
def app(environ, start_response):
request = webob.Request(environ)
if environ['REQUEST_METHOD'] != 'GET':
response = webob.Response('501 Not Implemented', status=501)
elif request.domain != '127.0.0.1' or request.path != '/':
response = webob.Response('404 Not Found', status=404)
else:
response = webob.Response(time.ctime())
return response(environ, start_response)
10.7.3.1. WebOb库已经实现了10-2中需要自己实现的两个模式:
1)从可能包含port的Host-head中,单独分离出hostname
2)忽略路径结尾的查询str
WebOb也提供了一个Response对象,该对象已经设置了,所有关于内容类型,及编码的信息(默认情况下,将内容类型设置为纯文本),只需传入一个str作为res-body即可,其他所有内容都由WebOb自动处理。
WebOb有一个特性,在众多Py-HTTP-res对象的实现中,几乎是独一无二的。
WebOb的Response类提供了content_type和charset两个独立的属性,允许user将Content-Type-head-mes的两部分(text/plain;charset=utf-8)看作两个独立的值进行处理。
10.7.3.2. Werkzeug库作为Flask框架的基础,尽管在纯WSGI编程中,没有WebOb流行,但也广受user支持。Werkzeug的req和res对象,是不可变的,WSGI环境无法修改这两个对象。
10-4展示了Werkzeug与WebOb在简化操作时的不同之处
# 10-4 使用Werkzeug编写的WSGI可调用对象返回当前时间 timeapp_werkz.py
import time
from werkzeug.wrappers import Request, Response
@Request.application
def app(request):
host = request.host
if ':' in host:
host, port = host.split(':', 1)
if request.method != 'GET':
return Response('501 Not Implemented', status=501)
elif host != '127.0.0.1' or request.path !='/':
return Response('404 Not Found', status=404)
else:
return Response(time.ctime())
使用Werkzeug时,甚至不需知道,符合WSGI可调用对象规范的参数和返回值。使用了一个装饰器,简化了函数的调用方式。我们只负责接收一个Werkzeug的Request对象,作为唯一的参数,且只需返回一个Response对象即可,其他所有事情都由Werkzeug库来解决。
10.7.4. 相较于WebOb编写的代码,使用Werkzeug的唯一不足就是,需要自己从127.0.0.1:8000这样的str中,将hostname分割出来,没有提供方便的分割方法。
不过,除了这个不同外,两个库其实都做了同一件事,使得我们可在更高的抽象层次,来操作HTTP-req和res,而不需要直接去了解WSGI的规范。
10.7.5. 作为DEV,通常都不值得把时间花在底层操作上,可以选择使用Web框架。但在将收到的HTTP-req交给Web框架处理前,有时想对req进行一些变换。此时编写原始的WSGI程序就有用了。
此外,如果要编写自定义的rev-prx/其他用Py编写的纯HTTP服务,也可直接编写WSGI-app
10.7.6. 如果把范围扩大,原始WSGI可调用对象,在Py中的位置,类似于fwd-prx和rev-prx在整个HTTP生态OS中的位置。
相对于作为HTTP服务来根据,特定的hostname和路径返回资源而言,更适合处理req过滤、req规范化、req分发这样较为底层的任务。
10.8. 小结
10.8.1. Py中有一个内置的http.server模块,从命令行启动该Serv,可向Cli返回当前work-dir下的文件。尽管在紧急情况下及所请求的web-site直接存储在磁盘上时,使用起来很方便,但该模块现在很少用于新型HTTP服务的创建。
10.8.2. Py中,标准的同步HTTP,通常会用到WSGI标准。Serv负责解析收到的req,然后生成一个保存了所有mes的dic,app从dic中获取mes,返回HTTP-head及res-body(如果有)。使得能够自由选择Web-Serv,来与任意标准的Py-Web框架配合使用。
10.8.3. WSGI生态OS,不支持异步Web-Serv。WSGI可调用对象,不是完整意义上的协程,所有异步HTTP-Serv都需要,针对使用各自的Web框架所编写的服务,采用特定的处理方式。这种情况下,Serv和框架是绑定在一起的,通常不会与其他Serv/框架有更多的互操作性。
10.8.4. 使用Py提供HTTP服务,有4种流行的架构。
1)使用Gunicorn/其他纯Py-Serv(如CherryPy)直接运行单独的Serv
2)其他架构会选择通过mod_wsgi在Apache的控制下,运行Py。
由于rev-prx的概念,是所有种类Web服务的首选模式,
3)许多架构师发现,直接将Gunicorn/其他纯Py-Serv,部署在nginx/Apache后端更简单。
nginx/Apache和纯Py-Serv都作为独立的HTTP服务。当请求路径指向动态资源时,nginx/Apache会将req转发给后端的纯py-Serv
4)可在上面所有架构模式的前端,部署Varnish/其他反向代理,用于增加一层缓存。缓存实例可存在于req机器的同一机房中(甚至是同一机器上),通常会根据其地理位置的分布进行部署,使得各特定的HTTP客户群,都有距离较近的缓存实例