【翻译】一步步开发一个Web服务器.Part 2.

2024-08-28 13:32

本文主要是介绍【翻译】一步步开发一个Web服务器.Part 2.,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文地址:Let’s Build A Web Server. Part 2.
译文地址:【翻译】一步步开发一个Web服务器.Part 2.
原文代码基于python2.x

本系列其他文章:
【翻译】一步步开发一个Web服务器.Part 1.
【翻译】一步步开发一个Web服务器.Part 2.
【翻译】一步步开发一个Web服务器.Part 3.

记得,我在Part 1 中留了个作业题:不改变服务器代码的情况下,怎样在你刚完成的服务器上运行一个Django、Flask、Pyramid应用,适应不同的web框架? 阅读完这篇文章你就能得到答案。

在过去,你选择一个python web框架往往受限于可用的web服务器的选择,反之亦然。如果你选择的框架和你的服务器恰好能在一起工作,这时候你就乐了:
这里写图片描述
你或许要面对(或者已经遇到了)这样一个问题:当你想要把一个框架和一个服务器结合在一起时,你发现它们并不能很好的结合在一起。
这里写图片描述
从根本上来讲,你不得不用那些能一起工作的却不是你想用的框架和服务器。

所以,在不改变web服务器和web框架的情况下,你要怎样才能确保你的web服务器能够适应不同的web框架?答案就是Python 服务器网关接口(Web Server Gateway Interface, 缩写为WSGI,读作wizgy)
这里写图片描述
WSGI 允许开发者在web框架和web服务器上做出不同选择。你可以根据你的需要搭配不同的web框架和web服务器了。可以使用Django、Flask或者Pyramid,举个栗子,Gunicorn或者Nginx/uWSGI又或者Waitress。它做到了真正的混合和搭配,同时支持服务器和框架,WSGI谢天谢地你来了。
这里写图片描述
所以,WSGI就是我一遍又一遍问你的那个问题的答案。你的web服务器必须实现一个WSGI的服务器接口,当下的Python Web框架都已经实现了WSGI的框架接口。如此一来,不用修改任何代码,你的web服务器可以使用接口适用于一个特定的web框架。

现在你已经知道了WSGI同时支持服务器和框架来满足你不同选择的需求,这也使得服务器和框架的开发者们收益,它们能专注于他们擅长的领域而不必涉足对方的领域。其他语言也有类似的接口,又举个栗子:Java有Servlet API、Ruby有Rack。

这些都挺不错的(你心里或许正嘀咕:别逼逼了,上代码!)。OK,这下面是一个最简单的WSGI服务器的实现:

