自己动手开发一个 Web 服务器(二)

简介:

自己动手开发一个 Web 服务器(二)



在《自己动手开发一个 Web 服务器(一)》中,我给大家留了一个问题:如何在不对服务器代码作任何修改的情况下,通过该服务器运行Djando应用、Flask应用和Pyramid应用,同时满足这些不同网络框架的要求?读完这篇文章,你就可以回答这个问题了。

以前,你选择的Python网络框架将会限制所能够使用的 Web 服务器,反之亦然。如果框架和服务器在设计时就是可以相互匹配的,那你就不会面临这个问题:

服务器与框架是否匹配

但是如果你试图将设计不相匹配的服务器与框架相结合,那么你肯定就会碰到下面这张图所展示的这个问题:

服务器与框架之间冲突

这就意味着,你基本上只能使用能够正常运行的服务器与框架组合,而不能选择你希望使用的服务器或框架。

那么,你怎样确保可以在不修改 Web 服务器代码或网络框架代码的前提下,使用自己选择的服务器,并且匹配多个不同的网络框架呢?为了解决这个问题,就出现了Python Web 服务器网关接口Web Server Gateway Interface,WSGI

WSGI接口

WSGI的出现,让开发者可以将网络框架与 Web 服务器的选择分隔开来,不再相互限制。现在,你可以真正地将不同的 Web 服务器与网络开发框架进行混合搭配,选择满足自己需求的组合。例如,你可以使用Gunicorn或Nginx/uWSGI或Waitress服务器来运行Django、Flask或Pyramid应用。正是由于服务器和框架均支持WSGI,才真正得以实现二者之间的自由混合搭配。

所以,WSGI就是我在上一篇文章中所留问题的答案。你的 Web 服务器必须实现一个服务器端的WSGI接口,而目前所有现代Python网络框架都已经实现了框架端的WSGI接口,这样开发者不需要修改服务器的代码,就可以支持某个网络框架。

Web 服务器和网络框架支持WSGI协议,不仅让应用开发者选择符合自己需求的组合,同时也有利于服务器和框架的开发者,因为他们可以将注意力集中在自己擅长的领域,而不是相互倾轧。其他编程语言也拥有类似的接口:例如Java的Servlet API和Ruby的Rack。

