自己写一个Web服务器(2)

转载自:http://www.codeceo.com/article/make-web-server-2.html

自己写一个Web服务器(1)

自己写一个Web服务器(2)

自己写一个Web服务器(3)

还记着第一篇的问题吗?你怎么在你刚建立的Web服务器上运行一个Django应用,Flask应用和Pyramid应用,如何不做任何改变而适应不同的web架构呢?往下看,来找答案。

在以前,你选择 Python web 架构会受制于可用的web服务器,反之亦然。如果架构和服务器可以协同工作,那你就走运了:

但你有可能面对(或者曾有过)下面的问题,当要把一个服务器和一个架构结合起来是发现他们不是被设计成协同工作的:

基本上你只能用可以一起运行的而非你想要使用的。

那么,你怎么可以不修改服务器和架构代码而确保可以在多个架构下运行web服务器呢?答案就是 Python Web Server Gateway Interface (或简称 WSGI,读作“wizgy”)。

WSGI允许开发者将选择web框架和web服务器分开。现在你可以混合匹配web服务器和web框架,选择一个适合你需要的配对。比如,你可以在Gunicorn 或者 Nginx/uWSGI 或者 Waitress上运行 Django, Flask, 或 Pyramid。真正的混合匹配,得益于WSGI同时支持服务器和架构:

WSGI是第一篇和这篇开头又重复问道问题的答案。你的web服务器必须具备WSGI接口,所有的现代Python Web框架都已具备WSGI接口,它让你不对代码作修改就能使服务器和特点的web框架协同工作。

现在你知道WSGI由web服务器支持,而web框架允许你选择适合自己的配对,但它同样对于服务器和框架开发者提供便利使他们可以专注于自己偏爱的领域和专长而不至于相互牵制。其他语言也有类似接口: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 webserver2.py
Provide a WSGI application object as module:callable

它需要为web应用服务,这样才会有意思。运行服务器你唯一要做的就是按照python。但是要运行 Pyramid, Flask, 和 Django 写的应用你得先按照这些框架。我们索性三个都安装好了。我偏爱用virtualenv。只要按照下面的步骤创建一个虚拟环境然后按照这三个web框架。

$ [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,也可以直接在Githhub下载:

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 应用准备好了:

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

你告诉服务器载入一个来自python ‘pyramidapp’模块的‘app’,然后做好准备接收请求并传给你的 Pyramid 应用。这个应用只处理一个路径: /hello 路径。在浏览器中输入地址 http://localhost:8888/hello,按下Enter,就看到结果:

你也可以用’curl‘在命令行中测试服务器:

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

接着是 Flask。同样的步骤:

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下载,然后运行服务器:

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

在浏览器中输入地址 http://localhost:8888/hello,按下Enter:

继续,用’curl‘看看服务器返回 Flask 应用生成的消息:

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

这个服务器能处理 Django 应用吗?试试看!这会更复杂一点,我建议克隆整个repo,用djangoapp.py, 它是GitHub repository的一部分。这里给出代码,只是添加Django ’helloworld‘工程到当前python路径然后导入它的WSGI应用。

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

app = wsgi.application

保存代码为 djangoapp.py 然后在服务器上运行 Django 应用:

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

输入地址,按下Enter:

同样的就像你以及试过的几次,再用命令行试试看,确认这个 Django 应用也是可以处理你的请求:

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

你试了吗?你确定这个服务器和三个框架都能工作吗?要是没有,快去做吧。看文章很重要,但这个系列是重建,就是说你得身体力行。去试试,我会等你的,别担心。你必须自己验证,最好能自己写所用的东西以确保它能达到预期。

好了,你已经体验了WSGI的强大:它让你混合匹配web服务器和架构。WSGI为python web服务器和框架提供一个最小的接口。它非常简单且容易应用到服务器和框架两端。下面的代码段是服务器和框架端的接口:

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规范没有规定如何应实现)
  2. 服务器从HTTP客户端接收请求,并调用’application’。它把包含 WSGI/CGI 变量的字典‘environ’和‘start_response’ 调用作为参数传给 ‘application’ 。
  3. 框架/应用生成一个HTTP状态和HTTP响应头,将他们传入‘start_response’ 让服务器来存储。框架/应用同时返回响应体。
  4. 服务器将状态,响应头和响应提结合成HTTP响应并且传送到客户端(这一步不是标准中的部分,但却是合乎逻辑的下一步,为明了起见我加在这里)

这里是界面的可视化表示:

到此为止,你看到了Pyramid, Flask, 和 Django Web 应用,看到了服务器端实现WSGI规范的代码。你看到了没用任何框架的WSGI应用代码。

问题是你在用这些框架写web应用时是在一个更高的层级并不直接接触WSGI,但我知道你也好奇框架端的WSGI接口,当然也因为你在看这篇文章。所以,我们来建一个极简的WSGI web应用/框架,不用Pyramid, Flask, 或Django,在你的服务器上运行:

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下载,想下面这样运行:

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

输入地址,按下Enter,你就看到结果:

回去看服务器传了什么给客户端 。这里是你用HTTP客户端调用你的Pyramid应用时服务器生成的HTTP响应:

这个响应有些部分和第一篇看到的相似,但也有些新的东西。它有四个你之前没看到过的HTTP头:内容类型,内容长度,日期和服务器。这些头饰一个web服务器响应通常应该有的。即便没有一个是必须的,它没的作用是发送HTTP请求/响应的附加信息。

你对WSGI接口有了更多的了解,这里还有些信息关于这条HTTP响应是那部分产生的:

我还没说过‘environ’字典的任何东西,它基本上就是必须包含由WSGI规范规定的明确WSGI和CGI参数的python字典。服务器解析请求后从请求中取出参数放入字典。这是字典中包含内容的样子:

web框架用字典中的信息决定通过特点路径的呈现,响应方式去,哪里去读取响应体和哪里去写入错误,如果有的话。

你创建了自己的WSGI web服务器,你用不同框架写了自己的web应用。你也创建了自己基本的web应用/框架。这是一个 heck 之旅。来回顾一下你的WSGI服务器对应用都要做些什么:

  • 首先,服务器启动然后载入你的web框架/应用提供的‘application’调用
  • 然后,服务器读取请求
  • 然后,服务器解析它
  • 然后,它根据请求数据创建一个‘environ’ 字典
  • 然后,它用‘environ’ 字典调用‘application’,‘start_response’ 做参数同时得到一个响应体
  • 然后, 通过‘start_response’ 用‘application’返回的数据和状态及响应头创建一个HTTP响应。
  • 最后,服务器将HTTP响应传回到客户端。

结束之前,有个问题你想想看,你怎么保证你的服务器能同时处理多个请求?

译文链接: http://www.codeceo.com/article/make-web-server-2.html
英文原文: Let’s Build A Web Server. Part 2
翻译作者: 码农网  – 王坚
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值