一起写一个 Web 服务器

2023-11-15

http://my.oschina.net/leejun2005/blog/486771

一起写一个 Web 服务器(2)

2015/06/06 · 实践项目 · 9 评论 · Web服务器

分享到: 8
本文由  伯乐在线 -  高世界 翻译, 艾凌风 校稿。未经许可,禁止转载!
英文出处: ruslan spivak。欢迎加入 翻译组

还记得吗?在本系列第一部分我问过你:“怎样在你的刚完成的WEB服务器下运行 Django 应用、Flask 应用和 Pyramid 应用?在不单独修改服务器来适应这些不同的WEB框架的情况下。”往下看,来找出答案。

过去,你所选择的一个Python Web框架会限制你选择可用的Web服务器,反之亦然。如果框架和服务器设计的是可以一起工作的,那就很好:

但是,当你试着结合没有设计成可以一起工作的服务器和框架时,你可能要面对(可能你已经面对了)下面这种问题:

基本上,你只能用可以在一起工作的部分,而不是你想用的部分。

那么,怎样确保在不修改Web服务器和Web框架下,用你的Web服务器运行不同的Web框架?答案就是Python Web服务器网关接口(或者缩写为WSGI,读作“wizgy”)。

WSGI允许开发者把框架的选择和服务器的选择分开。现在你可以真正地混合、匹配Web服务器和Web框架了。例如,你可以在Gunicorn或者Nginx/uWSGI或者Waitress上面运行Django,Flask,或Pyramid。真正的混合和匹配哟,感谢WSGI服务器和框架两者都支持:

就这样,WSGI成了我在本系列第一部分和本文开头重复问的问题的答案。你的Web服务器必须实现WSGI接口的服务器端,所有的现代Python Web框架已经实现 了WSGI接口的框架端了,这就让你可以不用修改服务器代码,适应某个框架。

现在你了解了Web服务器和WEb框架支持的WSGI允许你选择一对儿合适的(服务器和框架),它对服务器和框架的开发者也有益,因为他们可以专注于他们特定的领域,而不是越俎代庖。其他语言也有相似的接口:例如,Java有Servlet API,Ruby有Rack。

一切都还不错,但我打赌你会说:“秀代码给我看!” 好吧,看看这个漂亮且简约的WSGI服务器实现

# Tested with Python 2.7.9, Linux & Mac OS X
import socket
import StringIO
import sys
 
 
class WSGIServer ( object ) :
 
     address_family = socket . AF_INET
     socket_type = socket . SOCK_STREAM
     request_queue_size = 1
 
     def __init__ ( self , server_address ) :
         # Create a listening socket
         self . listen_socket = listen_socket = socket . socket (
             self . address_family ,
             self . socket _type
         )
         # Allow to reuse the same address
         listen_socket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
         # Bind
         listen_socket . bind ( server_address )
         # Activate
         listen_socket . listen ( self . request_queue_size )
         # Get server host name and port
         host , port = self . listen_socket . getsockname ( ) [ : 2 ]
         self . server_name = socket . getfqdn ( host )
         self . server_port = port
         # Return headers set by Web framework/Web application
         self . headers_set = [ ]
 
     def set_app ( self , application ) :
         self . application = application
 
     def serve_forever ( self ) :
         listen_socket = self . listen_socket
         while True :
             # New client connection
             self . client_connection , client_address = listen_socket . accept ( )
             # Handle one request and close the client connection. Then
             # loop over to wait for another client connection
             self . 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 data
         env = self . get_environ ( )
 
         # It's time to call our application callable and get
         # back a result that will become HTTP response body
         result = self . application ( env , self . start_response )
 
         # Construct a response and send it back to the client
         self . 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 ,    # GET
         self . path ,              # /hello
         self . 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 variables
         env [ 'wsgi.version' ]        = ( 1 , 0 )
         env [ 'wsgi.url_scheme' ]    = 'http'
         env [ 'wsgi.input' ]          = StringIO . StringIO ( self . request_data )
         env [ 'wsgi.errors' ]        = sys . stderr
         env [ 'wsgi.multithread' ]    = False
         env [ 'wsgi.multiprocess' ] = False
         env [ 'wsgi.run_once' ]      = False
         # Required CGI variables
         env [ 'REQUEST_METHOD' ]      = self . request_method      # GET
         env [ 'PATH_INFO' ]          = self . path                # /hello
         env [ 'SERVER_NAME' ]        = self . server_name        # localhost
         env [ 'SERVER_PORT' ]        = str ( self . server_port )    # 8888
         return env
 
     def start_response ( self , status , response_headers , exc_info = None ) :
         # Add necessary server headers
         server_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_response
 
     def finish_response ( self , result ) :
         try :
             status , response_headers = self . headers_set
             response = '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 ) = '' , 8888
 
 