口说无凭,我猜你肯定在想:“无代码无真相!”既然如此,我就在这里给出一个非常简单的WSGI服务器实现:


  
  
  1. # Tested with Python 2.7.9, Linux & Mac OS X
  2. import socket
  3. import StringIO
  4. import sys
  5. class WSGIServer(object):
  6. address_family = socket.AF_INET
  7. socket_type = socket.SOCK_STREAM
  8. request_queue_size = 1
  9. def __init__(self, server_address):
  10. # Create a listening socket
  11. self.listen_socket = listen_socket = socket.socket(
  12. self.address_family,
  13. self.socket_type
  14. )
  15. # Allow to reuse the same address
  16. listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  17. # Bind
  18. listen_socket.bind(server_address)
  19. # Activate
  20. listen_socket.listen(self.request_queue_size)
  21. # Get server host name and port
  22. host, port = self.listen_socket.getsockname()[:2]
  23. self.server_name = socket.getfqdn(host)
  24. self.server_port = port
  25. # Return headers set by Web framework/Web application
  26. self.headers_set = []
  27. def set_app(self, application):
  28. self.application = application
  29. def serve_forever(self):
  30. listen_socket = self.listen_socket
  31. while True:
  32. # New client connection
  33. self.client_connection, client_address = listen_socket.accept()
  34. # Handle one request and close the client connection. Then
  35. # loop over to wait for another client connection
  36. self.handle_one_request()
  37. def handle_one_request(self):
  38. self.request_data = request_data = self.client_connection.recv(1024)
  39. # Print formatted request data a la 'curl -v'
  40. print(''.join(
  41. '< {line}\n'.format(line=line)
  42. for line in request_data.splitlines()
  43. ))
  44. self.parse_request(request_data)
  45. # Construct environment dictionary using request data
  46. env = self.get_environ()
  47. # It's time to call our application callable and get
  48. # back a result that will become HTTP response body
  49. result = self.application(env, self.start_response)
  50. # Construct a response and send it back to the client
  51. self.finish_response(result)
  52. def parse_request(self, text):
  53. request_line = text.splitlines()[0]
  54. request_line = request_line.rstrip('\r\n')
  55. # Break down the request line into components
  56. (self.request_method, # GET
  57. self.path, # /hello
  58. self.request_version # HTTP/1.1
  59. ) = request_line.split()
  60. def get_environ(self):
  61. env = {}
  62. # The following code snippet does not follow PEP8 conventions
  63. # but it's formatted the way it is for demonstration purposes
  64. # to emphasize the required variables and their values
  65. #
  66. # Required WSGI variables
  67. env['wsgi.version'] = (1, 0)
  68. env['wsgi.url_scheme'] = 'http'
  69. env['wsgi.input'] = StringIO.StringIO(self.request_data)
  70. env['wsgi.errors'] = sys.stderr
  71. env['wsgi.multithread'] = False
  72. env['wsgi.multiprocess'] = False
  73. env['wsgi.run_once'] = False
  74. # Required CGI variables
  75. env['REQUEST_METHOD'] = self.request_method # GET
  76. env['PATH_INFO'] = self.path # /hello
  77. env['SERVER_NAME'] = self.server_name # localhost
  78. env['SERVER_PORT'] = str(self.server_port) # 8888
  79. return env
  80. def start_response(self, status, response_headers, exc_info=None):
  81. # Add necessary server headers
  82. server_headers = [
  83. ('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
  84. ('Server', 'WSGIServer 0.2'),
  85. ]
  86. self.headers_set = [status, response_headers + server_headers]
  87. # To adhere to WSGI specification the start_response must return
  88. # a 'write' callable. We simplicity's sake we'll ignore that detail
  89. # for now.
  90. # return self.finish_response
  91. def finish_response(self, result):
  92. try:
  93. status, response_headers = self.headers_set
  94. response = 'HTTP/1.1 {status}\r\n'.format(status=status)
  95. for header in response_headers:
  96. response += '{0}: {1}\r\n'.format(*header)
  97. response += '\r\n'
  98. for data in result:
  99. response += data
  100. # Print formatted response data a la 'curl -v'
  101. print(''.join(
  102. '> {line}\n'.format(line=line)
  103. for line in response.splitlines()
  104. ))
  105. self.client_connection.sendall(response)
  106. finally:
  107. self.client_connection.close()
  108. SERVER_ADDRESS = (HOST, PORT) = '', 8888
  109. def make_server(server_address, application):
  110. server = WSGIServer(server_address)
  111. server.set_app(application)
  112. return server
  113. if __name__ == '__main__':
  114. if len(sys.argv) < 2:
  115. sys.exit('Provide a WSGI application object as module:callable')
  116. app_path = sys.argv[1]
  117. module, application = app_path.split(':')
  118. module = __import__(module)
  119. application = getattr(module, application)
  120. httpd = make_server(SERVER_ADDRESS, application)
  121. print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))
  122. httpd.serve_forever()

上面的代码比第一部分的服务器实现代码要长的多,但是这些代码实际也不算太长,只有不到150行,大家理解起来并不会太困难。上面这个服务器的功能也更多——它可以运行你使用自己喜欢的框架所写出来的网络应用,无论你选择Pyramid、Flask、Django或是其他支持WSGI协议的框架。

你不信?你可以自己测试一下,看看结果如何。将上述代码保存为webserver2.py,或者直接从我的Github仓库下载。如果你运行该文件时没有提供任何参数,那么程序就会报错并退出。


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

上述程序设计的目的,就是运行你开发的网络应用,但是你还需要满足一些它的要求。要运行服务器,你只需要安装Python即可。但是要运行使用Pyramid、Flask和Django等框架开发的网络应用,你还需要先安装这些框架。我们接下来安装这三种框架。我倾向于使用virtualenv安装。请按照下面的提示创建并激活一个虚拟环境,然后安装这三个网络框架。


  
  
  1. $ [sudo] pip install virtualenv
  2. $ mkdir ~/envs
  3. $ virtualenv ~/envs/lsbaws/
  4. $ cd ~/envs/lsbaws/
  5. $ ls
  6. bin include lib
  7. $ source bin/activate
  8. (lsbaws) $ pip install pyramid
  9. (lsbaws) $ pip install flask
  10. (lsbaws) $ pip install django