# Tested with Python 2.7.9, Linux & Mac OS X
import socket
import StringIO
import sysclass WSGIServer(object):address_family = socket.AF_INETsocket_type = socket.SOCK_STREAMrequest_queue_size = 1def __init__(self, server_address):# Create a listening socketself.listen_socket = listen_socket = socket.socket(self.address_family,self.socket_type)# Allow to reuse the same addresslisten_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)# Bindlisten_socket.bind(server_address)# Activatelisten_socket.listen(self.request_queue_size)# Get server host name and porthost, port = self.listen_socket.getsockname()[:2]self.server_name = socket.getfqdn(host)self.server_port = port# Return headers set by Web framework/Web applicationself.headers_set = []def set_app(self, application):self.application = applicationdef serve_forever(self):listen_socket = self.listen_socketwhile True:# New client connectionself.client_connection, client_address = listen_socket.accept()# Handle one request and close the client connection. Then# loop over to wait for another client connectionself.handle_one_request()def handle_one_request(self):self.request_data = request_data = self.client_connection.recv(1024)# Print formatted request data a la 'curl -v'print(''.join('< {line}\n'.format(line=line)for line in request_data.splitlines()))self.parse_request(request_data)# Construct environment dictionary using request dataenv = self.get_environ()# It's time to call our application callable and get# back a result that will become HTTP response bodyresult = self.application(env, self.start_response)# Construct a response and send it back to the clientself.finish_response(result)def parse_request(self, text):request_line = text.splitlines()[0]request_line = request_line.rstrip('\r\n')# Break down the request line into components(self.request_method,  # GETself.path,            # /helloself.request_version  # HTTP/1.1) = request_line.split()def get_environ(self):env = {}# The following code snippet does not follow PEP8 conventions# but it's formatted the way it is for demonstration purposes# to emphasize the required variables and their values## Required WSGI variablesenv['wsgi.version']      = (1, 0)env['wsgi.url_scheme']   = 'http'env['wsgi.input']        = StringIO.StringIO(self.request_data)env['wsgi.errors']       = sys.stderrenv['wsgi.multithread']  = Falseenv['wsgi.multiprocess'] = Falseenv['wsgi.run_once']     = False# Required CGI variablesenv['REQUEST_METHOD']    = self.request_method    # GETenv['PATH_INFO']         = self.path              # /helloenv['SERVER_NAME']       = self.server_name       # localhostenv['SERVER_PORT']       = str(self.server_port)  # 8888return envdef start_response(self, status, response_headers, exc_info=None):# Add necessary server headersserver_headers = [('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),('Server', 'WSGIServer 0.2'),]self.headers_set = [status, response_headers + server_headers]# To adhere to WSGI specification the start_response must return# a 'write' callable. We simplicity's sake we'll ignore that detail# for now.# return self.finish_responsedef finish_response(self, result):try:status, response_headers = self.headers_setresponse = 'HTTP/1.1 {status}\r\n'.format(status=status)for header in response_headers:response += '{0}: {1}\r\n'.format(*header)response += '\r\n'for data in result:response += data# Print formatted response data a la 'curl -v'print(''.join('> {line}\n'.format(line=line)for line in response.splitlines()))self.client_connection.sendall(response)finally:self.client_connection.close()SERVER_ADDRESS = (HOST, PORT) = '', 8888def make_server(server_address, application):server = WSGIServer(server_address)server.set_app(application)return serverif __name__ == '__main__':if len(sys.argv) < 2:sys.exit('Provide a WSGI application object as module:callable')app_path = sys.argv[1]module, application = app_path.split(':')module = __import__(module)application = getattr(module, application)httpd = make_server(SERVER_ADDRESS, application)print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))httpd.serve_forever()

你肯定在想,什么鬼?说好的简单实现呢?(译者:我也在想什么鬼!这么多!)但是它足够小到能让你理解却不会陷入到细节上。上面最简单的服务器也能做到很多,他可以运行最基本的Pyramid、Flask、Django或者其他的PythonWSGI框架。

还别不信,咱们来试试。看见上面的代码了吗?直接拷下来或者从Github上down下来,保存为webserver2.py文件。如果不加参数直接运行会报错并退出。

$ python webserver2.py
Provide a WSGI application object as module:callable

它真想为你的web应用服务,却无从下手。想让这个服务器运行起来只需要安装python就行了,但是想要运行Pyramid、Flask、Django的应用,还得先安装这些框架。我推荐的方法是使用虚拟环境(virtualenv)。你只要跟着下面的步骤一步步来就OK,首先安装虚拟环境,然后安装这三个框架。(译者认为可以不用虚拟环境,直接安装三个框架)

$ [sudo] pip install virtualenv
$ mkdir ~/envs
$ virtualenv ~/envs/lsbaws/
$ cd ~/envs/lsbaws/
$ ls
bin  include  lib
$ source bin/activate
(lsbaws) $ pip install pyramid
(lsbaws) $ pip install flask
(lsbaws) $ pip install django

这时候要创建一个web应用。这里以Pyramid为例。同样的,保存下面的代码或者从Github直接down下来,命名为pyramidapp.py。跟你的webserver2.py保存到同一目录下:

