当前位置: 首页 > 科技观察

如何搭建Web服务器(二)

时间:2023-03-20 19:32:00 科技观察

在***部分,我问了一个问题:“如何在刚搭建的Web服务器上适配Django、Flask或Pyramid应用程序,而不必为Web服务器如何进行更改以适应各种不同的Web框架?”我们可以从这篇文章中找到答案。曾经有一段时间,您对PythonWeb框架的选择限制了您对Web服务器的选择,反之亦然。如果框架和服务器被设计为协同工作,那么一切都很好:但您可能面临(或已经面临)试图匹配一对不能协同工作的框架和服务器的问题:基本上,您需要选择一起工作的框架和服务器,而不是你想使用的框架和服务器。那么,如何在不对Web服务器或框架的代码进行任何更改的情况下,确保您的Web服务器与多个不同的Web框架一起工作?这个问题的答案是Python网络服务器网关接口(WebServerGatewayInterface)(简称WSGI,读作“wizgy”)。WSGI允许开发者在互不干扰的情况下选择Web框架和Web服务器的类型。现在,您可以真正混合搭配Web服务器和框架,并选择最适合您的。例如,您可以使用Django、Flask或Pyramid,结合Gunicorn、Nginx/uWSGI或Waitress。感谢WSGI对服务器和框架的支持,我们可以真正选择使用哪种组合。所以,WSGI是我在***部分提出并在本文开头重复的问题的答案。你的web服务器必须实现了WSGI接口的server部分,现代的Pythonweb框架都实现了WSGI接口的framework部分,这样你就可以直接在web服务器中使用任何框架而不用改动任何服务器代码来兼容特定的web构架。现在,你知道Web服务器和Web框架都支持WSGI,这样你就可以选择最合适的一对来使用,这对服务器和框架开发人员来说也是有好处的,让他们只需要专注于自己擅长的事情。在不触及代码的另一部分的情况下进行开发。其他语言也有类似的接口,例如:Java有ServletAPI,Ruby有Rack。这些理论都很好,但我敢打赌你在说:“给我看代码!”好吧,我们来看看下面的WSGI小服务器实现:###UsingPython2.7.9,Linux和MacOSX下测试importsocketimportStringIOimportsysclassWSGIServer(object):address_family=socket.AF_INETsocket_type=socket.SOCK_STREAMrequest_queue_size=1def__init__(self,server_address):###创建监听socketself.listen_typesocket=listen_socket=socket.socket(self.family_address_)###允许重复使用相同地址listen_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)###绑定地址listen_socket。bind(server_address)###激活socketlisten_socket.listen(self.request_queue_size)###获取主机host的名称和端口,port=self.listen_socket.getsockname()[:2]self.server_name=socket.getfqdn(host)self.server_port=port###返回Web框架/应用响应头字段self.headers_set=[]defset_app(self,application):self.application=applicationdefserve_forever(self):listen_socket=self.listen_socketwhileTrue:###获取新的客户端连接self.client_connection,client_address=listen_socket.accept()###处理完一个请求后关闭连接,然后循环等待另一个连接建立self.handle_one_request()defhandle_one_request(self):self.request_data=request_data=self.client_connection.recv(1024)###输出'curl-v'风格的格式化请求数据print(''.join('<{line}\n'.format(line=line)forlineinrequest_data.splitlines()))self.parse_request(request_data)###根据请求数据构建环境变量字典env=self.get_environ()###这时候需要调用webapplication获取结果,###获取到的结果将成为HTTP响应体result=self.application(env,self.start_response)###构造一个response返回给客户端self.finish_response(result)defparse_request(self,text):request_line=text.splitlines()[0]request_line=request_line.rstrip('\r\n')###将请求行分成几个部分(self.request_method,#GETself.path,#/helloself.request_version#HTTP/1.1)=request_line.split()defget_environ(self):env={}###以下代码段不遵循PEP8规则,但是这样排版是为了通过emphas实现其显示izing###required变量及其值目的######WSGI需要变量env['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###CGIrequiredvariableenv['REQUEST_METHOD']=self.request_method#GETenv['PATH_INFO']=self.path#/helloenv['SERVER_NAME']=self.server_name#localhostenv['SERVER_PORT']=str(self.server_port)#8888returnenvdefstart_response(self,status,response_headers,exc_info=None):###Addnecessaryserverheaderfieldsserver_headers=[('Date','Tue,31Mar201512:54:48GMT'),('Server','WSGIServer0.2'),]self.headers_set=[status,response_headers+server_headers]###为了遵循WSGI协议,start_response函数必须返回一个'write'###callable对象(返回值.write可以作为函数调用)。为了简洁起见,我们###在这里忽略这个细节。###returnsself.finish_responsedeffinish_response(self,result):try:status,response_headers=self.headers_setresponse='HTTP/1.1{status}\r\n'.format(status=status)forheaderinresponse_headers:response+='{0}:{1}\r\n'.format(*header)response+='\r\n'fordatainresult:response+=data###以'curl-v'的格式输出格式化请求数据print(''.join('>{line}\n'.format(line=line)forlineinresponse.splitlines()))self.client_connection.sendall(response)finally:self.client_connection.close()SERVER_ADDRESS=(HOST,PORT)='',8888defmake_server(server_address,application):server=WSGIServer(server_address)server.set_app(application)returnserverif__name__=='__main__':iflen(sys.argv)<2:sys.exit('ProvideaWSGIapplicationobjectasmodule: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:ServingHTTPonport{port}...\n'.format(port=PORT))httpd.serve_forever()当然,这个这段代码比***部分中的服务器代码长很多,但仍然很短(不到150行),您无需深入研究细节就可以轻松理解它。上面的服务器代码做的更多——它可以用来运行一些用你最喜欢的框架编写的web应用程序,这些框架可以是Pyramid、Flask、Django或其他PythonWSGI框架。不相信?自己试试吧。将以上代码保存为webserver2.py,或者直接从Github上下载。如果您尝试在不带任何参数的情况下运行它,它会报错并退出。$pythonwebserver2.pyProvideaWSGIapplicationobjectasmodule:callable它想要做的实际上是为您的web应用程序提供服务,这是主要事件。为了运行这个服务器,你唯一需要的就是安装Python。但是,如果您希望运行Pyramid、Flask或Django应用程序,则需要先安装这些框架。因此,让我们安装所有这三个。我推荐的安装方式是通过virtualenv。按照以下步骤,您可以创建并激活一个虚拟环境,并在其中安装上述三个Web框架。现在,你需要创建一个Web应用程序。让我们先从金字塔开始。将下面的代码保存为pyramidapp.py,和刚才的webserver2.py放在同一个目录下,或者直接从Github上下载文件:现在,你可以使用你自己的运行Pyramid应用程序的Web服务器:(lsbaws)$pythonwebserver2.pypyramidapp:appWSGIServer:ServingHTTPonport8888...你刚刚告诉你的服务器加载Python模块pyramidapp中的可执行对象app。现在您的服务器可以接收请求并将它们转发到您的Pyramid应用程序。在浏览器中输入http://localhost:8888/hello,回车,看到结果:也可以使用命令行工具curl测试服务器:$curl-vhttp://localhost:8888/hello。..查看服务器和curl打印到标准输出流的内容。现在让我们试试Flask。操作步骤和上面一样.wsgi_app将以上代码保存为flaskapp.py,或者直接从Github下载,然后输入以下命令运行服务器:(lsbaws)$pythonwebserver2.pyflaskapp:appWSGIServer:ServingHTTPonport8888...现在输入http://localhost:8888/在你的浏览器hello中回车:同样,试试curl,你会看到服务器返回了一条由Flask应用程序生成的消息:$curl-vhttp://localhost:8888/hello...这个服务器可以吗处理Django应用程序??试一试!但是这个任务可能有点复杂,所以我建议你克隆整个存储库并使用Github存储库中的djangoapp.py来完成这个实验。这里的源码主要是将Django的helloworld项目(使用Django的django-admin.pystartproject命令创建)添加到当前Python路径,然后导入该项目的WSGI应用。(LCTT译注:除了这里展示的代码外,还需要配套的helloworld工程才能运行,代码可以在Github仓库中找到。)importsyssys.path.insert(0,'./helloworld')fromhelloworldimportwsgiapp=wsgi.application将上面的代码保存为djangoapp.py,然后用你的web服务器运行这个Django应用程序:(lsbaws)$pythonwebserver2.pydjangoapp:appWSGIServer:ServingHTTPonport8888...输入以下链接并按回车键:您也可以在命令行上测试这次-您之前应该已经做过两次-以确认Django应用程序处理您的请求:$curl-vhttp://localhost:8888/你好……你试过了吗?您确定该服务器适用于这三个框架吗?如果没有,请尝试一下。阅读固然重要,但这个系列的内容是重建,这意味着你需要自己做一些工作。继续尝试。别担心,我在等你。不开玩笑,您真的需要试一试,自己尝试每一步并确保它按预期工作。好了,你已经体会到了WSGI的强大之处:它可以将Web服务器和Web框架进行任意组合。WSGI在PythonWeb服务器和框架之间提供了一个微型接口。它非常简单,可以在服务器端和框架端轻松实现。以下代码片段显示了WSGI接口的服务器端和框架端实现:defrun_application(application):"""服务器端代码。"""###Web应用/框架在这里存放HTTP状态码和HTTP响应头,###服务器会将这些信息传递给客户端headers_set=[]###用于存放WSGI/CGI环境变量Dictionaryenviron={}defstart_response(status,response_headers,exc_info=None):headers_set[:]=[status,response_headers]###服务端唤醒可执行变量“application”,获取响应头result=application(environ,start_response)###服务器组装一个HTTP响应并将其发送给客户端...defapp(environ,start_response):"""AnemptyWSGIapplication"""start_response('200OK',[('Content-Type','text/plain')])return['Helloworld!']run_application(app)它是这样工作的:web框架提供了一个可调用的对象application(WSGI规范没有规定它是如何实现的)。每次web服务器收到一个messagefrom客户端的HTTP请求后,可调用对象application会被唤醒,会传递给对象一个environment变量字典environ包含WSGI/CGI变量,和一个可调用对象start_response。Web框架或应用程序生成HTTP状态码和HTTP响应头部分,然后将其传递给start_response函数,服务器将存储它。同时,Web框架或应用程序也会返回HTTP响应文本。服务端将状态码、响应头和响应文本组装成一个HTTP响应,然后发送给客户端(这一步在WSGI规范中是没有的,但按理来说,这一步应该包含在工作流中。所以为了阐明这个过程,我把它写了出来)这里是这个接口规范的图形表示:到目前为止,你已经看到了用Pyramid、Flask和Django编写的Web应用程序的代码,并且你已经看到了如何Web服务器在代码中实现了WSGI规范的另一半(服务器端)。您甚至看到了我们如何在不使用任何框架的情况下使用一段代码实现最小的WSGIWeb应用程序。事实上,当你使用上述框架编写一个web应用程序时,你只是在高层工作,而没有直接与WSGI打交道。但我知道你一定也对WSGI接口的框架部分感兴趣,因为你正在阅读这篇文章。所以,不用Pyramid、Flask或Django,让我们自己创建最简单的WSGIweb应用程序(或web框架),然后在你的服务器上运行它:defapp(environ,start_response):"""AThesimplestWSGIapplication.This是你自己的web框架的起点^_^"""status='200OK'response_headers=[('Content-Type','text/plain')]start_response(status,response_headers)return['HelloworldfromasimpleWSGIapplication!\n']另外,将上面的代码保存为wsgiapp.py或直接从Github下载文件,然后在Web服务器上运行应用程序,如下所示:(lsbaws)$pythonwebserver2.pywsgiapp:appWSGIServer:ServingHTTPonport8888...在下面输入地址您的浏览器,然后按Enter。以下是您应该看到的内容:在学习如何创建Web服务器的过程中,您刚刚自己编写了一个最小的WSGIWeb框架!惊人的!现在,让我们回到服务器传递给客户端的内容。这是使用HTTP客户端调用您的Pyramid应用程序时服务器生成的HTTP响应的内容:此响应与您在本系列第***部分中看到的HTTP响应有一些共同点,但它的内容更多一些.例如,它有四个您以前从未见过的HTTP标头:Content-Type、Content-Length、Date和Server。这些标头基本上出现在Web服务器返回的每个响应中。但是,它们都不是严格要求出现的。这些HTTP请求/响应标头字段的目的是它可以向您传递一些有关HTTP请求/响应的额外信息。既然你对WSGI接口有了更深入的了解,我来展示一下上面HTTP响应的各个部分的信息来源:上面的environ字典我没有解释,但是基本上这个字典肯定包含那些是WSGI和WSGI规范预先定义的CGI变量值。服务器在解析HTTP请求时,会从请求中获取这些变量的值。environ字典应该是这样的:web框架将使用上述字典中包含的信息来决定使用哪个视图来处理响应,从哪里读取请求文本,以及通过请求路径将其输出到哪里,字典中的请求操作等错误消息(如果有)。现在,您已经创建了自己的WSGIWeb服务器,并且使用不同的Web框架制作了多个Web应用程序。此外,您还在此过程中自己创建了一个简单的Web应用程序和框架。这个过程真的很累。现在让我们回顾一下你的WSGIweb服务器在服务请求时需要为WSGI应用做些什么:请求,然后服务器解析请求,然后服务器使用请求数据构建environ字典,然后它使用environ字典和可调用对象start_response作为参数调用应用程序并获取响应正文内容。然后,服务器会使用应用程序返回的响应体,以及start_response函数设置的状态码和响应头内容来构建一个HTTP响应。最后,服务器将HTTP响应发送回客户端。这基本上就是所有服务器必须做的。您现在拥有一个工作的WSGI服务器,可以为使用任何符合WSGI的Web框架(例如Django、Flask、Pyramid和您自己编写的框架)构建的Web应用程序提供服务。最好的部分是它可以在不更改任何服务器代码的情况下与多个不同的Web框架一起工作。真的不错。在结束之前,你可以想一想这个问题:“你如何让你的服务器同时处理多个请求?”请继续关注,我将在第3部分向您展示解决此问题的方法。干杯!顺便说一下,我正在写一本书,叫做《搭个 Web 服务器:从头开始》。这本书解释了如何从头开始编写一个基本的Web服务器,比本文更详细。通过订阅邮件列表,您可以获得本书的最新进展,以及发布日期。