Dantangfan

自己动手写一个简单的 python web framework

每天都使用 web 框架,实际上实现一个简单的 web 框架也是很容易的

写在前面

虽然这篇文章很简单,但是依然需要一些基础知识。

好了,上面就是所有的基础知识。理清这些基础知识,就不用继续往下看了,直接上代码就可以了。

重中之重,处略看完代码之后,可以看这幅图来理清逻辑

框架设计

我们的目标是用python原生的包来构建一个可以响应的web框架,它非常简单:支持url路由、get、post方法,支持cookie,其他内容实现并不困难,但这里只介绍最简单的。

我们要实现的web框架最终的helloworld可以这样写

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from easyweb import *

@get('/')
def index(request):
    return 'hello world !'

runserver()

其中@get('/')获取了路由,并将路由注册到相应的处理函数中,request是浏览器发送的请求,数据由字典表示,可以在处理函数中自由调用。return语句将字符串响应到客户端,字符串可以是html格式的,这样就能在浏览器直接渲染成响应的网页。

上面几乎就是这篇文章要实现的所有功能。

服务器

于是我们就先从服务器说起吧,如果您使用过wsgiref这个包,强烈建议您去看看他的源代码,因为我的代码多数也是从里面copy过来。

我们要写的服务器只需要满足WSGI就行,如何从网络上读取或者写入数据不是我们的目标,所以我们使用了python内置的BaseHTTPserver,它将负责数据的最终传输。在他的上层,我们需要实现自己的服务器处理函数,经过我们我们服务器加工过的数据必须是符合WSGI标准的。

服务器就像一个容器,他把web框架放在里面,外面连接着网络。在网络数据请求到来的时候,要先经过服务器加工处理才能到框架/应用程序手里,我们常常看到的应用都是用框架写的,有些长得如下:

@get('/')
def index(request):
    return 'hello world !'

其实,这里的request是已经被服务器处理过的数据了,不要认为浏览器那么聪明,一下子就发过来一个你需要的数据类型。

相信你已经看过PEP333了,一个典型的WSGI应用一般长这样:

def application(environ, start_response):
    """Simplest possible application object"""
    status = "200 OK"
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return ['Hello world !\n']

一个完整应用当然要比这个复杂点点咯

def handle_request(environ, start_response):
    try:
        request = Request(environ, start_response)
    except Exception, e:
        return Response(e, "%s %s" % (500, _RESPONSE_STATUSES[500]))
    try:
        (re_url, url, callback), kwargs = find_matching_url(request)
        response = callback(request, **kwargs)
    except Exception, e:
        return handle_error(e, request)
    if not isinstance(response, Response):
        response = Response(response)
    return response.send(start_response)

他按照PEP333要求返回了一个可迭代对象, 但是他要返回给谁呢?当然是服务器咯。在服务器处理一次请求的时候会调用一次你的应用程序,并将你的应用程序的输出作为最终输出到web的HTTP body,服务器的run函数大致如下。

    def run(self, application):
        try:
            self.setup()
            self.result = application(self.environ, self.start_response)
            self.finish()
        except:
            try:
                self.handle_error()
            except:
                self.close()
                raise

start_response是什么东西?这个参数是用来干什么的呢?

他是一个函数,属于服务器的函数,它主要用于处理输出和错误控制。

    def start_response(self, status, headers, exc_info=None):
        if exc_info:
            try:
                if self.headers_sent:
                    raise exc_info[0], exc_info[1], exc_info[2]
            finally:
                exc_info = None
        elif self.headers is not None:
            raise AssertionError("headers already sent")
        self.status = status
        self.headers = Header(headers)
        return self.write

按照PEP333的规定,第三个参数只有出错的时候才会有值,并且每次调用之后都必须保证他这个参数的值为空,PEP333还规定,在调用这个函数之前如果已经发送了header,那么就需要抛出错误。返回的write是一个可调用的函数,这时WSGI为了和之前的框架相兼容强加的,对我们新的框架一般没神码用。

然后整个服务器框架剩余的部分几乎都是围绕者两个函数展开的,至于handle_request是怎么跑的就是框架的事情了,我们等等再说。

我们可以看到,服务器每处理一个请求实际上就是调用一次run的过程。

首先每次处理一个请求的时候都要重置环境变量setup(),这里加入了PEP要求的必须的环境变量,而这些环境变量只能由服务器提供,比如操作系统变量等。

    def setup(self):
        env = self.environ
        env['wsgi.input'] = self.get_stdin()
        env['wsgi.errors'] = self.get_stderr()
        env['wsgi.version'] = (0, 1)
        env['wsgi.run_once'] = False
        env['wsgi.url_scheme'] = self.http_s_()
        env['wsgi.multithread'] = True
        env['wsgi.multiprocess'] = True

然后我们用result接收应用程序返回的可迭代对象。

最后finish()完成一次请求。

    def finish(self):
        for data in self.result:
            self.write(data)
        self.finish_content()
        self.close()