from pyramid.config import Configurator
from pyramid.response import Responsedef hello_world(request):return Response('Hello world from Pyramid!\n',content_type='text/plain',)config = Configurator()
config.add_route('hello', '/hello')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()

现在你可以写一个Pyramid程序并且用你刚才的web服务器运行了。

(lsbaws) $ python webserver2.py pyramidapp:app
WSGIServer: Serving HTTP on port 8888 ...

你只是告诉你的服务器从python的pyramidapp模块中加载这个‘app’。你的服务器已经准备好接收请求并且转发给你的Pyramid程序。现在你的web应用只处理/hello这一个路由路径。在你的浏览器中输入http://localhost:8888/hello 按下回车,你会发现,神奇的操作又一次发生了。
这里写图片描述
同样你也可以使用curl命令在命令行中测试你的服务器。

$ curl -v http://localhost:8888/hello
...

查看一下使用curl命令,你的服务器输出了什么。

现在我们使用Flask创建一个同样web应用。创建、启动、运行、测试都与上面相同。

from flask import Flask
from flask import Response
flask_app = Flask('flaskapp')@flask_app.route('/hello')
def hello_world():return Response('Hello world from Flask!\n',mimetype='text/plain')app = flask_app.wsgi_app

保存上面的代码或者直接从Github上下载,保存为flaskapp.py 也是放在同一文件夹。在命令行中输入:

(lsbaws) $ python webserver2.py flaskapp:app
WSGIServer: Serving HTTP on port 8888 ...

再次在浏览器中输入 http://localhost:8888/hello 回车,神奇的操作又来了!(大家别嫌烦,后面Django还要来一遍…)
这里写图片描述
同样使用curl命令在命令行中测试web服务器,Flask应用回产生。。。

$ curl -v http://localhost:8888/hello
...

这个服务器能处理Django应用吗?(真的又要来一遍。。。)来试试!但是这次有点儿复杂,我建议从Github直接把整个项目down下来,只使用djangoapp.py这部分。这是工程的源代码,只需要将‘helloworld’这个Django工程添加到当前路径,并且将工程的WSGI应用导入。

import sys
sys.path.insert(0, './helloworld')
from helloworld import wsgiapp = wsgi.application

保存上面的代码,命名为djangoapp.py然后用你的web服务器运行这个程序。

(lsbaws) $ python webserver2.py djangoapp:app
WSGIServer: Serving HTTP on port 8888 ...

然后再浏览器中输入http://localhost:8888/hello
这里写图片描述
前面你已经做过两次了,这次你也可以在命令行中测试,以进一步确认你的Django程序能正确处理的请求:

$ curl -v http://localhost:8888/hello
...

你确定你试过了吗?你确定你的服务器支持这几种框架吗?如果没有,请认真做一遍。读过文章和代码固然重要,但这一系列文章是动手开发自己的web服务器,这意味着你必须自己亲自动手写一遍。撸起袖子加油干~你必须自己亲自动手把代码敲一遍以确保能按照预期运行。

相信你已经见识到WSGI的强大了。它能让你随意搭配服务器和web框架。WSGI在Python Web服务器和Python Web框架之间提供了一个极简的接口。不管是对于框架还是服务器,WSGI都非常容易实现。下面的代码片段展示了服务器和框架接口的代码实现。

def run_application(application):"""Server code."""# This is where an application/framework stores# an HTTP status and HTTP response headers for the server# to transmit to the clientheaders_set = []# Environment dictionary with WSGI/CGI variablesenviron = {}def start_response(status, response_headers, exc_info=None):headers_set[:] = [status, response_headers]# Server invokes the ‘application' callable and gets back the# response bodyresult = application(environ, start_response)# Server builds an HTTP response and transmits it to the clientdef app(environ, start_response):"""A barebones WSGI app."""start_response('200 OK', [('Content-Type', 'text/plain')])return ['Hello world!']run_application(app)

接下来讲讲它的工作原理:

  1. 框架提供一个可供调用的‘application’对象。(WSGI规范里并没有规定这个application要怎么实现)

