HTTPRequest,HTTPServer与HTTPConnection

前面小节在分析 handler 时提到,handler 的读写实际是依靠 httprequest 来完成的。今天就分析 tornado 在 HTTP 这一层上的实现,类包括 HTTPRequest, HTTPServer 和 HTTPConnection.

首先,HTTP 协议是建立在面向连接的可靠连接协议 TCP 协议之上,是应用层协议,亦即它的协议内容会涉及网络业务逻辑,而与网络连接处理等底层细节关系不大,因此今天只会提到少量 socket 内容,具体的 TCP 细节留待后面说。http 协议详情查看RFC2616,简言之,http协议约定服务器接收到的内容应该包括三部分,首行,请求头,请求体。首行声明了请求方法,请求 uri,协议版本。请求头是一系列键值对,每行代表一个键值对,建与值间用': '分割。请求体就比较随意了,默认情况是用&连接键值对,也有可能是 RFC1867 里规定的multipart/form-data。先从 HTTPServer 说起吧。

1. HTTPServer

HTTPServer 继承于 TCPServer。它的__init__ 记录了连接到来时的回调函数(http 层次的回调),亦即 application 对象(它的__call__方法会被触发),然后就是父类的初始化了。TCP 服务器细节后面再看,简言之,它可以:它可以监听在特定地址-端口上,并每当有客户端发起连接到服务器时接收该连接,并调用方法 handle_stream(TCP 层次的回调,这个方法总是被子类覆盖,因为只有在这里才可以实现不同应用层协议的业务逻辑)。

HTTPServer 覆盖了父类的 handler_stream 方法,并在该方法里生成 HTTPConnection 对象就结束了。由此可知,HTTPConnection对象被构建就立即开始了 http 协议的处理,这样是合理的,因为 handle_stream 被调用的时候肯定是新连接到来,这时缓冲区里一般有数据可读,当然可以直接读取并处理。

2. HTTPConnection

HTTPConnection 是 HTTP 协议在服务端的真正实现者,我的意思是说,对于请求的读取和解析,基本是由它(依靠HTTPHeaders)完成的。但是响应方面的处理(包括响应头,响应主体的中间处理等)则是在 RequestHandler 里的 flush 方法和 finish 方法里完成的。我把它跳过了,有兴趣的可以自己自己看吧。

HTTPConnection 里的方法,大部分都可以顾名思义,有几个方法需要注意:__init__, _on_headers, _on_request_body. 其它读写之类的方法则直接对应到 IOStream 里,以后再说了。首先是__init__, 它也没干什么,初始化协议参数和回调函数的默认值(一般是None)。然后设定了 header_callback,并开始读取,如下:

# Save stack context here, outside of any request.  This keeps
# contexts from one request from leaking into the next.
self._header_callback = stack_context.wrap(self._on_headers)
self.stream.read_until(b"\r\n\r\n", self._header_callback)

然后这里涉及到两个:stack_context.wrap和stream.read_until。

先是这个wrap函数,它就是一个装饰器,就是封装了一下,对多线程执行环境的上下文做了一些维护。

还有一个就是read_until了,顾名思义,就是一直读取知道\r\n\r\n(这一般意味这请求头的结束),然后调用回调函数_on_headers(这是 IOStream 层次的回调)。具体怎么做的以后再说了,先确认是这么个功能。然后 _on_headers 函数在请求头结束时被调用,它的参数只有一个 data,亦即读取到的请求头字符串。首先是找到起始行:

eol = data.find("\r\n")
start_line = data[:eol]

然后是用空格分解首行来找到方法,uri和协议版本:

method, uri, version = start_line.split(" ")

接着依靠HTTPHeaders解析剩余的请求头,返回一个字典:

headers = httputil.HTTPHeaders.parse(data[eol:])

然后设定 remote_ip(好像没什么用?)。接着创建 request 对象(这就是 RequestHandler 接收的那个 request),然后用 Content-Length 检查是否有请求体,如果没有则直接调用 HTTP 层次的回调(亦即 application 的__call__方法),如果有则读取指定长度的内容并跳到回调 _on_request_body, 当然最终还是会调用 application 对象。在 _on_request_body 方法里是调用 parse_body_arguments方法来完成解析主体,请求头和请求体的解析稍候再说。至此,执行流程就和 Application对象的接口与起到的作用 接上了。至于何时调用handle_stream,后面会说到。