def make_server ( server_address , application ) :
     server = WSGIServer ( server_address )
     server . set_app ( application )
     return server
 
 
if __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 ( )

它明显比本系列第一部分中的服务器代码大,但为了方便你理解,而不陷入具体细节,它也足够小了(只有150行不到)。上面的服务器还做了别的事 – 它可以运行你喜欢的Web框架写的基本的Web应用,可以是Pyramid,Flask,Django,或者其他的Python WSGI框架。

不信?自己试试看。把上面的代码保存成webserver2.py或者直接从Github上下载。如果你不带参数地直接运行它,它就会报怨然后退出。

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

它真的想给Web框架提供服务,从这开始有趣起来。要运行服务器你唯一需要做的是安装Python。但是要运行使用Pyramid,Flask,和Django写的应用,你得先安装这些框架。一起安装这三个吧。我比较喜欢使用virtualenv。跟着以下步骤来创建和激活一个虚拟环境,然后安装这三个Web框架。

Python
1
2
3
4
5
6
7
8
9
10
$ [ 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开始吧。保存以下代码到保存webserver2.py时相同的目录。命名为pyramidapp.py。或者直接从Github上下载:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pyramid . config import Configurator
from pyramid . response import Response
 
 
def 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 ( )

现在你已经准备好用完全属于自己的Web服务器来运行Pyramid应用了:

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

刚才你告诉你的服务器从python模块‘pyramidapp’中加载可调用的‘app’,现在你的服务器准备好了接受请求然后转发它们给你的Pyramid应用。目前应用只处理一个路由:/hello 路由。在浏览器里输入http://localhost:8888/hello地址,按回车键,观察结果:

你也可以在命令行下使用‘curl’工具来测试服务器:

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

检查服务器和curl输出了什么到标准输出。

现在弄Flask。按照相同的步骤。

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
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

保存以上代码为flaskapp.py或者从Github上下载它。然后像这样运行服务器:

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

现在在浏览器里输入http://localhost:8888/hello然后按回车:

再一次,试试‘curl’,看看服务器返回了一条Flask应用产生的消息:

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

服务器也能处理Django应用吗?试试吧!尽管这有点复杂,但我还是推荐克隆整个仓库,然后使用djangoapp.py,它是GitHub仓库的一部分。以下的源码,简单地把Django ‘helloworld’ 工程(使用Django的django-admin.py启动项目预创建的)添加到当前Python路径,然后导入了工程的WSGI应用。

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

把以上代码保存为djangoapp.py,然后用你的Web服务器运行Django应用:

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

输入下面的地址,然后按回车键:

虽然你已经做过两次啦,你还是可以再在命令行测试一下,确认一下,这次是Django应用处理了请求。

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

你试了吧?你确定服务器可以和这三个框架一起工作吧?如果没试,请试一下。阅读挺重要,但这个系列是关于重建的,也就是说,你要自己动手。去动手试试吧。别担心,我等你哟。你必须试下,最好呢,你亲自输入所有的东西,确保它工作起来像你期望的那样。

很好,你已经体验到了WSGI的强大:它可以让你把Web服务器和Web框架结合起来。WSGI提供了Python Web服务器和Python Web框架之间的一个最小接口。它非常简单,在服务器和框架端都可以轻易实现。下面的代码片段展示了(WSGI)接口的服务器和框架端:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 client
     headers_set = [ ]
     # Environment dictionary with WSGI/CGI variables
     environ = { }
 
     def start_response ( status , response_headers , exc_info = None ) :
         headers_set [ : ] = [ status , response_headers ]
 
     # Server invokes the ‘application' callable and gets back the
     # response body
     result = application ( environ , start_response )
     # Server builds an HTTP response and transmits it to the client
    
 
def app ( environ , start_response ) :
     """A barebones WSGI app."""
     start_response ( '200 OK' , [ ( 'Content-Type' , 'text/plain' ) ] )
     return [ 'Hello world!' ]
 
run_application ( app )

以下是它如何工作的:

  • 1.框架提供一个可调用的’应用’(WSGI规格并没有要求如何实现)
  • 2.服务器每次接收到HTTP客户端请求后,执行可调用的’应用’。服务器把一个包含了WSGI/CGI变量的字典和一个可调用的’start_response’做为参数给可调用的’application’。
  • 3.框架/应用生成HTTP状态和HTTP响应头,然后把它们传给可调用的’start_response’,让服务器保存它们。框架/应用也返回一个响应体。
  • 4.服务器把状态,响应头,响应体合并到HTTP响应里,然后传给(HTTP)客户端(这步不是(WSGI)规格里的一部分,但它是后面流程中的一步,为了解释清楚我加上了这步)

以下是接口的视觉描述: 

目前为止,你已经了解了Pyramid,Flask,和Django Web应用,你还了解了实现了WSGI规范服务器端的服务器代码。你甚至已经知道了不使用任何框架的基本的WSGI应用代码片段。

问题就在于,当你使用这些框架中的一个来写Web应用时,你站在一个比较高的层次,并不直接和WSGI打交道,但我知道你对WSGI接口的框架端好奇,因为你在读本文。所以,咱们一起写个极简的WSGI Web应用/Web框架吧,不用Pyramid,Flask,或者Django,然后用你的服务器运行它:

Python
1
2
3
4
5
6
7
8
9
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' ]

再次,保存以上代码到wsgiapp.py文件,或者直接从GitHub上下载,然后像下面这样使用你的Web服务器运行应用:

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

输入下面地址,敲回车。你应该就看到下面结果了:

在你学习怎样写一个Web服务器时,你刚刚写了一个你自己的极简的WSGI Web框架!棒极啦。

现在,让我们回头看看服务器传输了什么给客户端。以下就是使用HTTP客户端调用Pyramid应用时生成的HTTP响应: 

这个响应跟你在本系列第一部分看到的有一些相近的部分,但也有一些新东西。例如,你以前没见过的4个HTTP头:Content-Type, Content-Length, Date, 和Servedr。这些头是Web服务器生成的响应应该有的。虽然他们并不是必须的。头的目的传输HTTP请求/响应的额外信息。

现在你对WSGI接口了解的更多啦,同样,以下是带有更多信息的HTTP响应,这些信息表示了哪些部件产生的它(响应): 

我还没有介绍’environ’字典呢,但它基本上就是一个Python字典,必须包含WSGI规范规定的必要的WSGI和CGI变量。服务器在解析请求后,从HTTP请求拿到了字典的值,字典的内容看起来像下面这样: 

Web框架使用字典里的信息来决定使用哪个视图,基于指定的路由,请求方法等,从哪里读请求体,错误写到哪里去,如果有的话。

现在你已经创建了你自己的WSGI Web服务器,使用不同的Web框架写Web应用。还有,你还顺手写了个简单的Web应用/Web框架。真是段难忘的旅程。咱们简要重述下WSGI Web服务器必须做哪些工作才能处理发给WSGI应用的请求吧:

  • 首先,服务器启动并加载一个由Web框架/应用提供的可调用的’application’
  • 然后,服务器读取请求
  • 然后,服务器解析它
  • 然后,服务器使用请求的数据创建了一个’environ’字典
  • 然后,服务器使用’environ’字典和’start_response’做为参数调用’application’,并拿到返回的响应体。
  • 然后,服务器使用调用’application’返回的数据,由’start_response’设置的状态和响应头,来构造HTTP响应。
  • 最终,服务器把HTTP响应传回给户端。 

这就是全部啦。现在你有了一个可工作的WSGI服务器,它可以处理使用像Django,Flask,Pyramid或者 你自己的WSGI框架这样的兼容WSGI的Web框架写的基本的Web应用。最优秀的地方是,服务器可以在不修改代码的情况下,使用不同的Web框架。

在你离开之前,还有个问题请你想一下,“该怎么做才能让服务器同一时间处理多个请求呢?”

保持关注,我会在本系列第三部分秀给你看实现它的一种方式。欢呼!

顺便说下,我在写一本书《一起构建WEB服务器:第一步》,它解释了从零开始写一个基本的WEB服务器,还更详细地讲解了我上面提到的话题。订阅邮件组来获取关于书籍和发布时间和最近更新。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一起写一个 Web 服务器 的相关文章

随机推荐

  • touch、mkdir、rmdir、cp、mv、rm命令的常用参数的使用

    touch 可创建多个新文件或更新文件的修改日期 touch m t 时间 修改文件的时间 并可以指定修改时间 touch a 将文件的存取时间改为当前时间 mkdir 用于创建一个目录 mkdir p 用于创建目录时 如果父目录不存在 则
  • 基于MATLAB用图解法解方程(附图像与代码)

    目录 一 一元方程图解法 例题1 二 二元方程图解法 例题2 三 多项式型方程 例题 3 一 一元方程图解法 例题1 用图解法求 解 MATLAB代码 clc clear ezplot exp 3 t sin 4 t 2 4 exp 0 5
  • C# 笔记4——如何实现单击放大全屏和退出全屏

    C 笔记4 如何实现单击放大全屏和退出全屏 由于工作需求 需要实现单击放大和退出全屏功能 想了一下 即单击放大时候把播放视频的picturebox的大小设置和屏幕宽高相同 位置设置为屏幕左上角 0 0 即可 单击退出全屏时候把控件大小和位置
  • 扩散模型与生成模型详解

    扩散模型与其他生成模型 什么是扩散模型 扩散模型的简介 生成建模是理解自然数据分布的开创性任务之一 VAE GAN和Flow系列模型因其实用性能而在过去几年中占据了该领域的主导地位 尽管取得了商业上的成功 但它们的理论和设计缺陷 棘手的似然
  • 中国传统节日端午节网页HTML代码 学生网页课程设计期末作业下载 春节大学生网页设计制作成品下载 DW春节节日网页作业代码下载

    HTML5期末大作业 节日网站设计 中国传统节日端午节网页HTML代码 7页 HTML CSS JavaScript 学生DW网页设计作业成品 web课程设计网页规划与设计 计算机毕设网页设计源码 常见网页设计作业题材有 个人 美食 公司
  • visio技巧(曲线、连接点、自制模具)

    一 画曲线 1 1 铅笔 任意多边形 弧形都可以画曲线 但曲度不好更改 1 2 鼠标选中连接线 在画布上画一个直角线 选中该线 点击右键 选曲线连接线 随意拉动该线上的连接点可以调整成任意曲度 二 增加 移动 删除图形上的连接点 1 1 增
  • 【Python 基础篇】Python代码 之 程序结构

    目录 前言 一 顺序结构 1 1 分支结构 1 2 双向分支 1 3 多路分支 1 4 if语句补充 二 顺序结构 三 循环结构 while while else for in for else 四 流程控制语句 break continu
  • SparkStreaming知识总结

    一 流式计算的概述 1 1 什么是流式计算 1 数据流与静态数据的区别 数据流指的就是不断产生的数据 是源源不断 不会停止 静态数据指的就是存储在磁盘中的固定的数据 2 流式计算的概念 就是对数据流进行计算 由于数据是炼苗不断的产生的 所以
  • VMware15.5安装win7旗舰版系统

    1 启动vmware 文件 新建虚拟机 2 选择自定义安装 下一步 3 兼容性默认不做修改 下一步 4 安装来源选择稍后安装操作系统 5 选择windows7 64 6 安装位置选择非系统盘位置 放在C盘会拖累系统运行速度 7 8 取决于物
  • PostgreSQL_row_number() over()

    语法 row number over partition by col1 order by col2 desc row number 为返回的记录定义各行编号 pritition by 分组 order by 排序 实例 实例数据来源 利用
  • vue实现文件下载

    原理 a href url 复制代码 实际使用场景 上面的原理中适合开放的资源下载 http请求中无需验证时使用 在实际使用过程中 a标签中的url中直接设置header比较麻烦且不安全 而且从开发规范上api一般上要封装一下 header
  • 华为OD机试 - 最多颜色的车辆(Java)

    题目描述 在一个狭小的路口 每秒只能通过一辆车 假设车辆的颜色只有 3 种 找出 N 秒内经过的最多颜色的车辆数量 三种颜色编号为0 1 2 输入描述 第一行输入的是通过的车辆颜色信息 0 1 1 2 代表4 秒钟通过的车辆颜色分别是 0
  • LeetCode 2545. 根据第 K 场考试的分数排序

    班里有 m 位学生 共计划组织 n 场考试 给你一个下标从 0 开始 大小为 m x n 的整数矩阵 score 其中每一行对应一位学生 而 score i j 表示第 i 位学生在第 j 场考试取得的分数 矩阵 score 包含的整数 互
  • git忽略指定文件夹

    git忽略指定文件夹 如下结构 总共有三个文件夹 假设要忽略第一层的B文件夹 在目录下新建一个 gitignore文件 并填写下面内容 B 假设要忽略第一层的A文件夹 在文件中填写A 的话 会把B文件夹下的A文件夹也忽略了 这个时候可以加上
  • 要称王,先做行业破坏者

    author skate time 2010 06 18 高端阅读78期 原标题为 世界 油王 的职场启示 我的人生 狠 字当头 有极强的故事性 白手起家 狂赚几亿美元 后遭朋友暗算 被踢出一手创办并成功发展40年的公司 同期不得不应对麻烦
  • Linux nrm 运行失败,解决:npm中 下载速度慢 和(无法将“nrm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确, 然后再试一次)...

    1 解决下载速度 因为我们npm下载默认是 连接国外的服务器 所以网速不是特别好的时候 可能下不了包 安装nrm 使用 npm i nrm g 我们的一般工具包都是下载到全局 安装完毕之后 可以运行 命令 nrm ls ls 表示 list
  • Django-Model层ORM之查询操作(六)

    目录 一 Django查询相关API all 查询所有记录 返回一个集合对象 filter 属性 根据条件查询 返回一个集合对象 first 和 last 查询第一个和最后一个记录 返回单个对象 get id 2 根据id查询 返回一个对象
  • Ubuntu 15.04 下编译Caffe2

    深度学习大神贾扬清在四月底发布了最新框架Caffe2 最近在Ubuntu15 04下编译了它的源代码 遇到一些坑 记录下来以供参考 基本安装次序如官网所述 https caffe2 ai docs getting started html
  • k8s Trouble Shooting 故障排除

    本文要讲的是k8s的故障排除 比较浅 最近刚入门 主要涵盖的内容是查看k8s对象的当前运行时信息 对于服务 容器的问题是如何诊断的 对于某些复杂的问题例如pod调度问题是如何排查的 1 查看系统的Event事件 在对象资源 pod serv
  • 一起写一个 Web 服务器

    http my oschina net leejun2005 blog 486771 一起写一个 Web 服务器 2 2015 06 06 实践项目 9 评论 Web服务器 分享到 8 本文由 伯乐在线 高世界 翻译 艾凌风 校稿 未经许可