2.对于每个来自HTTP客户端的请求,服务器都会调用这个可调用的‘application’对象。服务器会向可调用对象‘application’传递一个名为‘environ’字典类型的参数,字典里包含WSGI/SGI的一些变量和一个‘start_response’可调用对象。

3.框架/应用程序会产生一个HTPP状态和HTTP响应报头,传递给可调用对象‘start_response’以供服务器储存。框架/应用程序也会返回一个响应的主体。

4.服务器将状态码、响应头部、响应主体组合成HTTP响应,将它传递给客户端(这一部没有在规范里写出来,但为了下一步的逻辑能清晰点儿,我才把它加上)。

下面是接口的一个可视化表示:
这里写图片描述
目前为止,你已经见识了Pyramid、Flask、Django的Web应用,服务器的WSGI接口实现,没用任何框架的WSGI应用的代码片段。

还有一件事,当你使用任何一种框架写一个web应用时,你的思想在一个更高的逻辑,不会与WSGI接口直接接触,我知道你会好奇框架的WSGI接口是什么样的,因为你在读这篇文章啊。所以我早就准备好了一个不用各种框架就能在服务器上运行的WSGI Web应用/Web框架。

def app(environ, start_response):"""A barebones WSGI application.This is a starting point for your own Web framework :)"""status = '200 OK'response_headers = [('Content-Type', 'text/plain')]start_response(status, response_headers)return ['Hello world from a simple WSGI application!\n']

保存上面的代码或者直接从Github上down下来,保存为wsgiapp.py文件,与你的服务器保存在同一路径。这样就可以运行了:

(lsbaws) $ python webserver2.py wsgiapp:app
WSGIServer: Serving HTTP on port 8888 ...

在浏览器地址栏中输入http://localhost:8888/hello,你就能看见如下结果:
这里写图片描述
学习开发web服务器的时候,你还写了个最简单的WSGI Web框架,惊不惊喜?意不意外?

接下来,我们转向服务器传递给客户端的HTPP报文。下面是你使用HTTP客户端调用Pyramid应用时,服务器产生的HTTP响应报文:
这里写图片描述
这个响应与你再Part1中见到的有相似之处,也有一些新东西。再举个栗子,它有四个你以前没见过的HTTP头部:Content-Type、Content-Length、Date和Server。这些头部是一个web服务器产生的响应应该有的,尽管并没有严格要求。这些头部的目的是传递一些关于HTTP请求/响应的附加信息。

既然你已经对WSGI接口有了进一步的了解,下图还是那个HTTP响应,不过多了些内容,告诉你响应的每条信息是从哪里产生的。
这里写图片描述
目前为止我还没有提及‘environ’这个字典,简单来说‘environ’是个标准的Python字典类型,它必须包含WSGI规范里规定的一些特定的WSGI和SGI变量。服务器解析请求之后从HTTP请求中获取字典所需要的值。下图是字典内容:
这里写图片描述
web框架从字典中获取信息,根据特定的路由、请求方法等来决定使用哪个页面、从哪里读取请求主体,或者写入错误信息。

目前为止,写过了WSGI Web服务器,使用各种框架写过Web应用,写过最基本的web应用/web框架,真真是可怕的噩梦。一起来概括下你的WSGIweb服务器针对一个WSGI应用都要处理哪些请求。

1.服务器启动并且加载一个web应用/web框架提供的‘application’可调用对象。

2.服务器读取一个请求

3.服务器解析这个请求

4.服务器创建使用请求的数据创建一个‘environ’的字典

5.调用可调用对象‘application’,使用‘environ’字典和可调用对象‘start_response’作为参数,得到一个响应主体。

6.使用可调用对象‘application’返回的数据、状态码、可调用对象‘start_response’设置的响应头部。