在看解析请求前,简单提一下 HTTPRequest。它是客户端请求的代表,它携带了所有和客户端请求的信息,因为 application 的回调__call__方法只接收 request 参数,当然是把所有信息包在其中。另外,由于服务器只把 request 对象暴露给 application 的回调,因此request 对象还需要提供 write,finish 方法来提供服务,其实就是对 HTTPConnection 对象的封装调用。其它也没什么了。

接下来是关于请求的解析,这和 HTTP 协议的内容密切相关。先看 httpheaders(在httputil.py里)。HTTPHeaders 继承于 dict。它的parse 是一个静态方法,接收字符串参数,内容很简单如下:

h = cls()
for line in headers.splitlines():
if line:
h.parse_line(line)
return h

首先创建一个自身对象,然后把字符串分解成行,对每行调用parse_line,最后返回自身对象。以下是parse_line:

if line[0].isspace():
# continuation of a multi-line header
new_part = ' ' + line.lstrip()
self._as_list[self._last_key][-1] += new_part
dict.__setitem__(self, self._last_key,
self[self._last_key] + new_part)
else:
name, value = line.split(":", 1)
self.add(name, value.strip())

parse_line 方法接收一行字符串,先检查首字符是否为空格。如果不是,则用:分割该行得到键和值,保存即可。如果是空格,说明这一行的内容是从属于上一行,简单的把这一行内容附加到上次键值对的内容里。接下来是关于httputil.parse_body_arguments方法:

def parse_body_arguments(content_type, body, arguments, files):
"""Parses a form request body. Supports ``application/x-www-form-urlencoded`` and
``multipart/form-data``. The ``content_type`` parameter should be
a string and ``body`` should be a byte string. The ``arguments``
and ``files`` parameters are dictionaries that will be updated
with the parsed contents.
"""
if content_type.startswith("application/x-www-form-urlencoded"):
uri_arguments = parse_qs_bytes(native_str(body), keep_blank_values=True)
for name, values in uri_arguments.items():
if values:
arguments.setdefault(name, []).extend(values)
elif content_type.startswith("multipart/form-data"):
fields = content_type.split(";")
for field in fields:
k, sep, v = field.strip().partition("=")
if k == "boundary" and v:
parse_multipart_form_data(utf8(v), body, arguments, files)
break
else:
gen_log.warning("Invalid multipart/form-data")

首先检查conten_type是否以application/x-www-form-urlencoded开头,如果是,说明是用&连接的简单字符串,调用parse_qs_bytes(与urllib.parse.parse_qs等效)来进行解析。而如果是以multipart/form-data开头,则用;分解content_type,目的是找到boundary,当找到了boundary时(这时一般content_type也到尽头了),就调用parse_multipart_data对请求体进行解析。接下来看这个函数:

def parse_multipart_form_data(boundary, data, arguments, files):
"""Parses a ``multipart/form-data`` body. The ``boundary`` and ``data`` parameters are both byte strings.
The dictionaries given in the arguments and files parameters
will be updated with the contents of the body.
"""
# The standard allows for the boundary to be quoted in the header,
# although it's rare (it happens at least for google app engine
# xmpp). I think we're also supposed to handle backslash-escapes
# here but I'll save that until we see a client that uses them
# in the wild.
if boundary.startswith(b'"') and boundary.endswith(b'"'):
boundary = boundary[1:-1]
final_boundary_index = data.rfind(b"--" + boundary + b"--")
if final_boundary_index == -1:
gen_log.warning("Invalid multipart/form-data: no final boundary")
return
parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
for part in parts:
if not part:
continue
eoh = part.find(b"\r\n\r\n")
if eoh == -1:
gen_log.warning("multipart/form-data missing headers")
continue
headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
disp_header = headers.get("Content-Disposition", "")
disposition, disp_params = _parse_header(disp_header)
if disposition != "form-data" or not part.endswith(b"\r\n"):
gen_log.warning("Invalid multipart/form-data")
continue
value = part[eoh + 4:-2]
if not disp_params.get("name"):
gen_log.warning("multipart/form-data value missing name")
continue
name = disp_params["name"]
if disp_params.get("filename"):
ctype = headers.get("Content-Type", "application/unknown")
files.setdefault(name, []).append(HTTPFile(
filename=disp_params["filename"], body=value,
content_type=ctype))
else:
arguments.setdefault(name, []).append(value)

首先,保证边界 boundary 没被“”包裹。然后用b"--" + boundary + b"--"找到请求体的结束位置。然后在开头与结束位置间用分隔符b"--" + boundary + b"\r\n"把请求体分割成多个类似部分。然后对于每一部分循环进行如下处理:用b"\r\n\r\n"分隔元描述和实际内容,在元描述里可以找到 Content-Disposition, Content-type, name, filename 等信息,name 一般成为标识该部分内容的键,在字典里存储该内容。详细细节请参看RFC1867

