【翻译】一步步开发一个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 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()

你肯定在想,什么鬼?说好的简单实现呢?(译者:我也在想什么鬼!这么多!)但是它足够小到能让你理解却不会陷入到细节上。上面最简单的服务器也能做到很多,他可以运行最基本的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 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()

现在你可以写一个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 wsgi


app = 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 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. 框架提供一个可供调用的‘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中讲解。老铁们,双击评论666,小火箭走一波!

0 0 投票数
文章评分
订阅评论
提醒
guest
10 评论
内联反馈
查看所有评论
Lenzo

赞,咋个注册

lenzo

我是纯小白,给个内部注册。以后遇到问题可以直接再评论区咨询

chenwong

666

chenwong

大佬,我想了解下,您的vps是购买的还是用的你的安卓手机做的内网穿透。感觉访问很流畅啊