接下来,你需要创建一个网络应用。我们首先创建Pyramid应用。将下面的代码保存为pyramidapp.py文件,放至webserver2.py所在的文件夹中,或者直接从我的Github仓库下载该文件:


  
  
  1. from pyramid.config import Configurator
  2. from pyramid.response import Response
  3. def hello_world(request):
  4. return Response(
  5. 'Hello world from Pyramid!\n',
  6. content_type='text/plain',
  7. )
  8. config = Configurator()
  9. config.add_route('hello', '/hello')
  10. config.add_view(hello_world, route_name='hello')
  11. app = config.make_wsgi_app()

现在,你可以通过自己开发的 Web 服务器来启动上面的Pyramid应用。


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

在运行webserver2.py时,你告诉自己的服务器去加载pyramidapp模块中的app可调用对象(callable)。你的服务器现在可以接收HTTP请求,并将请求中转至你的Pyramid应用。应用目前只能处理一个路由(route):/hello。在浏览器的地址栏输入http://localhost:8888/hello,按下回车键,观察会出现什么情况:

Pyramid应用运行情况

你还可以在命令行使用curl命令,来测试服务器运行情况:


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

接下来我们创建Flask应用。重复上面的步骤。


  
  
  1. from flask import Flask
  2. from flask import Response
  3. flask_app = Flask('flaskapp')
  4. @flask_app.route('/hello')
  5. def hello_world():
  6. return Response(
  7. 'Hello world from Flask!\n',
  8. mimetype='text/plain'
  9. )
  10. app = flask_app.wsgi_app

将上面的代码保存为flaskapp.py,或者直接从我的Github仓库下载文件,并运行:


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

然后在浏览器地址栏输入http://localhost:8888/hello,并按下回车:

Flask应用运行情况

同样,在命令行使用curl命令,看看服务器是否会返回Flask应用生成的信息:


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

这个服务器是不是也能支持Django应用?试一试就知道了!不过接下来的操作更为复杂一些,我建议大家克隆整个仓库,并使用其中的djangoapp.py文件。下面的代码将一个名叫helloworld的Django应用添加至当前的Python路径中,然后导入了该项目的WSGI应用。


  
  
  1. import sys
  2. sys.path.insert(0, './helloworld')
  3. from helloworld import wsgi
  4. app = wsgi.application

将上面的代码保存为djangoapp.py,并使用你开发的服务器运行这个Django应用。


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

同样,在浏览器中输入http://localhost:8888/hello,并按下回车键:

Django应用的运行情况

接下来,和前面几次一样,你通过命令行使用curl命令进行测试,确认了这个Djando应用成功处理了你发出的请求:


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

你有没有按照上面的步骤测试?你做到了让服务器支持全部三种框架吗?如果没有,请尽量自己动手操作。阅读代码很重要,但这系列文章的目的在于重新开发,而这意味着你需要自己亲自动手。最好是你自己重新输入所有的代码,并确保代码运行结果符合预期。

经过上面的介绍,你应该已经认识到了WSGI的强大之处:它可以让你自由混合搭配 Web 服务器和框架。WSGI为Python Web 服务器与Python网络框架之间的交互提供了一个极简的接口,而且非常容易在服务器端和框架端实现。下面的代码段分别展示了服务器端和框架端的WSGI接口:


  
  
  1. def run_application(application):
  2. """Server code."""
  3. # This is where an application/framework stores
  4. # an HTTP status and HTTP response headers for the server
  5. # to transmit to the client
  6. headers_set = []
  7. # Environment dictionary with WSGI/CGI variables
  8. environ = {}
  9. def start_response(status, response_headers, exc_info=None):
  10. headers_set[:] = [status, response_headers]
  11. # Server invokes the application' callable and gets back the
  12. # response body
  13. result = application(environ, start_response)
  14. # Server builds an HTTP response and transmits it to the client
  15. def app(environ, start_response):
  16. """A barebones WSGI app."""
  17. start_response('200 OK', [('Content-Type', 'text/plain')])
  18. return ['Hello world!']
  19. run_application(app)