在关闭连接之前,我们需要先把数据写回给客户端,write()finish_content()的相互作用,保证了在第一次写回数据之前,headers已经被发送了,

   def finish_content(self):
        if not self.headers_sent:
            self.headers['Content-Length'] = 0
            self.send_headers()
        else:
            pass

    def write(self, data):
        assert type(data) is types.StringType
        if not self.status:
            raise AssertionError('write() before start_response()')
        if not self.headers_sent:
            self.bytes_sent = len(data)
            self.send_headers()
        else:
            self.bytes_sent += len(data)
        self._write(data)
        self._flush()

    def send_headers(self):
        if not self.headers.haskey('Content-Length'):
            try:
                blocks = len(self.result)
            except:
                pass
            else:
                if blocks == 1:
                    self.headers['Content-Length'] = str(self.bytes_sent)
                else:
                    pass
                    # todo
        self.headers_sent = True
        self._write('Status %s\r\n' % self.status)
        self._write(str(self.headers))

在发送headers的时候,按照PEP333的要求,如果没有Content-Length变量,服务器会自己拟定一个变量或者直接关闭连接。

这样,我们的WSGI的服务器基本功能就差不多完成了,剩下的就是适配到HTTPserver和BaseRequestHandler上。这不是这篇文章要描述的,直接看代码。

def make_server(host, port, app, server_class=WSGIServer, handle_class=WSGIRequestHandler):
    server = server_class((host, port), handle_class)
    server.set_app(app)
    return server

框架

刚刚我们看到我们的应用程序是长这样的

def handle_request(environ, start_response):
    try:
        request = Request(environ, start_response)
    except Exception, e:
        return Response(e, "%s %s" % (500, _RESPONSE_STATUSES[500]))
    try:
        (re_url, url, callback), kwargs = find_matching_url(request)
        response = callback(request, **kwargs)
    except Exception, e:
        return handle_error(e, request)
    if not isinstance(response, Response):
        response = Response(response)
    return response.send(start_response)

这个程序包括了我们框架所有需要的东西:一个Request,一个Response和一个错误处理。

在文章的开头,给了一个描述一次请求的流程图,就是这幅图。在真正执行我们的应用程序的时候:

@get('/')
def index(request):
    return 'hello world !'

这里的request参数其实已经是一个Request对象。这个对象很简单,只需要一些提取提取请求内容的功能

class Request(object):

    def __init__(self, environ, start_response):
        self._environ = environ
        self._start_response = start_response

    def __getitem__(self, key):
          pass
    def get(self, key, default=None):
        pass
    def gets(self, key):
        pass
    def get_body(self):
        pass
    @property
    def remote_addr(self):
        pass
    @property
    def document_root(self):
        pass
    @property
    def query_string(self):
        pass
    @property
    def environ(self):
        pass
    @property
    def request_method(self):
        pass
    @property
    def path_info(self):
        pass
    @property
    def host(self):
        pass

    @property
    def cookies(self):
        pass
    def cookie(self, name, default=None):
        pass

然后我们需根据find_matching_url(request)url找到对应的处理函数,也就是你写的应用程序

def find_matching_url(request):
    if request.request_method not in REQUEST_MAPPINGS:
        raise seemore("The HTTP request method '%s' is not supported." % request.request_method)
    for url_set in REQUEST_MAPPINGS[request.request_method]:
        match = url_set[0].search(request.path_info)
        if match is not None:
            return url_set, match.groupdict()
    raise notfound()

这里的REQUEST_MAPPINGS是我们的函数注册列表,应用启动的时候,就已经把每个函数注册到了对应的方法上。

def get(url):
    """
    decorator for get method
    register
    """
    url = add_slash(url)
    def _decorator(func):
        re_url = re.compile("^%s$" % url)
        REQUEST_MAPPINGS['GET'].append((re_url, url, func))
        return func
    return _decorator

之后,再生成一个Response对象作为响应,并且调用他的send函数来返回数据

    def send(self, start_response):
        start_response(self.status, self.headers)
        if isinstance(self._output, unicode):
            return self._output.encode("utf-8")
        return self._output

这个返回值就直接返回到了我们的服务器run函数的result中去了。

当然, 就如PEP333所说,框架需要实现一些服务器不提供的功能,比如cookie等, 框架还能自由的修改headers:

class Response(object):
    def __init__(self, output="", status="200 OK"):
        """
        :param output:
        :param status:
        :return:
        """
        self._status = status
        self._headers = {'CONTENT-TYPE': 'text/html; charset=utf-8'}
        self._output = output

    @property
    def headers(self):
        pass

    def unset_header(self, name):
        pass

    def set_header(self, name, value):
        pass

    @property
    def content_type(self):
        return self.header('CONTENT-TYPE')

    def set_cookie(self, name, value, max_age=None, expires=None, path="", domain=None, secure=False, http_only=True):
        pass

    def unset_cookie(self, name):
        pass

然后,我们的framework就做好了!!它既包含了一个简单的框架,有包含了一个简单的服务器。

当然,需要改进的地方还有很多。但是我不打算修改了,代码和文档和翻译已经耗费了一周的时间。

参考文件




blog comments powered by Disqus