HTTPServer与Request处理流程

TCPServer.bind_sockets()会返回一个socket对象的列表,列表中的socket都是用来监听客户端连接的。

列表由TCPServer.add_sockets()处理。在这个函数里我们就会看到IOLoop相关的东西。

def add_sockets(self, sockets):
if self.io_loop is None:
self.io_loop = IOLoop.current() for sock in sockets:
self._sockets[sock.fileno()] = sock
add_accept_handler(sock, self._handle_connection, io_loop=self.io_loop)

首先,io_loop是TCPServer的一个成员变量,这说明每个TCPServer都绑定了一个io_loop。注意,跟传统的做法不同,ioloop不是跟socket一一对应,而是跟TCPServer一一对应。也就是说,一个Server上即使有多个listening socket,他们也是由同一个ioloop在处理。

前面提到过,HTTPServer的初始化可以带一个ioloop参数,最终它会被赋值给TCPServer的成员。如果没有带ioloop参数(如helloworld.py所展示的),TCPServer则会自己倒腾一个,即IOLoop.current()。

add_accept_handler()定义在netutil.py中(bind_sockets在这里)。它的代码并复杂,如下:

def add_accept_handler(sock, callback, io_loop=None):
if io_loop is None:
io_loop = IOLoop.current()
def accept_handler(fd, events):
while True:
try:
connection, address = sock.accept()
except socket.error as e:
# EWOULDBLOCK and EAGAIN indicate we have accepted every
# connection that is available.
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
return
# ECONNABORTED indicates that there was a connection
# but it was closed while still in the accept queue.
# (observed on FreeBSD).
if e.args[0] == errno.ECONNABORTED:
continue
raise
callback(connection, address)
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)

文档中说,IOLoop.current()返回当前线程的IOLoop对象。可能有点不好理解。

实际上IOLoop的实例就相当于一个线程,有start, run, stop这些函数。IOLoop实例都包含一个叫_current的成员,指向创建它的线程。每个线程在创建IOLoop时,都被绑定到创建它的线程上。在IOLoop的类定义中,有这么一行:

_current = threading.local()

创建好IOLoop后,下面又定义了一个accept_handler。这是一个函数内定义的函数。

accept_handler相当于连接监视器的,所以我们把它绑定到listening socket上。

io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)

它正式对socket句柄调用 accept。当接收到一个connection后,调用callback()。不难想到,callback就是客户端连上来后对应的响应函数。

回到add_sockets函数里看:

add_accept_handler(sock, self._handle_connection,
io_loop=self.io_loop)

callback就是我们传进去的TCPServer._handle_connection()函数。TCPServer._handle_connection()本身不复杂。前面大段都是在处理SSL事务,把这些东西都滤掉的话,实际上代码很少。

def _handle_connection(self, connection, address):
try:
stream = IOStream(connection, io_loop=self.io_loop, max_buffer_size=self.max_buffer_size)
self.handle_stream(stream, address)
except Exception:
app_log.error("Error in connection callback", exc_info=True)

思路是很清晰的,客户端连接在这里被转化成一个IOStream。然后由handle_stream函数处理。

这个handle_stream就是我们前面提到过的未直接实现的接口,它是由HTTPServer类实现的。

def handle_stream(self, stream, address):
HTTPConnection(stream, address, self.request_callback,
self.no_keep_alive, self.xheaders, self.protocol)

最后,处理流程又回到了HTTPServer类中。可以预见,在HTTConnection这个类中,stream将和我们注册的RequestHandler协作,一边读客户端请求,一边调用相应的handler处理。

总结一下:

listening socket创建起来以后,我们给它绑定一个响应函数叫accept_handler。当用户连接上来时,会触发listening socket上的事件,然后accept_handler被调用。accept_handler在listening socket上获得一个针对该用户的新socket。这个socket专门用来跟用户做数据沟通。TCPServer把这个socket封闭成一个IOStream,最后交到HTTPServer的handle_stream里。

经过一翻周围,用户连接后的HTTP通讯终于被我们导入到了HTTPConnection。在HTTPConnection里我们将看到熟悉的HTTP通信协议的处理。

HTTPConnection类也定义在httpserver.py中,它的构造函数如下:

