`
kenby
  • 浏览: 725431 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Tornado源码分析之http服务器篇

阅读更多

一. Tornado是什么?

Facebook发布了开源网络服务器框架Tornado,该平台基于Facebook刚刚收购的社交聚合网站FriendFeed的实时信息服务开发而来.Tornado由Python编写,是一款轻量级的Web服务器,同时又是一个开发框架。采用非阻塞I/O模型(epoll),主要是为了应对高并发 访问量而被开发出来,尤其适用于comet应用。

 

 
二. 为什么要阅读Tornado的源代码
Tornado由前google员工开发, 代码非常精练, 实现也很轻巧, 加上清晰的注释和丰富的demo, 我们可以很容易的阅读分析tornado. 通过阅读Tornado的源码, 你将学到:
   * 理解Tornado的内部实现, 使用tornado进行web开发将更加得心应手
    * 如何实现一个高性能,非阻塞的http服务器
    * 如何实现一个web框架
    * 各种网络编程的知识, 比如epoll
    * python编程的绝佳实践
 
三. 从http服务器开始
Tornado不仅是一个web开发框架, 还自己实现了一个http服务器. 谈到http服务器, 我们自然想到C10K.
其中介绍了很多种服务器的编程模型, tornado的http服务器采用的是: 
多进程 + 非阻塞 + epoll + pre-fork 模型
在分析tornado服务器之前, 有必要了解web服务器的工作流程.
 
四 http服务器工作三部曲
从实现上来说, web服务器是这样工作的:
(1) 创建listen socket, 在指定的监听端口, 等待客户端请求的到来
(2) listen socket接受客户端的请求, 得到client socket, 接下来通过client socket与客户端通信
(3) 处理客户端的请求, 首先从client socket读取http请求的协议头, 如果是post协议, 还可能要
      读取客户端上传的数据, 然后处理请求, 准备好客户端需要的数据, 通过client socket写给客户端
 
五 Hello World from Http Server
为了更加理解web服务器的工作流程, 我们使用python编写一个简单的http服务器, 返回Hello, World给浏览器
import socket

def handle_request(client):
  buf = client.recv(1024)
  print buf
  client.send("HTTP/1.1 200 OK\r\n\r\n")
  client.send("Hello, World")

def main():
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  sock.bind(('localhost',8080))
  sock.listen(5)

  while True:
    connection, address = sock.accept()
    handle_request(connection)
    connection.close()

if __name__ == '__main__':
  main()
 

运行如下:

 

六. Hello World from Tornado Http Server

Tornado不能算是一个完整的http服务器, 它只实现小部分的http协议, 大部分要靠用户去实现.

tornado其实是一个服务器开发框架, 使用它我们可以快速的开发一个高效的http服务器. 下面我们

就使用tornado再写一个Hello, World的Http服务器.

 

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

import tornado.httpserver
import tornado.ioloop

def handle_request(request):
   message = "Hello World from Tornado Http Server"
   request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
                 len(message), message))
   request.finish()

http_server = tornado.httpserver.HTTPServer(handle_request)
http_server.listen(8080)
tornado.ioloop.IOLoop.instance().start()

 运行如下:

实现非常简单, 只需要定义自己的处理方法, 其它的东西全部交给Tornado完成. 简单看一下Tornado做了哪些工作.

 

首先创建HTTPServer类, 并把我们的处理方法传递过去

然后在8080开始监听

最后启动事件循环, 开始监听网络事件. 主要是socket的读和写

 

到了这里, 我有点等不及了, 迫切想了解tornado的内部实现是怎么样的. 特别是想知道Tornado的IOLoop到底是如何

工作的. 接下来我们开始解剖Tornado

 

七. Tornado服务器概览

理解了web服务器的工作流程之后, 我们再来看看Tornado服务器是如何实现这些处理流程的.

Tornado服务器有3大核心模块:

(1) IOLoop

与我们上面那个简陋的http服务器不同, Tornado为了实现高并发和高性能, 使用了一个

IOLoop来处理socket的读写事件, IOLoop基于epoll, 可以高效的响应网络事件. 这是Tornado

高效的保证. 

(2) IOStream

为了在处理请求的时候, 实现对socket的异步读写, Tornado实现了IOStream类, 用来处理socket

的异步读写. 

(3) HTTPConnection

这个类用来处理http的请求, 包括读取http请求头, 读取post过来的数据, 调用用户自定义的处理方法,

以及把响应数据写给客户端socket

 

下面这幅图描述了tornado服务器的大体处理流程, 接下来我们将会详细分析每一步流程的实现

 

八. 创建listen socket

httpserver.py, 定位到bind方法:

 

for res in socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
                            0, socket.AI_PASSIVE | socket.AI_ADDRCONFIG):
  af, socktype, proto, canonname, sockaddr = res
  
  # 创建listen socket
  sock = socket.socket(af, socktype, proto)

  # 设置socket的属性 
  flags = fcntl.fcntl(sock.fileno(), fcntl.F_GETFD)
  flags |= fcntl.FD_CLOEXEC
  fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, flags)
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  if af == socket.AF_INET6:
      if hasattr(socket, "IPPROTO_IPV6"):
          sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  sock.setblocking(0)

  # bind 和 listen
  sock.bind(sockaddr)
  sock.listen(128)

  # 加入ioloop
  self._sockets[sock.fileno()] = sock
  if self._started:
      self.io_loop.add_handler(sock.fileno(), self._handle_events,
                               ioloop.IOLoop.READ)

这是实现web服务器的标准步骤, 首先getaddrinfo返回服务器的所有网卡信息, 每块网卡上都要创建监听客户端的请求.

按照socket -> bind -> listen步骤走下来, 最后把新建的listen socket加入ioloop. 那么ioloop又是个什么东西呢?

 

暂时我们把ioloop理解为一个事件容器. 用户把socket和回调函数注册到容器中, 容器内部会轮询socket, 一旦某个socket

可以读写, 就调用回调函数来处理socket的读写事件.

 

这里, 我们只监听listen socket的读事件, 回调函数为_handle_events, 一旦listen socket可读, 说明客户端请求到来, 

然后调用_handle_events接受客户端的请求. 

 

九. accept

httpserver.py, 定位到_handle_events. 这个方法接受客户端的请求. 

为了便于分析, 我把处理ssl那部分代码剥离出去了.

 

def _handle_events(self, fd, events):
  while True:
      try:
          connection, address = self._sockets[fd].accept()
      except socket.error, e:
          if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
              return
          raise
      try:
          stream = iostream.IOStream(connection, io_loop=self.io_loop)
          HTTPConnection(stream, address, self.request_callback,
                         self.no_keep_alive, self.xheaders)
      except:
          logging.error("Error in connection callback", exc_info=True)

accept方法返回客户端的socket(注意connection的类型是socket), 以及客户端的地址

然后创建IOStream对象, 用来处理socket的异步读写. 这一步会调用ioloop.add_handler把client socket加入ioloop

再然后创建HTTPConnection, 处理用户的请求.

 

 

十. 创建IOStream

 

10.1 何为IOStream

accept完成后, 我们就可以用client socket与客户端通信了. 为了实现对client socket的异步读写, 我们为client socket

创建两个缓冲区: _read_buffer和_write_buffer, 写: 先写到_write_buffer, 读: 从_read_buffer读. 这样我们就不用

直接读写socket, 进而实现异步读写. 这些操作都封装在IOStream类中, 概括来说,

IOStream对socket的读写做了一层封装, 通过使用两个缓冲区, 实现对socket的异步读写.

 

10.2 IOStream的初始化

IOStream与socket是一一对应的, 初始化主要做4个工作

(1) 初始化IOStream对应的socket

(2) 分配输入缓冲区_write_buffer

(3) 分配输出缓冲区_read_buffer

(4) 把socket加入ioloop, 这样当socket可读写的时候, 调用回调函数_handle_events把数据从socket读入buffer, 

     或者把数据从buffer发送给socket

找到iosteram.py, 定位到__init__方法

 

self.socket = socket
self.io_loop = io_loop or ioloop.IOLoop.instance()
self._read_buffer = collections.deque()
self._write_buffer = collections.deque()
self.io_loop.add_handler(
    self.socket.fileno(), self._handle_events, self._state)

 

 

 

10.3 IOStream提供的接口

IOStream对外提供了3个接口, 用来对socket的读写

(1) write(data)

把数据写入IOStream的_write_buffer

 

(2) read_until(delimiter, callback)

从_read_buffer读取数据, delimiter作为读取结束符, 完了调用callback

 

(3) read_bytes(num_of_bytes, callback)

从_read_buffer读取指定大小的数据, 完了调用callback

 

 

read_until和read_bytes都会调用_read_from_buffer把从buffer读取数据, 然后调用_consume消耗掉buffer中

的数据.

 

 

10.4 体验异步IO

下面我们来看一个异步IO的实例, 这是一个异步http client的例子, 使用IOStream来下载http://nginx.net/index.html

 

 

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

from tornado import ioloop
from tornado import iostream
import socket

def send_request():
    stream.write("GET /index.html HTTP/1.0\r\nHost: nginx.net\r\n\r\n")
    stream.read_until("\r\n\r\n", on_headers)

def on_headers(data):
    headers = {}
    for line in data.split("\r\n"):
       parts = line.split(":")
       if len(parts) == 2:
           headers[parts[0].strip()] = parts[1].strip()
    stream.read_bytes(int(headers["Content-Length"]), on_body)

def on_body(data):
    print data
    stream.close()
    ioloop.IOLoop.instance().stop()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
stream = iostream.IOStream(s)
stream.connect(("nginx.net", 80), send_request)
ioloop.IOLoop.instance().start()

首先调用connect连接服务器, 完成后回调send_request发出请求, 并读取服务器返回的http协议头, 然后回调

on_headers解析协议头, 然后调用read_bytes读取数据体, 然后回调on_body把数据打印出来. 最后关闭stream

可以看到, 这一系列的调用都是通过回调函数实现的, 这就是异步的处理方式.

 

 

10.5 IOStream响应ioloop事件

上面提到, IOStream初始化的时候, 把socket加入ioloop, 一旦socket可读写, 就调用回调函数_handle_events处理IO

事件. 打开iostream.py, 定位到_handle_events

 

    def _handle_events(self, fd, events):
        if not self.socket:
            logging.warning("Got events for closed stream %d", fd)
            return
        try:
            if events & self.io_loop.READ:
                self._handle_read()
            if not self.socket:
                return
            if events & self.io_loop.WRITE:
                if self._connecting:
                    self._handle_connect()
                self._handle_write()
            if not self.socket:
                return
            if events & self.io_loop.ERROR:
                # We may have queued up a user callback in _handle_read or
                # _handle_write, so don't close the IOStream until those
                # callbacks have had a chance to run.
                self.io_loop.add_callback(self.close)
                return
            state = self.io_loop.ERROR
            if self.reading():
                state |= self.io_loop.READ
            if self.writing():
                state |= self.io_loop.WRITE
            if state != self._state:
                self._state = state
                self.io_loop.update_handler(self.socket.fileno(), self._state)
        except:
            logging.error("Uncaught exception, closing connection.",
                          exc_info=True)
            self.close()
            raise

 可以看到_handle_events根据IO事件的类型, 来调用不同的处理函数, 对于可读事件, 调用handle_read来处理.

handle_read会从socket读取数据, 然后把数据存到_read_buffer.

 

十一. 处理请求 -- HTTPConnection

HttpConnection类专门用来处理http请求, 处理http请求的一般流程是:

HTTPConnection实现了一系列的函数用来处理这些流程, 参见下图:

 

至于每个函数是如何实现的, 可以参考代码

 

 

十二. IOLoop

在Tornado服务器中, IOLoop是调度的核心模块, Tornado服务器回把所有的socket描述符都注册到IOLoop, 注册的时候

指明回调处理函数, IOLoop内部不断的监听IO事件, 一旦发现某个socket可读写, 就调用其注册时指定的回调函数. 

IOLoop的结构图如下所示:

下面我们使用IOLoop实现一个简单的TCP服务器, 看完之后相信可以对IOLoop有一个大概的了解.

 

12.1 A Simple TCP Server Using IOLoop

 

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

from tornado import ioloop
from tornado import iostream
import socket
import errno
import functools

def handle_connection(client, address):
  client.send("Hello World from A Simple TCP Server")
  client.close()

def connection_ready(sock, fd, events):
    while True:
        try:
            connection, address = sock.accept()
        except socket.error, e:
            if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
                raise
            return
        connection.setblocking(0)
        handle_connection(connection, address)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setblocking(0)
sock.bind(("localhost", 8080))
sock.listen(128)

io_loop = ioloop.IOLoop.instance()
callback = functools.partial(connection_ready, sock)
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
io_loop.start()

创建完listen socket后, 再得到IOLoop的实例, 后面回介绍IOLoop的单例模式.然后调用add_handle把listen socket

注册到ioloop中, 指定监听事件为READ, 指定回调函数为connection_ready. 这样客户端来了一个连接后, 就会调用

connecion_ready来处理连接.

 

12.2 单例模式

 

看了很多IOLoop的代码, 有一个地方相信大家注意到了, 得到IOLoop对象的时候, 都是通过instance()返回的. 事实上,

IOLoop使用了单例模式. 在Tornado运行的整个过程中, 只有一个IOLoop实例. 仅需一个 IOLoop实例, 就可以处理全部

的IO事件.  以前学习J2EE的时候接触过Java的单例模式, 接下来看看Python是如何实现单例模式的. 

 

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

import os

class IOLoop(object):
    @classmethod
    def instance(cls):
        if not hasattr(cls, "_instance"):
            cls._instance = cls()
        return cls._instance

    @classmethod
    def initialized(cls):
        """Returns true if the singleton instance has been created."""
        return hasattr(cls, "_instance")

    def service(self):
      print 'Hello,World'

print IOLoop.initialized(),
ioloop = IOLoop.instance()
ioloop.service()

if os.fork() == 0:
  print IOLoop.initialized(),
  ioloop = IOLoop.instance()
  ioloop.service()

 

代码直接从ioloop.py文件抽取下来的, 演示了Python单例模式的实现方法. 实现相当简洁, 这得益于python强大的自省

功能. 代码中使用了cls, 这不是一个关键字, 像self一样, cls是python的一个built-in变量. self表示类的实例, 而cls表示类,

cls一般用于static method, 因为static method无须实例化就可以调用, 所以传递cls给static method. 然后调用cls()

可以创建对象. 就像调用IOLoop()一样. 

最后两句话:

 

Always use 'self' for the first argument to instance methods.

Always use 'cls' for the first argument to class methods.

 

 
  • 大小: 52.8 KB
  • 大小: 28.4 KB
  • 大小: 7.1 KB
  • 大小: 10.2 KB
  • 大小: 20.6 KB
  • 大小: 9.2 KB
  • 大小: 8.2 KB
  • 大小: 38 KB
  • 大小: 8.7 KB
  • 大小: 28.3 KB
  • 大小: 55.6 KB
  • bnac.rar (24.5 KB)
  • 描述: dddd
  • 下载次数: 21
分享到:
评论
7 楼 u011076827 2014-02-20  
读您这篇非常收益,谢谢!
同时有个问题不太懂:(图2)  IOloop容器里共有多少个个listen socket? 这多个listen socket 是在什么时候加入的?

收到能回复我一下么,谢谢!

6 楼 zts0813 2013-09-21  

谢楼主.这两天都在看这篇文章!
5 楼 2057 2013-05-22  
山寨比目鱼 写道
第八段的代码示例中
<span style="white-space: normal;"> </span>
这段是?

应该是编辑文本的时候没识别这个标签导致的。
4 楼 山寨比目鱼 2013-05-10  
第八段的代码示例中
<span style="white-space: normal;"> </span>
这段是?
3 楼 2057 2013-03-29  
楼上两位难道是他国人,分析的真好。
2 楼 longbo567 2011-11-24  
Great article!
1 楼 firojre 2011-10-01  
Very nice I am looking for this ,many thanks .

相关推荐

    tornado-1.2.0源码

    通过对Tornado 1.2.0源码的分析,我们可以看到其高效、灵活的设计,这对于理解和优化Web服务器的性能,或者开发基于Tornado的复杂Web应用具有重要的指导意义。学习源码不仅能帮助我们更好地运用这个框架,还能够提升...

    tornado源代码

    - **tornado**: 包含 Tornado 源码的核心模块,如 httpserver.py(HTTP 服务器实现)、websocket.py(WebSocket 支持)、ioloop.py(I/O 循环)等。 - **test**: 测试目录,包含了对 Tornado 各个组件的单元测试,...

    Tornado框架课件资料.rar

    这个"**Tornado框架课件资料.rar**"压缩包包含了深入学习Tornado框架所需的重要资源,包括源码分析、文档资料和操作截图,旨在帮助用户全面理解和掌握Python Web开发以及与数据库的交互。 首先,**04源码.rar**可能...

    股票分析系统源码,基于python,tornado框架.zip

    综上所述,这个基于Python和Tornado框架的股票分析系统源码涉及了多种编程技术和金融分析方法,对于想要学习股票分析软件开发的人来说,这是一个很好的学习资源。开发者可以通过深入研究这个源码,了解如何构建一个...

    Tornado 最新代码

    首先,Tornado的核心特性之一是其非阻塞I/O模型,也称为事件驱动或异步编程。这种模式使得Tornado能够处理大量的并发连接,特别适合于实时Web应用,如聊天服务、流媒体平台或长轮询API。在传统的Web服务器中,每个...

    Tornado 2.2 入门介绍 中文

    - **阅读源码**: 对于更深入的理解,可以研究 Tornado 的源码,了解其实现细节。 总的来说,Tornado 2.2 是一个强大的工具,适用于构建高性能、高并发的Web应用。通过学习和实践,你可以掌握其精髓,为自己的项目...

    Tornado User's Manual

    3. **调试器**:Tornado包含了一个强大的源码级调试器,支持断点设置、变量查看、调用堆栈分析等功能,极大地简化了调试过程。 4. **目标板支持**:Tornado能够与各种硬件平台进行通信,允许在实际硬件上进行远程...

    tornado最新

    这个“tornado最新”压缩包包含了最新的Tornado源码和文档,为开发者提供了一个深入理解并利用Tornado功能的机会。 Tornado最初由FriendFeed开发,后来被Facebook收购并在开源社区中得到广泛发展。它的核心组件包括...

    tornado(python)实现样例代码.

    下面将通过分析提供的样例代码 `tornadoT1`,来深入探讨 Tornado 框架的核心特性。 1. **基础结构** Tornado 应用通常由一系列的 `RequestHandler` 类组成,这些类处理特定的 URL 请求。例如,在 `tornadoT1` 中...

    Tornado使用手册

    - **其他工具**:除了CrossWind,Tornado还包括编译器、构建工具、内存分析工具等,为开发人员提供完整的开发流程支持。 总的来说,Tornado是一个功能全面、适应性强的嵌入式系统开发环境,它简化了在有限资源的...

    tornado-redis:简单的异步 Tornado-redis 连接器

    **项目源码分析** 在提供的压缩包文件`tornado-redis-master`中,包含了Tornado-Redis项目的源代码。通过查看源代码,你可以深入了解其内部实现,包括连接管理、命令处理、错误处理等机制,这对于深入理解和定制...

    PyPI 官网下载 | tornado_pyvows-0.5.2.tar.gz

    **源码分析** 压缩包中的文件通常包括`setup.py`,这是Python项目用来安装的脚本;`MANIFEST.in`用于指定打包时应包含的额外文件;可能还会有`LICENSE`或`README`文件,分别包含了软件的许可信息和使用指南;`...

    PyPI 官网下载 | simple_tornado-0.2.1-py3-none-any.whl

    Tornado 是一个开源的Web服务器和网络库,最初由FriendFeed开发并随后被Facebook收购。Tornado以其非阻塞I/O模型和异步处理能力而闻名,特别适合高性能、高并发的Web应用。它支持Web服务器、WebSocket、HTTP客户端/...

    PyPI 官网下载 | tornado-couchdb-0.2.5.tar.gz

    【PyPI官网下载 | tornado-couchdb-0.2.5.tar.gz】 PyPI(Python Package Index)是Python开发者发布和获取Python软件包的主要平台。这个压缩文件“tornado-couchdb-0.2.5.tar.gz”是从PyPI官网下载的,它包含了名...

Global site tag (gtag.js) - Google Analytics