7.最后服务器把这个HTTP响应发送回客户端。
这里写图片描述
上面就是所有的内容了,你已经有一个可以工作的WSGI服务器了,他可以运行与WSGI兼容的web框架例如Flask、Django、Pyramid或者你自己的WSGI框架。最棒的是不用修改任何代码就可以使用不同的web框架。哎哟,不错哟!

留一个课后思考题,“怎样保证你的服务器能在同一时间处理多个请求?”

敬请关注,我会在Part 3中讲解。

译者注:
文章中的图大家不要只看看热闹就过去了,作者在图中放了很多干货,大家看仔细点儿

这篇关于【翻译】一步步开发一个Web服务器.Part 2.的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1114900

相关文章

JSON Web Token在登陆中的使用过程

《JSONWebToken在登陆中的使用过程》:本文主要介绍JSONWebToken在登陆中的使用过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录JWT 介绍微服务架构中的 JWT 使用结合微服务网关的 JWT 验证1. 用户登录,生成 JWT2. 自定义过滤

一文教你如何将maven项目转成web项目

《一文教你如何将maven项目转成web项目》在软件开发过程中,有时我们需要将一个普通的Maven项目转换为Web项目,以便能够部署到Web容器中运行,本文将详细介绍如何通过简单的步骤完成这一转换过程... 目录准备工作步骤一:修改​​pom.XML​​1.1 添加​​packaging​​标签1.2 添加

利用Python开发Markdown表格结构转换为Excel工具

《利用Python开发Markdown表格结构转换为Excel工具》在数据管理和文档编写过程中,我们经常使用Markdown来记录表格数据,但它没有Excel使用方便,所以本文将使用Python编写一... 目录1.完整代码2. 项目概述3. 代码解析3.1 依赖库3.2 GUI 设计3.3 解析 Mark

CentOS 7部署主域名服务器 DNS的方法

《CentOS7部署主域名服务器DNS的方法》文章详细介绍了在CentOS7上部署主域名服务器DNS的步骤,包括安装BIND服务、配置DNS服务、添加域名区域、创建区域文件、配置反向解析、检查配置... 目录1. 安装 BIND 服务和工具2.  配置 BIND 服务3 . 添加你的域名区域配置4.创建区域

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

Windows Server服务器上配置FileZilla后,FTP连接不上?

《WindowsServer服务器上配置FileZilla后,FTP连接不上?》WindowsServer服务器上配置FileZilla后,FTP连接错误和操作超时的问题,应该如何解决?首先,通过... 目录在Windohttp://www.chinasem.cnws防火墙开启的情况下,遇到的错误如下:无法与

基于Python开发批量提取Excel图片的小工具

《基于Python开发批量提取Excel图片的小工具》这篇文章主要为大家详细介绍了如何使用Python中的openpyxl库开发一个小工具,可以实现批量提取Excel图片,有需要的小伙伴可以参考一下... 目前有一个需求,就是批量读取当前目录下所有文件夹里的Excel文件,去获取出Excel文件中的图片,并

基于Python开发PDF转PNG的可视化工具

《基于Python开发PDF转PNG的可视化工具》在数字文档处理领域,PDF到图像格式的转换是常见需求,本文介绍如何利用Python的PyMuPDF库和Tkinter框架开发一个带图形界面的PDF转P... 目录一、引言二、功能特性三、技术架构1. 技术栈组成2. 系统架构javascript设计3.效果图

Windows server服务器使用blat命令行发送邮件

《Windowsserver服务器使用blat命令行发送邮件》在linux平台的命令行下可以使用mail命令来发送邮件,windows平台没有内置的命令,但可以使用开源的blat,其官方主页为ht... 目录下载blatBAT命令行示例备注总结在linux平台的命令行下可以使用mail命令来发送邮件,Win

基于Python开发PDF转Doc格式小程序

《基于Python开发PDF转Doc格式小程序》这篇文章主要为大家详细介绍了如何基于Python开发PDF转Doc格式小程序,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用python实现PDF转Doc格式小程序以下是一个使用Python实现PDF转DOC格式的GUI程序,采用T