下面给大家解释一下上述代码的工作原理:

  1. 网络框架提供一个命名为application的可调用对象(WSGI协议并没有指定如何实现这个对象)。
  2. 服务器每次从HTTP客户端接收请求之后,调用application。它会向可调用对象传递一个名叫environ的字典作为参数,其中包含了WSGI/CGI的诸多变量,以及一个名为start_response的可调用对象。
  3. 框架/应用生成HTTP状态码以及HTTP响应报头response headers,然后将二者传递至start_response,等待服务器保存。此外,框架/应用还将返回响应的正文。
  4. 服务器将状态码、响应报头和响应正文组合成HTTP响应,并返回给客户端(这一步并不属于WSGI协议)。

下面这张图直观地说明了WSGI接口的情况:

WSGI接口

有一点要提醒大家,当你使用上述框架开发网络应用的时候,你处理的是更高层级的逻辑,并不会直接处理WSGI协议相关的要求,但是我很清楚,既然你正在看这篇文章,你一定对框架端的WSGI接口很感兴趣。所以,我们接下来在不使用Pyramid、Flask或Djando框架的前提下,自己开发一个极简的WSGI网络应用/网络框架,并使用WSGI服务器运行该应用:


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

将上述代码保存为wsgiapp.py文件,或者直接从我的Github仓库下载,然后利用 Web 服务器运行该应用:


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

在浏览器中输入下图中的地址,然后按回车键。结果应该是这样的:

简单的WSGI应用

你刚刚自己编写了一个极简的WSGI网络框架!太不可思议了。

接下来,我们重新分析服务器返回给客户端的对象。下面这张图展示的是你通过HTTP客户端调用Pyramid应用后,服务器生成的HTTP响应:

HTTP响应对象

上图中的响应与你在第一篇中看到的有些类似,但是也有明显不同之处。举个例子,其中就出现了你之前没有看到过的4个HTTP报头:Content-Type,Content-Length,Date和Server。这些事 Web 服务器返回的响应对象通常都会包含的报头。不过,这四个都不是必须的。报头的目的是传递有关HTTP请求/响应的额外信息。

既然你已经对WSGI接口有了更深的理解,下面这张图对响应对象的内容进行了更详细的解释,说明了每条内容是如何产生的。

HTTP响应对象2

到目前为止,我还没有介绍过environ字典的具体内容,但简单来说,它是一个必须包含着WSGI协议所指定的某些WSGI和CGI变量。服务器从HTTP请求中获取字典所需的值。下面这张图展示的是字典的详细内容:

Environ字典的详细内容

网络框架通过该字典提供的信息,根据指定的路由和请求方法等参数来决定使用哪个视图views,从哪里读取请求正文,以及如何输出错误信息。

截至目前,你已经成功创建了自己的支持WSGI协议的 Web 服务器,还利用不同的网络框架开发了多个网络应用。另外,你还自己开发了一个极简的网络框架。本文介绍的内容不可谓不丰富。我们接下来回顾一下WSGI Web 服务器如何处理HTTP请求:

  • 首先,服务器启动并加载网络框架/应用提供的application可调用对象
  • 然后,服务器读取一个请求信息
  • 然后,服务器对请求进行解析
  • 然后,服务器使用请求数据创建一个名叫environ的字典
  • 然后,服务器以environ字典和start_response可调用对象作为参数,调用application,并获得应用生成的响应正文。
  • 然后,服务器根据调用application对象后返回的数据,以及start_response设置的状态码和响应标头,构建一个HTTP响应。
  • 最后,服务器将HTTP响应返回至客户端。

服务器工作原理梳理

以上就是第二部分的所有内容。你现在已经拥有了一个正常运行的WSGI服务器,可以支持通过遵守WSGI协议的网络框架所写的网络应用。最棒的是,这个服务器可以不需要作任何代码修改,就可以与多个网络框架配合使用。

最后,我再给大家留一道思考题:怎样让服务器一次处理多个请求?



本文来自云栖社区合作伙伴“Linux中国”

原文发布时间为:2013-04-02.