def __init__(self, stream, address, request_callback, no_keep_alive=False,
xheaders=False, protocol=None):
self.stream = stream
self.address = address
# Save the socket's address family now so we know how to
# interpret self.address even after the stream is closed
# and its socket attribute replaced with None.
self.address_family = stream.socket.family
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self.protocol = protocol
self._clear_request_state()
# Save stack context here, outside of any request. This keeps
# contexts from one request from leaking into the next.
self._header_callback = stack_context.wrap(self._on_headers)
self.stream.set_close_callback(self._on_connection_close)
self.stream.read_until(b"\r\n\r\n", self._header_callback)

常规的参数保存动作,还有一些初始化、清理动作,最后一句开始办正事:

从socket中读数据,直到读到”\r\n\r\n”为止。这是HTTP头部结束标志,读到的数据就会由self._header_callback处理。

经过stack_context.wrap()的传递,HTTP头会交给HTTPConnection._on_headers()。

HTTPConnection._on_headers()完成了HTTP头的分析,具体过程这里就不必详述了(如果有写HTTPServer需求,倒是可以借鉴一下)。

经过一轮校验与分析,HTTP头(注意,只是HTTP头部哦)被组装成一个HTTPRequest对象。(HTTP Body如果数据量不是太大,就直接放进了一个buffer里,就叫self._on_request_body。)

self._request = HTTPRequest(
connection=self, method=method, uri=uri, version=version,
headers=headers, remote_ip=remote_ip, protocol=self.protocol)

HTTPRequest对象被交给了(其实HTTP Body最后也是交给他的……)

self.request_callback(self._request)

这个request_callback是什么来头呢?它是在HTTPConnection构造时传进来的参数。我们回到HTTPServer.handle_stream()

def handle_stream(self, stream, address):
HTTPConnection(stream, address, self.request_callback,
self.no_keep_alive, self.xheaders, self.protocol)

它是一个HTTPServer类的成员,继续往回追来历:

def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
xheaders=False, ssl_options=None, protocol=None, **kwargs):
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self.protocol = protocol
TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
**kwargs)

Bingo!这就是HTTPServer初始化时传进来的那个RequestHandler。

在helloworld.py里,我们看到的是:

application = tornado.web.Application([(r"/", MainHandler), ])
http_server = tornado.httpserver.HTTPServer(application)

在另一个例子里,我们看到的是:

def handle_request(request):
message = "You requested %s\n" % request.uri
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) 

可见这个request_handler通吃很多种类型的参数,可以是一个Application类的对象,也可是一个简单的函数。

如果是handler是简单函数,如上面的handle_request,这个很好理解,由一个函数处理HTTPRequest对象嘛。

如果是一个Application对象,就有点奇怪了。我们能把一个对象作另一个对象的参数来呼叫吗?

Python中有一个有趣的语法,只要定义类型的时候,实现__call__函数,这个类型就成为可调用的。换句话说,我们可以把这个类的对象当作函数来使用,相当于重载了括号运算符。

至此,HTTP 层内容终于看完了。明天继续看TCP层。

最新文章

  1. [CareerCup] 2.5 Add Two Numbers 两个数字相加
  2. TinyFrame升级之一:框架概览
  3. LintCode Maximum Depth of Binary Tree
  4. LeetCode() Search a 2D MatrixII
  5. ThreadPool for Delphi
  6. [转] 8张图学习javascript
  7. linux中服务器定时程序设定
  8. 我只是想获取access_token而已
  9. 5分钟spark streaming实践之 与kafka联姻
  10. Salt: Master server cannot see any Minion
  11. Quartz.Net进阶之六:详述 JobStores
  12. 利用maven/eclipse搭建ssm(spring+spring mvc+mybatis)
  13. canvas学习总结四:绘制虚线
  14. loadrunner 事务、同步点和思考时间
  15. ajax请求成功 但是被error拦截
  16. HTTP协议工作原理
  17. 20170805_Elasticsearch_简介和安装
  18. java监控工具VisualVM
  19. Linux Ubuntu16.04LTS安装TensorFlow(CPU-only,python3.7)——使用Anaconda安装
  20. mysql BLOB字段转String的方法

热门文章

  1. Vector类与Enumeration接口
  2. redis php 实例二
  3. iOS开发之--制作属于自己的frameWork
  4. 将BT转为磁力链接
  5. Linux下自动调整时间和时区与Internet时间同步
  6. [MySQL] 变量(参数)的查看和设置 [转]
  7. xmpp muc 群聊协议 3
  8. 160620、利用 jQuery UI 和 Ajax 创建可定制的 Web 界面
  9. 创建Json
  10. ZOJ 3331 Process the Tasks(双塔DP)