Tornado可以当做一个http客户端去发送请求,可以处理需要和客户端建立长连接的请求。可是Tornado最为知名的还是它作为一个HTTP web框架…,上一篇讲述了IOLoop的套路。本篇讲解一下如何将IOLoop和httpserver联系起来(本代码思路依据1.0.0版本)
目标 本文的基本目标是实现这个需求
1 2 3 4 5 6 7 8 9 10 11 12 from  tornado import  httpserverfrom  tornado import  ioloopdef  handle_request (request ):    message = b"Hello World\n"      request.write(b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\n\r\n%s"  % (         len (message), message))     request.finish() http_server = httpserver.HTTPServer(handle_request) http_server.listen(8888 ) ioloop.IOLoop.instance().start() 
最简服务端 根据上一篇。我们可以想到当执行listen监听操作的时候,会创建一个监听socket.然后将处理函数添加到事件回调中。不考虑异常因素,最简单的是这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import  socketfrom  tornado import  ioloopfrom  selectors import  EVENT_READ, EVENT_WRITEclass  HTTPServer :    def  __init__ (self, handler ):         self.handler = handler         self.io_loop = ioloop.IOLoop.instance()     def  listen (self, port ):         self.s = socket.socket()         self.s.setblocking(0 )         self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )         self.s.bind(('127.0.0.1' , port))         self.s.listen(128 )         self.io_loop.add_handler(self.s.fileno(), EVENT_READ, self._handle)     def  _handle (self ):         client_sock, _ = self.s.accept()         client_sock.send(b"HTTP/1.0 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n" )         client_sock.close() 
对于监听socket,当触发可读的时候意味有新的客户端连接进来了,此时调用回调进行accept得到交互socket连接。简单起见直接send发送一个简易的HTTP消息然后关闭socket
封装Request对象 从上面的示例中可以看到。它没有调用传入的request函数,因为socket对象并没有write和finish方法。此外,即使我们将socket的send和close当做write和finish。它还有一个巨大的缺陷。我们只是进行了发送,而没有对接收到的http报文进行任何处理。
1 2 3 4 5 6 7 GET /docs/index.html HTTP/1.1 Host: www.ipinfo.com Accept: image/gif, image/jpeg, */* Accept-Language: en-us Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1) (blank line) 
获取流程如下。当读取到第一个\r\n即确定Reques Line部分,当读取到\r\n\r\n即确定Header部分,如果Header部分存在Content-Length则表示存在body部分,存在则继续读取Content-Length长度。如此下来单个HTTP报文读取完毕。可以构建这个过程,Requests Line读取完毕后—>回调读取Header部分—>回调读取Body部分—>构建Request对象—>回调handler函数,将Request对象传入。另外第一步可以省略,读取Header即包含Requests Line
可以看到对于一个socket对象,将它根据分隔符以及长度读取就能解析成单个http包,另外对于很多流协议也是如此。我们将socket封装一下。让它提供两个功能read_until、read_bytes。允许读取到分隔符后调用回调函数、读取到固定长度后调用回调函数。最简代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from  tornado import  ioloopfrom  selectors import  EVENT_READclass  IOStream :    def  __init__ (self, socket ):         self.socket = socket         self.ioloop = ioloop.IOLoop.instance()         self.ioloop.add_handler(self.socket.fileno(), EVENT_READ, self._handler)         self._read_buffer = b""          self._read_delimiter = None          self._read_bytes = None          self._read_callback = None      def  _handler (self ):         data = self.socket.recv(1024 )         self._read_buffer += data         if  self._read_delimiter and  self._read_delimiter in  self._read_buffer:             result, self._read_buffer = self._read_buffer.split(self._read_delimiter)             self._read_callback(result)             self._read_delimiter = None              self._read_callback = None          elif  self._read_bytes and  len (self._read_buffer) > self._read_bytes:             result = self._read_buffer[:self._read_bytes]             self._read_buffer = self._read_buffer[self._read_bytes:]             self._read_callback(result)             self._read_bytes = None              self._read_callback = None      def  read_until (self, delimiter, callback ):         self._read_delimiter = delimiter         self._read_callback = callback     def  read_bytes (self, length, callback ):         self._read_bytes = length         self._read_callback = callback     def  write (self, data ):         self.socket.send(data)     def  close (self ):         self.ioloop.ioloop.unregister(self.socket.fileno())         self.socket.close() 
对于创建的IOStream对象,它添加READ事件,将回调函数设置为self._handler。当使用read_until的时候将历史数据给callback函数调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class  HTTPConnection :    def  __init__ (self, io_stream, handler_callback ):         self.io_loop = ioloop.IOLoop.instance()         self.stream = io_stream         self.handler_callback = handler_callback         self.stream.read_until(b'\r\n\r\n' , self._parse_header)     def  _parse_header (self, data ):                  request = Request(connect=self, header=data)         self.handler_callback(request)     def  write (self, data ):         self.stream.write(data)     def  finish (self ):         self.stream.close() 
显然,它传入的是IOStream对象,并且在初始化的时候就调用read_until读取HTTP Header部分内容。简单起见,这里只读取并没有解析。最后封装成Request对象。让handle_request函数调用。达到我们文章开头的目的
1 2 3 4 5 6 7 8 9 10 class  Request :    def  __init__ (self, connect, header ):         self.connect = connect         self.header = header     def  write (self, data ):         self.connect.write(data)     def  finish (self ):         self.connect.finish() 
将最开始的最简服务器部分稍微改动一下
1 2 3 4 def  _handle (self ):    client_sock, _ = self.s.accept()     stream = IOStream(client_sock)     HTTPConnection(stream, self.handler) 
小结 IOStream、HTTPConnection、Request、HTTPServer。其中暴漏给用户的几乎只有Request对象,从上面的分析中应该可以发现。将文件描述符加入事件,等待IOStream的全局_header进行回调,全局_header又针对各种情况(分隔符条件满足、长度条件满足)发起回调。同时如果不调用iostream.read_until等那么全局的_header就没有意义了 iostream.read_until。被回调则表明HTTP头部接收完成。组装成Requests然后调用handle_request。
作用分工:且可以传入回调 
另外本文简单起见,只注册了READ事件。对于send操作。同样是需要进行注册事件。等到WRITE事件触发才执行send操作,希望不要引起误解,另外本例中使用HTTP/1.0的逻辑,处理完成单个链接后直接关闭。对于HTTP/1.1来说要实现keep-alive只需要在finish的使用并不关闭链接,而是继续执行self.read_until(b’\r\n\r\n’,self._parse_header)继续等待处理下一个HTTP报文