相关文章
|
6天前
|
存储 人工智能 自然语言处理
ChatMCP:基于 MCP 协议开发的 AI 聊天客户端,支持多语言和自动化安装 MCP 服务器
ChatMCP 是一款基于模型上下文协议(MCP)的 AI 聊天客户端,支持多语言和自动化安装。它能够与多种大型语言模型(LLM)如 OpenAI、Claude 和 OLLama 等进行交互,具备自动化安装 MCP 服务器、SSE 传输支持、自动选择服务器、聊天记录管理等功能。
62 14
ChatMCP:基于 MCP 协议开发的 AI 聊天客户端,支持多语言和自动化安装 MCP 服务器
|
14天前
|
前端开发 安全 JavaScript
2025年,Web3开发学习路线全指南
本文提供了一条针对Dapp应用开发的学习路线,涵盖了Web3领域的重要技术栈,如区块链基础、以太坊技术、Solidity编程、智能合约开发及安全、web3.js和ethers.js库的使用、Truffle框架等。文章首先分析了国内区块链企业的技术需求,随后详细介绍了每个技术点的学习资源和方法,旨在帮助初学者系统地掌握Dapp开发所需的知识和技能。
2025年,Web3开发学习路线全指南
|
21天前
|
存储 前端开发 JavaScript
如何在项目中高效地进行 Web 组件化开发
高效地进行 Web 组件化开发需要从多个方面入手,通过明确目标、合理规划、规范开发、加强测试等一系列措施,实现组件的高效管理和利用,从而提高项目的整体开发效率和质量,为用户提供更好的体验。
27 7
|
25天前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
|
24天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
35 2
|
27天前
|
安全 开发工具 Swift
Swift 是苹果公司开发的现代编程语言,具备高效、安全、简洁的特点,支持类型推断、闭包、泛型等特性,广泛应用于苹果各平台及服务器端开发
Swift 是苹果公司开发的现代编程语言,具备高效、安全、简洁的特点,支持类型推断、闭包、泛型等特性,广泛应用于苹果各平台及服务器端开发。基础语法涵盖变量、常量、数据类型、运算符、控制流等,高级特性包括函数、闭包、类、结构体、协议和泛型。
27 2
|
28天前
|
XML 前端开发 JavaScript
PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑
本文深入探讨了PHP与Ajax在Web开发中的交互技术。PHP作为服务器端脚本语言,处理数据和业务逻辑;Ajax则通过异步请求实现页面无刷新更新。文中详细介绍了两者的工作原理、数据传输格式选择、具体实现方法及实际应用案例,如实时数据更新、表单验证与提交、动态加载内容等。同时,针对跨域问题、数据安全与性能优化提出了建议。总结指出,PHP与Ajax的结合能显著提升Web应用的效率和用户体验。
40 3
|
1月前
|
前端开发 API 开发者
Python Web开发者必看!AJAX、Fetch API实战技巧,让前后端交互如丝般顺滑!
在Web开发中,前后端的高效交互是提升用户体验的关键。本文通过一个基于Flask框架的博客系统实战案例,详细介绍了如何使用AJAX和Fetch API实现不刷新页面查看评论的功能。从后端路由设置到前端请求处理,全面展示了这两种技术的应用技巧,帮助Python Web开发者提升项目质量和开发效率。
51 1
|
2天前
|
弹性计算 运维 安全
阿里云轻量应用服务器与ECS的区别及选择指南
轻量应用服务器和云服务器ECS(Elastic Compute Service)是两款颇受欢迎的产品。本文将对这两者进行详细的对比,帮助用户更好地理解它们之间的区别,并根据自身需求做出明智的选择。
|
3天前
|
SQL 弹性计算 安全
阿里云上云优选与飞天加速计划活动区别及购买云服务器后续必做功课参考
对于很多用户来说,购买云服务器通常都是通过阿里云当下的各种活动来购买,这就有必要了解这些活动的区别,同时由于活动内的云服务器购买之后还需要单独购买并挂载数据盘,还需要设置远程密码以及安全组等操作之后才能正常使用云服务器。本文就为大家介绍一下目前比较热门的上云优选与飞天加速计划两个活动的区别,以及通过活动来购买云服务器之后的一些必做功课,确保云服务器可以正常使用,以供参考。