python tornado TCPserver异步协程实例

内容预览:

  • 它的值包括四种:AF_UNIX,AF_INET,AF_INET6和AF_UNSPEC~
  • # 相反,我们使用listen backlog作为我们可以合理接受的连接数的~
  • if errno_from_exception(e) == errno.ECONNABORTED: continue raise ca…~

项目所用知识点

  • tornado
  • socket
  • tcpserver
  • 协程
  • 异步

tornado tcpserver源码抛析

在tornado的tcpserver文件中,实现了TCPServer这个类,他是一个单线程的,非阻塞的tcp 服务。

为了与上层协议(在tornado中就是HTTPServer)交互,TCPServer提供了一个接口:handle_stream, 要求其子类必需实现该方法,该方法就是主要用来处理应用层逻辑的。

我们可以通过下面代码倒入模块查看源码

from tornado.tcpserver import TCPServer

源码中给了好多解释,先把源码注释贴进来

class TCPServer(object):

    ‘’‘

    1.要想用TCPserver,我给你提供你一个接口handle_stream,子类中必须要有这个方法

    2.他已经给我们举出了例子

    3.往下他给咱们介绍了几种启动方法,而我用的listen()方法启动看起来简单明了

    ’‘’

    r"""A non-blocking, single-threaded TCP server.



    To use `TCPServer`, define a subclass which overrides the `handle_stream`

    method. For example, a simple echo server could be defined like this::



      from tornado.tcpserver import TCPServer

      from tornado.iostream import StreamClosedError

      from tornado import gen



      class EchoServer(TCPServer):

          @gen.coroutine

          def handle_stream(self, stream, address):

              while True:

                  try:

                      data = yield stream.read_until(b"n")

                      yield stream.write(data)

                  except StreamClosedError:

                      break



    To make this server serve SSL traffic, send the ``ssl_options`` keyword

    argument with an `ssl.SSLContext` object. For compatibility with older

    versions of Python ``ssl_options`` may also be a dictionary of keyword

    arguments for the `ssl.wrap_socket` method.::



       ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

       ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"),

                               os.path.join(data_dir, "mydomain.key"))

       TCPServer(ssl_options=ssl_ctx)



    `TCPServer` initialization follows one of three patterns:



    1. `listen`: simple single-process::



            server = TCPServer()

            server.listen(8888)

            IOLoop.current().start()



    2. `bind`/`start`: simple multi-process::



            server = TCPServer()

            server.bind(8888)

            server.start(0)  # Forks multiple sub-processes

            IOLoop.current().start()



       When using this interface, an `.IOLoop` must *not* be passed

       to the `TCPServer` constructor.  `start` will always start

       the server on the default singleton `.IOLoop`.



    3. `add_sockets`: advanced multi-process::



            sockets = bind_sockets(8888)

            tornado.process.fork_processes(0)

            server = TCPServer()

            server.add_sockets(sockets)

            IOLoop.current().start()



       The `add_sockets` interface is more complicated, but it can be

       used with `tornado.process.fork_processes` to give you more

       flexibility in when the fork happens.  `add_sockets` can

       also be used in single-process servers if you want to create

       your listening sockets in some way other than

       `~tornado.netutil.bind_sockets`.



    .. versionadded:: 3.1

       The ``max_buffer_size`` argument.



    .. versionchanged:: 5.0

       The ``io_loop`` argument has been removed.

    """

自己仔细看该类的其他方法

    def listen(self, port, address=""):

        """Starts accepting connections on the given port.



        This method may be called more than once to listen on multiple ports.

        `listen` takes effect immediately; it is not necessary to call

        `TCPServer.start` afterwards.  It is, however, necessary to start

        the `.IOLoop`.

        """

        sockets = bind_sockets(port, address=address)

        self.add_sockets(sockets)



    def add_sockets(self, sockets):

        """Makes this server start accepting connections on the given sockets.



        The ``sockets`` parameter is a list of socket objects such as

        those returned by `~tornado.netutil.bind_sockets`.

        `add_sockets` is typically used in combination with that

        method and `tornado.process.fork_processes` to provide greater

        control over the initialization of a multi-process server.

        """

        for sock in sockets:

            self._sockets[sock.fileno()] = sock

            self._handlers[sock.fileno()] = add_accept_handler(

                sock, self._handle_connection)



    def add_socket(self, socket):

        """Singular version of `add_sockets`.  Takes a single socket object."""

        self.add_sockets([socket])



    def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128,

             reuse_port=False):

        """Binds this server to the given port on the given address.



        To start the server, call `start`. If you want to run this server

        in a single process, you can call `listen` as a shortcut to the

        sequence of `bind` and `start` calls.



        Address may be either an IP address or hostname.  If it's a hostname,

        the server will listen on all IP addresses associated with the

        name.  Address may be an empty string or None to listen on all

        available interfaces.  Family may be set to either `socket.AF_INET`

        or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise

        both will be used if available.



        The ``backlog`` argument has the same meaning as for

        `socket.listen <socket.socket.listen>`. The ``reuse_port`` argument

        has the same meaning as for `.bind_sockets`.



        This method may be called multiple times prior to `start` to listen

        on multiple ports or interfaces.



        .. versionchanged:: 4.4

           Added the ``reuse_port`` argument.

        """

        sockets = bind_sockets(port, address=address, family=family,

                               backlog=backlog, reuse_port=reuse_port)

        if self._started:

            self.add_sockets(sockets)

        else:

            self._pending_sockets.extend(sockets)



    def start(self, num_processes=1):

        """Starts this server in the `.IOLoop`.



        By default, we run the server in this process and do not fork any

        additional child process.



        If num_processes is ``None`` or <= 0, we detect the number of cores

        available on this machine and fork that number of child

        processes. If num_processes is given and > 1, we fork that

        specific number of sub-processes.



        Since we use processes and not threads, there is no shared memory

        between any server code.



        Note that multiple processes are not compatible with the autoreload

        module (or the ``autoreload=True`` option to `tornado.web.Application`

        which defaults to True when ``debug=True``).

        When using multiple processes, no IOLoops can be created or

        referenced until after the call to ``TCPServer.start(n)``.

        """

        assert not self._started

        self._started = True

        if num_processes != 1:

            process.fork_processes(num_processes)

        sockets = self._pending_sockets

        self._pending_sockets = []

        self.add_sockets(sockets)



    def stop(self):

        """Stops listening for new connections.



        Requests currently in progress may still continue after the

        server is stopped.

        """

        if self._stopped:

            return

        self._stopped = True

        for fd, sock in self._sockets.items():

            assert sock.fileno() == fd

            # Unregister socket from IOLoop

            self._handlers.pop(fd)()

            sock.close()



    def handle_stream(self, stream, address):

        """Override to handle a new `.IOStream` from an incoming connection.



        This method may be a coroutine; if so any exceptions it raises

        asynchronously will be logged. Accepting of incoming connections

        will not be blocked by this coroutine.



        If this `TCPServer` is configured for SSL, ``handle_stream``

        may be called before the SSL handshake has completed. Use

        `.SSLIOStream.wait_for_handshake` if you need to verify the client's

        certificate or use NPN/ALPN.



        .. versionchanged:: 4.2

           Added the option for this method to be a coroutine.

        """

        raise NotImplementedError()



    def _handle_connection(self, connection, address):

        if self.ssl_options is not None:

            assert ssl, "Python 2.6+ and OpenSSL required for SSL"

            try:

                connection = ssl_wrap_socket(connection,

                                             self.ssl_options,

                                             server_side=True,

                                             do_handshake_on_connect=False)

            except ssl.SSLError as err:

                if err.args[0] == ssl.SSL_ERROR_EOF:

                    return connection.close()

                else:

                    raise

            except socket.error as err:

                # If the connection is closed immediately after it is created

                # (as in a port scan), we can get one of several errors.

                # wrap_socket makes an internal call to getpeername,

                # which may return either EINVAL (Mac OS X) or ENOTCONN

                # (Linux).  If it returns ENOTCONN, this error is

                # silently swallowed by the ssl module, so we need to

                # catch another error later on (AttributeError in

                # SSLIOStream._do_ssl_handshake).

                # To test this behavior, try nmap with the -sT flag.

                # https://github.com/tornadoweb/tornado/pull/750

                if errno_from_exception(err) in (errno.ECONNABORTED, errno.EINVAL):

                    return connection.close()

                else:

                    raise

        try:

            if self.ssl_options is not None:

                stream = SSLIOStream(connection,

                                     max_buffer_size=self.max_buffer_size,

                                     read_chunk_size=self.read_chunk_size)

            else:

                stream = IOStream(connection,

                                  max_buffer_size=self.max_buffer_size,

                                  read_chunk_size=self.read_chunk_size)



            future = self.handle_stream(stream, address)

            if future is not None:

                IOLoop.current().add_future(gen.convert_yielded(future),

                                            lambda f: f.result())

        except Exception:

            app_log.error("Error in connection callback", exc_info=True)

通过方法名就能看出来,而且开头已经给出实例怎么去用,所以这个就不一一解释了,我自己的用法如下

from tornado.tcpserver import TCPServer

from tornado.iostream import IOStream, StreamClosedError

from tornado import gen

from tornado.ioloop import IOLoop

import struct



class ProxyServer(TCPServer):

    def __init__(self, *args, **kwargs):

        super(ProxyServer, self).__init__(*args, **kwargs)

        self.devices = dict()



    @gen.coroutine

    def handle_stream(self, stream, address):

          pass





if __name__ == "__main__":

    server = ProxyServer()

    server.listen(1234)

    IOLoop.current().start()

具体步骤来分析 一下

TCPServer执行过程

1.server = ProxyServer()创建tcpserver对象,该步骤仅仅做了一个初始化操作

def __init__(self, io_loop=None, ssl_options=None, max_buffer_size=None, read_chunk_size=None):

        self.io_loop = io_loop

        self.ssl_options = ssl_options

        self._sockets = {}  # fd -> socket object    用来存储文件描述符与socket对象的映射关系

        self._pending_sockets = []

        self._started = False

        self.max_buffer_size = max_buffer_size    # 最大缓冲长度

        self.read_chunk_size = read_chunk_size    # 每次读的chunk大小



        # 校验ssl选项. 

        if self.ssl_options is not None and isinstance(self.ssl_options, dict):

            if 'certfile' not in self.ssl_options:

                raise KeyError('missing key "certfile" in ssl_options')



            if not os.path.exists(self.ssl_options['certfile']):

                raise ValueError('certfile "%s" does not exist' % self.ssl_options['certfile'])

            if ('keyfile' in self.ssl_options and not os.path.exists(self.ssl_options['keyfile'])):

                raise ValueError('keyfile "%s" does not exist' % self.ssl_options['keyfile'])

2.想都不要想肯定是开启socket

步骤是执行server.listen(1234)的时候,

    def listen(self, port, address=""):

        """Starts accepting connections on the given port.



        This method may be called more than once to listen on multiple ports.

        `listen` takes effect immediately; it is not necessary to call

        `TCPServer.start` afterwards.  It is, however, necessary to start

        the `.IOLoop`.

        """

        sockets = bind_sockets(port, address=address)

        self.add_sockets(sockets)

3.看下listen里面有调用bind_sockets()方法,来看下该方法

def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=_DEFAULT_BACKLOG, flags=None, reuse_port=False):

    if reuse_port and not hasattr(socket, "SO_REUSEPORT"):

        raise ValueError("the platform doesn't support SO_REUSEPORT")



    sockets = []

    if address == "":

        address = None

    # address family参数指定调用者期待返回的套接口地址结构的类型。它的值包括四种:AF_UNIX,AF_INET,AF_INET6和AF_UNSPEC。

    # AF_UNIX用于同一台机器上的进程间通信

    # 如果指定AF_INET,那么函数就不能返回任何IPV6相关的地址信息;如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。

    # AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。

    # 如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回

    if not socket.has_ipv6 and family == socket.AF_UNSPEC: # 如果系统不支持ipv6

        family = socket.AF_INET

    if flags is None:

        flags = socket.AI_PASSIVE

    bound_port = None

    for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags)):

        af, socktype, proto, canonname, sockaddr = res

        if (sys.platform == 'darwin' and address == 'localhost' and af == socket.AF_INET6 and sockaddr[3] != 0):

            # Mac OS X在“localhost”的getaddrinfo结果中包含一个链接本地地址fe80 :: 1%lo0。 

            # 但是,防火墙不了解这是一个本地地址,并且会提示访问。 所以跳过这些地址。

            continue

        try:

            sock = socket.socket(af, socktype, proto)

        except socket.error as e:

            # 如果协议不支持该地址

            if errno_from_exception(e) == errno.EAFNOSUPPORT:

                continue

            raise

        # 为 fd 设置 FD_CLOEXEC 标识

        set_close_exec(sock.fileno())

        if os.name != 'nt': # 非windows

            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        if reuse_port:

            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

        if af == socket.AF_INET6:

            # On linux, ipv6 sockets accept ipv4 too by default,

            # but this makes it impossible to bind to both

            # 0.0.0.0 in ipv4 and :: in ipv6.  On other systems,

            # separate sockets *must* be used to listen for both ipv4

            # and ipv6.  For consistency, always disable ipv4 on our

            # ipv6 sockets and use a separate ipv4 socket when needed.

            #

            # Python 2.x on windows doesn't have IPPROTO_IPV6.

            if hasattr(socket, "IPPROTO_IPV6"):

                sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)



        # 自动端口分配,端口=None

        # 应该绑定在IPv4和IPv6地址上的同一个端口上

        host, requested_port = sockaddr[:2]

        if requested_port == 0 and bound_port is not None:

            sockaddr = tuple([host, bound_port] + list(sockaddr[2:]))

        # 设置socket为非阻塞

        sock.setblocking(0)    

        sock.bind(sockaddr)

        bound_port = sock.getsockname()[1]

        sock.listen(backlog)

        sockets.append(sock)

    return sockets

4.接下来执行的是add_sockets()方法

def add_sockets(self, sockets):

        if self.io_loop is None:

            self.io_loop = IOLoop.current()    # 获取IOLoop实例对象



        for sock in sockets:

            self._sockets[sock.fileno()] = sock

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

可以看到里面调用了add_accept_handler方法,来我们进去看看该方法干啥了

5.探析add_accept_handler方法

def add_accept_handler(sock, callback, io_loop=None):

    if io_loop is None: # 获取IOLoop实例对象

        io_loop = IOLoop.current()



    def accept_handler(fd, events):

        # 我们处理回调时可能会有许多的连接等待建立; 为了防止其他任务的饥饿,我们必须限制我们一次接受的连接数。 

        # 理想情况下,我们接受在处理回调过程中等待的连接数,但此可能会对负载产生不利影响。 

        # 相反,我们使用listen backlog作为我们可以合理接受的连接数的。

        for i in xrange(_DEFAULT_BACKLOG): # _DEFAULT_BACKLOG默认为128

            try:

                connection, address = sock.accept()

            except socket.error as e:

                # _ERRNO_WOULDBLOCK 与EAGAIN相同,表示再尝试一下,很多情况下是因为资源不足,或者条件未达成

                # 当某个子进程与客户端建立了连接,其他子进程再次尝试与该客户端建立连接时就会产生该错误

                if errno_from_exception(e) in _ERRNO_WOULDBLOCK:

                    return

                # ECONNABORTED表示有一个连接,在他处于等待被服务端accept的时候主动关闭了。

                if errno_from_exception(e) == errno.ECONNABORTED:

                    continue

                raise

            callback(connection, address)

    io_loop.add_handler(sock, accept_handler, IOLoop.READ) # 为socket注册handler:当发生READ事件时运行accept_handler函数。

欣欣然我们来到了最后一步

6.IOLoop.current().start(),然我们看下源码

def start(self):

        try:

            while True:    

                callbacks = self._callbacks

                self._callbacks = []

                due_timeouts = []

                # 将时间已到的定时任务放置到due_timeouts中,过程省略

                for callback in callbacks:          # 执行callback

                    self._run_callback(callback)

                for timeout in due_timeouts:        # 执行定时任务

                    if timeout.callback is not None:

                        self._run_callback(timeout.callback)       

                callbacks = callback = due_timeouts = timeout = None    # 释放内存

                # 根据情况设置poll_timeout的值,过程省略

                if not self._running:    # 终止ioloop运行时,在执行完了callback后结束循环

                    breaktry:

                    event_pairs = self._impl.poll(poll_timeout)

                except Exception as e:

                    if errno_from_exception(e) == errno.EINTR:  # 系统调用被信号处理函数中断,进行下一次循环

                        continue

                    else:

                        raise 

                self._events.update(event_pairs)

                while self._events: 

                    fd, events = self._events.popitem()             # 获取一个fd以及对应事件

                    try:

                        fd_obj, handler_func = self._handlers[fd]   # 获取该fd对应的事件处理函数

                        handler_func(fd_obj, events)                # 运行该事件处理函数

                    except (OSError, IOError) as e:         

                        if errno_from_exception(e) == errno.EPIPE:     # 当客户端关闭连接时会产生EPIPE错误                         

                            pass

                        # 其他异常处理已经省略

                fd_obj = handler_func = None       # 释放内存空间    

这一步想了解更多去参考这篇文章http://www.cnblogs.com/MnCu8261/p/6730691.html

代码实例

目前公司有这么一个需求,iphonex–server–ue4,面对两个客户端,达到iphonex把数据给ue4,server起一个代理作用,需求大概就是这样,具体实现代码如下

from tornado.tcpserver import TCPServer

from tornado.iostream import IOStream, StreamClosedError

from tornado import gen

from tornado.ioloop import IOLoop

import struct



class ProxyServer(TCPServer):

    def __init__(self, *args, **kwargs):

        super(ProxyServer, self).__init__(*args, **kwargs)

        self.devices = dict()



    @gen.coroutine

    def handle_stream(self, stream, address):

        device = yield stream.read_bytes(1)

        if device == b"x0a":

            self.handle_iphonex_stream(stream, address)

        elif device == b"x0b":

            self.handle_ue4_stream(stream, address)

        else:

            print("protocol error.")



    @gen.coroutine

    def handle_iphonex_stream(self, stream, address):

        yield stream.write(b"x00")

        print("iphonex")



        # uuid

        rlen = yield stream.read_bytes(4)

        rlen = struct.unpack(">I", rlen)[0]

        uuid = yield stream.read_bytes(rlen)

        uuid = uuid.decode()

        yield stream.write(b"x00")

        print(uuid)



        # keys

        rlen = yield stream.read_bytes(4)

        rlen = struct.unpack(">I", rlen)[0]

        keys = yield stream.read_bytes(rlen)

        keys = keys.decode()

        yield stream.write(b"x00")

        print(keys)



        # save

        self.devices[uuid] = {'keys': keys}



        # data

        keys = keys.split(',')

        fmt = "%df" % len(keys)



        while True:

            try:

                data = yield stream.read_bytes(struct.calcsize(fmt))

            except StreamClosedError:

                print 'iphonex is closed'

                break

            pdata = struct.unpack(fmt, data)

            print(pdata)



            ue4stream = self.devices[uuid].get('ue4')

            if ue4stream:

                try:

                    yield ue4stream.write(data)

                except Exception as e:

                    self.devices[uuid]['ue4'] = None

                    print('request for %s closed' % uuid)



    @gen.coroutine

    def handle_ue4_stream(self, stream, address):

        yield stream.write(b"x00")

        print("ue4")



        # uuid

        rlen = yield stream.read_bytes(4)

        rlen = struct.unpack(">I", rlen)[0]

        uuid = yield stream.read_bytes(rlen)

        uuid = uuid.decode()

        print(uuid)



        if self.devices.get(uuid):

            yield stream.write(b"x00")

        else:

            yield stream.write(b"x01")

            raise Exception



        # send keys

        keys = self.devices[uuid].get('keys')

        stream.write(struct.pack(">I", len(keys)))

        stream.write(keys.encode())



        valid = yield stream.read_bytes(1)

        if valid == b'x

tornado 3.0.1 中关于cookie和secure cookie的那点事儿

cookie是附加在http请求中的,tornado默认的set_cookie和get_cookie方法是明文不加密传输的,而set_secure_cookie和get_secure_cookie是明文加密传输的。代码见下:

import tornado.web
import tornado.ioloop

from tornado.options import define,options,parse_command_line
define('port',default=8888,help='run on the port',type=int)

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        if not self.get_secure_cookie('username'):
            self.set_secure_cookie('username','littlethunder')
            self.write('just set your cookie')
        else:
            a=self.get_secure_cookie('username')
            print(a)
            self.write('you have cookie before')

def main():
    parse_command_line()
    app=tornado.web.Application(
            [(r'/',MainHandler)],
                cookie_secret='abcde'
            )
    app.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()

if __name__=='__main__':
    main()

通过浏览器可以看到效果如下:

未分类

如果使用set_secure_cookie和get_secure_cookie方法,那么必须在tornado.web.Application中设置cookie_secret参数,’abcd’只是我测试写的,生产环境下可以用uuid生成一个随机字符串来用,但使用过程中要保证这个字符串恒定。 如果要用set_cookie和get_cookie方法,则只需把上面secure去掉,把cookie_secret参数也去掉即可,cookie就不截图了,是明文的。

最后总结一些cookie和secure_cookie的区别:cookie是基于session的,即打开浏览器设置cookie后,只要不关闭浏览器cookie会一直保存,关闭浏览器后cookie删除,其实就是保存在浏览器运行进程的那块内存中。secure_cookie是保存在硬盘中的cookie,过期时间为一个月,所以一旦设置secure_cookie后,不论浏览器关闭与否,只要不清空cookie,这个值就一直保存直到cookie过期失效。

我们的Tornado项目结构

Tornado项目结构

之前答应过群里几个同学要晒下我们的Tornado项目结构,后来就忘了。。。今天晒出来。

无论是Tornado项目还是Django的项目,大体结构都是一样的。最外层是工程结构,包含了配置、文档、打包等信息,当然还有源码。

项目结构大体都是这样:

project
    - conf/
    - docs/
    - src/
        - package1/
            - __init__.py
            - models.py
        - manager.py
        - server.py
    - setup.py
    - MANIFEST.in

对于项目来说,只考虑两个问题,第一:本地开发是否方便;第二:线上部署是否方便。

开发方便

Django的./manage.py runserver的方式对于本地开发调试就很方便,所以对于Tornado项目来说,也需要有一个类似的机制可以方便的在开发环境中启动项目。

部署方便

因为我们是采用标准的PyPi包分发的方式部署的项目,所有项目文件最终都会落到site-packages中,所以包目录的规划就是个问题。

比如像Django那样,把所有的App作为独立的包分散到site-packages中,还是把源码目录”src”作为独立的包放到site-packages中。

两种不同的方式,在启动时也有所差别,因为包的路径是不一样的。这里不讨论哪种方式更合理,我们只说实际的使用情况。

所以部署方便的点在于,我把包放到site-packages中后是否能方便的启动项目。这意味着包的结构需要兼容本地启动和线上启动。

本地和线上的差别

所以就扯到另外一个问题,本地启动项目时,你当前脚本所在的目录就是默认的包根目录,也就是在sys.path中会加入当前文件所在目录,也就是上面结构中的project/src。你要引用package1下的模块,直接from package1.models import xxxx即可。

而在线上的情况是,源码目录是作为独立包放在site-packages中的。你要引用的话需要from src.package1.models import xxx。

这种本地和线上不同引用的问题在Django中是没有的,除非你调整了Django的结构。

问题解决

包的依赖路径问题,基本上都可以通过sys.path.insert()来解决。
两种解决的方式,一个是改线上的sys.path,一个是改本地的。线上的改动只需要在项目加载时把src目录先insert到sys.path中,作为一个新的根路径。

另外一个就是改本地的启动命令,把src所在的目录加到sys.path中。
从个人的习惯来说,能调整本地逻辑的就不去修改部署环境的逻辑,让其按照默认的方式处理是最稳当的方式。毕竟线上挂了影响很大。

所以我会在本地启动项目是做上面的处理,同时所有包的引用触发是同包的情况下,否则都需要从src开始,也就是:from src.package1.models import xx 。

最终目录结构

project
   ├── MANIFEST.in
   ├── README.md
   ├── conf
   │   └── supervisord.conf
   ├── fabfile
   │   ├── __init__.py
   ├── requirements.txt
   ├── setup.cfg
   ├── setup.py
   └── project_src
       ├── __init__.py
       ├── handlers
       │   ├── __init__.py
       │   ├── base.py
       │   ├── index.py
       ├── server.py
       ├── settings
       │   ├── __init__.py
       │   ├── base.py
       │   └── develop.py
       ├── templates
       │   └── base.html
       ├── url.py
       └── models
           ├── __init__.py
           ├── model1.py
           └── model2.py

其中一server.py的示例代码如下:

#!/usr/bin/env python
import os
import sys
from logging.config import dictConfig
from importlib import import_module
import click
import tornado.ioloop
import tornado.web
from raven.contrib.tornado import AsyncSentryClient
def make_app(debug=None):
    from project_src.url import urls
    assert isinstance(urls, (list, tuple)), 'urls must be list or tuple'
    return tornado.web.Application(urls, debug=debug)
def init_log():
    dictConfig(tornado.settings.LOGGING)
@click.command()
@click.option('--port', default=9090)
@click.option('--profile', default='develop')
def serve(port, profile):
    settings = import_module(f'project_src.settings.{profile}')
    tornado.settings = settings
    init_log()
    app = make_app(settings.DEBUG)
    app.listen(port)
    app.sentry_client = AsyncSentryClient(
        '<sentry>'
    )
    sys.stdout.write(f"Start server at:http://0.0.0.0:{port} nProfile: {profile}n")
    tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
    current_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.insert(0, current_path)
    serve()

最后

其实无论哪种方式,只要能够保证大家保有共识就可以,比如the5fire不去修改线上的加载路径,目的是避免有人去按照常识去修改包目录之后产生不可预知的后果。

Tornado Web服务器多进程启动的2个方法

一、Tornado简介

Tornado 是 FriendFeed 的 Web 服务器及其常用工具的开源版本。Tornado 和现在的主流 Web 服务器框架(包括大多数 Python 的框架)有着明显的区别:它是非阻塞式服务器,而且速度相当快。得利于其 非阻塞的方式和对epoll的运用,Tornado 每秒可以处理数以千计的连接,因此 Tornado 是实时 Web 服务的一个理想框架。

二、多进程启动方法

正常启动方法:

server = HTTPServer(app)
server.listen(8888)
IOLoop.instance().start()

多进程、方案1:

server = HTTPServer(app)
server.bind(8888)
server.start(0)  # Forks multiple sub-processes
IOLoop.instance().start()

多进程、方案2:

sockets = tornado.netutil.bind_sockets(8888)
tornado.process.fork_processes(0)
server = HTTPServer(app)
server.add_sockets(sockets)
IOLoop.instance().start()

tornado 环境部署 supervisor配置

这个直接安装在root 角色下面,不是在虚拟环境的,虚拟环境知识和程序有关

1、下载安装

# 使用 pip 装会出问题的, 最好用yum 安装 可以直接使用 systemctl 会有问题
#sudo pip install supervisor

yum -y install supervisor

2、创建配置文件

这一步是生成supervisor 的配置文件

echo_supervisord_conf > /etc/supervisord.conf

3、修改 里面的配置文件

[include]
files = relative/directory/*.ini

修改为

[include]
files = /etc/supervisor/*.conf

这样配置的说明,相当于给这个文件进行扩展了不然太长

4、在etc 下面创建 supervisor目录,创建xxx.conf

mkdir /etc/supervisor
touch yabg.conf

启动的时候就会找到这个文件

5、填写配置信息

[group:tornadoes]
programs=tornado-8000,tornado-8001,tornado-8002

[program:tornado-8000]
command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat    /server.py --port=8000
directory=/home/python/Documents/demo/chat
user=python
autorestart=true
redirect_stderr=true
stdout_logfile=/home/python/tornado.log
loglevel=info

[program:tornado-8001]
command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat    /server.py --port=8001
directory=/home/python/Documents/demo/chat
user=python
autorestart=true
redirect_stderr=true
stdout_logfile=/home/python/tornado.log
loglevel=info

[program:tornado-8002]
command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat    /server.py --port=8002
directory=/home/python/Documents/demo/chat
user=python
autorestart=true
redirect_stderr=true
stdout_logfile=/home/python/tornado.log     存放日志的地方(标准日志输出)
loglevel=info

说明:

command 前面的是python 解释权所在的位置(这里为设置的虚拟的目录位置), 后面是tornado 的 main.py 所在的位置 –port 监听端口
directory 文件所在的目录
user 用户
上面有多个不同端口,直接监听不同的端口
现在来看下生成目前的配置

[yangxiaodong@dev conf.d]$ cat ohho.conf 
[program:ohho]

user=www
command=/data/env/yang/bin/python  /data/websites/work/main.py

process_name=%(program_name)s ; process_name expr (default %(program_name)s)
numprocs=1                    ; number of processes copies to start (def 1)
startretries=2                ;
stopsignal=TERM               ; signal used to kill process (default TERM)
redirect_stderr=true          ; redirect proc stderr to stdout (default false)
directory=/data/websites/SDevelop/
autostart=true
stdout_logfile = /data/websites/logs/ohho.out
stdout_logfile_maxbytes=100MB
stdout_logfile_backups=10
stderr_logfile = /data/websites/logs/ohho.err
stderr_logfile_maxbytes=100MB
stderr_logfile_backups=10

直接启动报错

systemctl start supervisord.service
Failed to start supervisord.service: Unit not found.

解决办法:

最好使用 yum 安装就可以啦
使用配置文件启动

supervisord -c /etc/supervisord.conf

查看是不是启动了

ps aux | grep supervisord

supervisorctl

我们可以利用supervisorctl来管理supervisor。

supervisorctl

status # 查看程序状态
stop tornadoes:* # 关闭 tornadoes组 程序
start tornadoes:* # 启动 tornadoes组 程序
restart tornadoes:* # 重启 tornadoes组 程序
update # 重启配置文件修改过的程序

使用Nginx 和Supervisor在Linux服务器上部署Tornado

Nginx 安装: sudo apt-get install nginx

Nginx 安装后用浏览器进入127.0.0.1就可以看到nginx的欢迎页了

nginx 常用命令

  1. sudo service nginx start 启动nginx
  2. sudo service nginx stop 停止nginx
  3. sudo service nginx restart 重启nginx
  4. sudo service nginx reload 重新加载配置文件

未分类

Supervisor 安装: sudo apt-get install supervisor

部署步骤:

  • Tornado项目路径 : /home/你的用户名/Tornado项目文件夹名称/main.py

  • 在/etc/nginx/下 创建nginx.conf配置文件
    这里我们使用8000-8003四个端口,进行端口转发 配置文件编写要注意main.py所在位置要写对,即下面配置文件中的中文

user root;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 768;
    multi_accept on;
    use epoll;
}
http {
    # Enumerate all the Tornado servers here
    upstream frontends {
        server 127.0.0.1:8000;
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
        server 127.0.0.1:8003;
    }

    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    access_log /var/log/nginx/access.log;

    keepalive_timeout 65;
    proxy_read_timeout 200;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    gzip on;
    gzip_min_length 1000;
    gzip_proxied any;
    gzip_types text/plain text/html text/css text/xml
               application/x-javascript application/xml
               application/atom+xml text/javascript;
    proxy_next_upstream error;

    server {
        listen 80;

        # Allow file uploads
        client_max_body_size 50M;

        location ^~ /static/ {
            root /home/用户名/项目文件夹名/;
            if ($query_string) {
                expires max;
            }
        }
        location = /favicon.ico {
            rewrite (.*) /static/favicon.ico;
        }
        location = /robots.txt {
            rewrite (.*) /static/robots.txt;
        }

        location / {
            proxy_pass_header Server;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_pass http://frontends;
        }
    }
}
  • 编写supervisor配置文件

  • 进入supervisor配置文件夹 cd /etc/supervisor/conf.d/

  • 创建tornados.conf

[group:tornadoes]
programs=tornado-8000,tornado-8001,tornado-8002,tornado-8003

[program:tornado-8000]
command=python /home/用户名/项目文件夹名/main.py --port=8000
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info

[program:tornado-8001]
command=python /home/用户名/项目文件夹名/main.py --port=8001
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info

[program:tornado-8002]
command=python /home/用户名/项目文件夹名/main.py --port=8002
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info

[program:tornado-8003]
command=python /home/用户名/项目文件夹名/main.py --port=8003
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info
  • 其中/var/log/tornado.log为日志文件目录
    然后先重载nginx的配置文件 sudo service nginx reload
    重启 nginx : sudo service nginx restart
    supervisor开启所有进程: sudo supervisorctrl restart all

再次打开127.0.0.1后可以看到项目已经成功部署。

Flask 与 Tornado 中的路由定义

Flask 和 Tornado 是我现在最“熟悉”的两个 Python web 框架,各自都具备一些别具一格的特性, 在很多实现上都走了截然不同的道路。它们的路由实现分别代表了 Python web 框架的两大风格:

Flask 使用函数作为 web 请求的 Handler,这一点和早期的 Django 是一致的, 不过 Django 似乎是 1.7 以后推荐使用 class 作为 Handler。和 Django 不同的是, Flask 使用装饰器注册路由,同类型的框架还有 Bottle。

# flaskapp.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

说到 class handler,web.py 就是其中很有名的 一个,而 Tornado 的最初的灵感就是来自 web.py,Tornado 和 web.py 在路由方面都是 使用路由元组做配置。

# tornadoapp.py
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

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

if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

(上面两段代码分别来自 Flask 和 Tornado 官方的示例教程。)

当然,并不是说 Flask 和 Tornado 就不能使用对方的路由配置模式了, 这里提供两个简单的实现抛砖引玉:

使用路由元组的 Flask

Flask 在路由方面支持惰性加载, 提供了 Flask.add_url_rule() 函数用于手动注册路由:

# flaskapp.py
import flask

class Flask(flask.Flask):
    def add_url_rules(self, url_rules):
        for url_rule in url_rules:
            self.add_url_rule(url_rule[0], view_func=url_rule[1])

def hello():
    return "Hello World!"

app = Flask(__name__)
app.add_url_rules([("/", hello)])

if __name__ == "__main__":
    app.run()

使用装饰器配置路由的 Tornado

上面的 “Hello World” 修改如下:

# tornadoapp.py
import tornado.ioloop
import tornado.web

class Application(tornado.web.Application):
    def route(self, pattern):
        def _(handler):
            handler_pattern = [(pattern, handler)]
            self.add_handlers(".*$", handler_pattern)
            return handler
        return _

app = Application()

@app.route(r"/")
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

if __name__ == "__main__":
    app.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

tornado分页实现-从本质到完全实现

初始化tornado目录

未分类

构建项目–实现post提交,get展示数据

实现类似

未分类

代码逻辑

未分类

完整代码

start.py

#!/usr/bin/env python
# coding=utf-8
import time
import tornado.ioloop
import tornado.web


# 业务逻辑处理模块


# 配置选项模块
from controllers import home


# 静态文件和模板文件的配置
settings = {
    'template_path': 'templates',
    'static_path': 'statics',
}

# 路由模块
## 动态路由系统
application = tornado.web.Application([
    (r"/index/(?P<page>d+)/(?P<nid>d+)", home.IndexHandler),
],
    **settings
)

## wsgi模块
if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
controllers/home.py

#!/usr/bin/env python
# coding=utf-8
import tornado.web

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]


class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self,page,nid):
        # print(page,nid)
        self.render("home/index.html", list_info=LIST_INFO)

    def post(self, *args, **kwargs):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/1/1")
home/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<h1>添加数据</h1>
<form action="/index/1/1" method="post">
    <input type="text" name="username">
    <input type="email" name="email">
    <input type="submit" typeof="提交">
</form>
<h1>显示数据</h1>
<table border="1">
    <thead>
        <tr>
            <th>用户名</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
        {% for line in list_info %}
            <tr>
                <td>{{ line['username'] }}</td>
                <td>{{ line['email'] }}</td>
            </tr>
        {% end %}
    </tbody>
</table>
</body>
</html>

实现url //

未分类

实现分页-每页显示5条数据

效果:

未分类

代码逻辑:

未分类

完整代码:

#!/usr/bin/env python
# coding=utf-8
import time
import tornado.ioloop
import tornado.web


# 业务逻辑处理模块


# 配置选项模块
from controllers import home


# 静态文件和模板文件的配置
settings = {
    'template_path': 'templates',
    'static_path': 'statics',
}

# 路由模块
## 动态路由系统
application = tornado.web.Application([
    (r"/index/(?P<page>d*)", home.IndexHandler),
],
    **settings
)

## wsgi模块
if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
#!/usr/bin/env python
# coding=utf-8
import tornado.web

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]


class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        # 每页显示5条数据
        # LIST_INFO[0:5]
        # LIST_INFO[5:10]
        page = int(page)
        print(page)
        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]
        self.render("home/index.html", list_info=current_list)

    def post(self, *args, **kwargs):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/1")
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<h1>添加数据</h1>
<form action="/index/1" method="post">
    <input type="text" name="username">
    <input type="email" name="email">
    <input type="submit" typeof="提交">
</form>
<h1>显示数据</h1>
<table border="1">
    <thead>
        <tr>
            <th>用户名</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
        {% for line in list_info %}
            <tr>
                <td>{{ line['username'] }}</td>
                <td>{{ line['email'] }}</td>
            </tr>
        {% end %}
    </tbody>
</table>
</body>
</html>

处理page异常

  • page为空 如page=
  • page转换int不成功, 如page=xfdsfd213
  • page<=0情况

思路:

使用try except来操作

class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        # 每页显示5条数据
        # LIST_INFO[0:5]
        # LIST_INFO[5:10]

        # c        
        # if not page:
        #     page = 1
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1

        print(page)
        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]
        self.render("home/index.html", list_info=current_list)

    def post(self, *args, **kwargs):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/1")

实现提交数据后,不总是跳转到index/1,而是和get时候index/x一样

实现效果

未分类

代码逻辑

未分类

完整代码:

#!/usr/bin/env python
# coding=utf-8
import tornado.web

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]


class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1

        print(page)
        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]
        self.render("home/index.html", list_info=current_list,current_page=page)

    def post(self,page):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/"+page)
#!/usr/bin/env python
# coding=utf-8
import time
import tornado.ioloop
import tornado.web


# 业务逻辑处理模块


# 配置选项模块
from controllers import home


# 静态文件和模板文件的配置
settings = {
    'template_path': 'templates',
    'static_path': 'statics',
}

# 路由模块
## 动态路由系统
application = tornado.web.Application([
    (r"/index/(?P<page>d*)", home.IndexHandler),
],
    **settings
)

## wsgi模块
if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<h1>添加数据</h1>
<form action="/index/{{ current_page }}" method="post">
    <input type="text" name="username">
    <input type="email" name="email">
    <input type="submit" typeof="提交">
</form>
<h1>显示数据</h1>
<table border="1">
    <thead>
        <tr>
            <th>用户名</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
        {% for line in list_info %}
            <tr>
                <td>{{ line['username'] }}</td>
                <td>{{ line['email'] }}</td>
            </tr>
        {% end %}
    </tbody>
</table>
</body>
</html>

xss跨站

效果: 提交js代码.每刷新一次都会执行一次.

未分类

实现:让代码按照原格式执行,前面加raw

    <tbody>
        {% for line in list_info %}
            <tr>
                <!--<td>{{ line['username'] }}</td>-->
                <td>{% raw line['username'] %}</td>
                <td>{{ line['email'] }}</td>
            </tr>
        {% end %}
    </tbody>

初步实现点分页链接

效果:

未分类

核心代码

<table border="1">
    <thead>
        <tr>
            <th>用户名</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
        {% for line in list_info %}
            <tr>
                <!--<td>{{ line['username'] }}</td>-->
                <td>{% raw line['username'] %}</td>
                <td>{{ line['email'] }}</td>
            </tr>
        {% end %}
    </tbody>
</table>
<div class = "pager">
    <a href="/index/1">1</a>
    <a href="/index/2">2</a>
    <a href="/index/3">3</a>
</div>

从后端传值过来

实现效果

未分类

    def get(self, page):
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1


        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]

        all_pager,c = divmod(len(LIST_INFO),5)
        if c>0:
            all_pager+=1

        str_page = """
        <a href="/index/1">1</a>
        <a href="/index/2">2</a>
        <a href="/index/3">3</a>
        <a href="/index/4">4</a>
        """
        self.render("home/index.html", list_info=current_list,current_page=page,str_page=str_page)

实现分页链接根据后端数据长度自动生成

效果图:

未分类

核心代码:

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]
for line in range(300):
    tmp={"username": "maotai", "email": "123456"}
    LIST_INFO.append(tmp)
    def get(self, page):
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1


        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]

        all_pager,c = divmod(len(LIST_INFO),5)
        if c>0:
            all_pager+=1

        # str_page = """
        # <a href="/index/1">1</a>
        # <a href="/index/2">2</a>
        # <a href="/index/3">3</a>
        # <a href="/index/4">4</a>
        # """
        list_page=[]
        for p in range(all_pager):
            tmp = '<a href="/index/%s">%s</a>'%(p+1,p+1)
            list_page.append(tmp)
        str_page="".join(list_page)
        self.render("home/index.html", list_info=current_list,current_page=page,str_page=str_page)
<div class = "pager">
    <!--{{str_page}}-->
    {% raw str_page %}
</div>

实现当前页分页链接高亮

未分类

    def get(self, page):
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1


        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]

        all_pager,c = divmod(len(LIST_INFO),5)
        if c>0:
            all_pager+=1

        # str_page = """
        # <a href="/index/1">1</a>
        # <a href="/index/2">2</a>
        # <a href="/index/3">3</a>
        # <a href="/index/4">4</a>
        # """
        list_page=[]
        for p in range(all_pager):
            if p+1 == page:
                tmp = '<a class="active" href="/index/%s">%s</a>' % (p + 1, p + 1)
            else:
                tmp = '<a href="/index/%s">%s</a>'%(p+1,p+1)
            list_page.append(tmp)
        str_page="".join(list_page)
        self.render("home/index.html", list_info=current_list,current_page=page,str_page=str_page)

前端:

        .pager a.active{
            background-color: brown;
            color: white;
        }

完整的code:

未分类

#!/usr/bin/env python
# coding=utf-8
import tornado.web

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]
for line in range(300):
    tmp={"username": "maotai", "email": "123456"}
    LIST_INFO.append(tmp)

class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1


        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]

        all_pager,c = divmod(len(LIST_INFO),5)
        if c>0:
            all_pager+=1

        # str_page = """
        # <a href="/index/1">1</a>
        # <a href="/index/2">2</a>
        # <a href="/index/3">3</a>
        # <a href="/index/4">4</a>
        # """
        list_page=[]
        for p in range(all_pager):
            if p+1 == page: #这里为什么p+1,因为遍历的是从0到all_pager
                tmp = '<a class="active" href="/index/%s">%s</a>' % (p + 1, p + 1)
            else:
                tmp = '<a href="/index/%s">%s</a>'%(p+1,p+1)
            list_page.append(tmp)
        str_page="".join(list_page)
        self.render("home/index.html", list_info=current_list,current_page=page,str_page=str_page)

    def post(self,page):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/"+page)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
    <style>
        .pager a{
            display: inline-block;
            padding:5px;
            margin: 3px;
            background-color: cadetblue;
        }
        .pager a.active{
            background-color: brown;
            color: white;
        }
    </style>
</head>
<body>
<h1>添加数据</h1>
<form action="/index/{{ current_page }}" method="post">
    <input type="text" name="username">
    <input type="email" name="email">
    <input type="submit" typeof="提交">
</form>
<h1>显示数据</h1>
<table border="1">
    <thead>
        <tr>
            <th>用户名</th>
            <th>邮箱</th>
        </tr>
    </thead>
    <tbody>
        {% for line in list_info %}
            <tr>
                <td>{{ line['username'] }}</td>
                <!--<td>{% raw line['username'] %}</td>-->
                <td>{{ line['email'] }}</td>
            </tr>
        {% end %}
    </tbody>
</table>
<div class = "pager">
    <!--{{str_page}}-->
    {% raw str_page %}
</div>
</body>
</html>

实现显示11项,当前页+-5

未分类

分析:

当总页数<11:
    1 11
当总页数>11:
    当前页<6:
        1 11
    当前页>6:
        当前页+5>总页数
            总页数-11 总页数
        当前页+5<总页数:
            s=page-5
            t=page+5

核心实现代码:

class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        try:
            page = int(page)
        except:
            page = 1
        if page <= 0:
            page = 1

        start = (page - 1) * 5
        end = page * 5
        current_list = LIST_INFO[start:end]

        all_pager, c = divmod(len(LIST_INFO), 5)
        if c > 0:
            all_pager += 1

        # str_page = """
        # <a href="/index/1">1</a>
        # <a href="/index/2">2</a>
        # <a href="/index/3">3</a>
        # <a href="/index/4">4</a>
        # """
        list_page = []
        if all_pager < 11:
            s = 1
            t = 11
        else:
            if page <= 6:
                s = 1
                t = 11
            elif page > 6:
                if page + 5 > all_pager:
                    s = all_pager - 11
                    t = all_pager
                else:
                    s = page - 5
                    t = page + 5

        for p in range(s, t + 1):
            if p == page:  # 这里为什么p,而不是p+1? 因为range(s, t + 1),不是从0开始的.
                tmp = '<a class="active" href="/index/%s">%s</a>' % (p, p)
            else:
                tmp = '<a href="/index/%s">%s</a>' % (p, p)
            list_page.append(tmp)
        str_page = "".join(list_page)
        self.render("home/index.html", list_info=current_list, current_page=page, str_page=str_page)

封装分页class

思路

未分类

代码:

#!/usr/bin/env python
# coding=utf-8
import tornado.web

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]
for line in range(300):
    tmp = {"username": "maotai", "email": "123456"}
    LIST_INFO.append(tmp)


class Pagination:
    def __init__(self, current_page, all_item):
        all_pager, c = divmod(len(all_item), 5)
        if c > 0:
            all_pager += 1
        try:
            current_page = int(current_page)
        except:
            current_page = 1
        if current_page <= 0:
            current_page = 1

        self.current_page = current_page
        self.all_pager = all_pager

    @property
    def start(self):
        return (self.current_page - 1) * 5

    @property
    def end(self):
        return self.current_page * 5

    def page_str(self, baseurl):
        list_page = []
        if self.all_pager < 11:
            s = 1
            t = 11
        else:
            if self.current_page <= 6:
                s = 1
                t = 11
            elif self.current_page > 6:
                if self.current_page + 5 > self.all_pager:
                    s = self.all_pager - 11
                    t = self.all_pager
                else:
                    s = self.current_page - 5
                    t = self.current_page + 5

        for p in range(s, t + 1):
            if p == self.current_page:
                tmp = '<a class="active" href="%s%s">%s</a>' % (baseurl, p, p)
            else:
                tmp = '<a href="%s%s">%s</a>' % (baseurl, p, p)
            list_page.append(tmp)
        return "".join(list_page)

    def current_list_info(self):
        current_list = LIST_INFO[self.start:self.end]
        return current_list


class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        ## 实例化分页类
        # 传入当前页 和 所有数据
        # 获得current_list 即每页5条数据列表
        page_obj = Pagination(page, LIST_INFO)
        current_list = page_obj.current_list_info()
        str_page = page_obj.page_str("/index/")
        self.render("home/index.html", list_info=current_list, current_page=page, str_page=str_page)

    def post(self, page):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/" + page)

实现上一页 下一页 跳转到

效果:

未分类

原理:

未分类

利用js的方法href实现跳转.

完整的code:

#!/usr/bin/env python
# coding=utf-8
import tornado.web

LIST_INFO = [
    {"username": "maotai", "email": "123456"},
]
for line in range(300):
    tmp = {"username": "maotai", "email": "123456"}
    LIST_INFO.append(tmp)


class Pagination:
    def __init__(self, current_page, all_item):
        all_pager, c = divmod(len(all_item), 5)
        if c > 0:
            all_pager += 1
        try:
            current_page = int(current_page)
        except:
            current_page = 1
        if current_page <= 0:
            current_page = 1

        self.current_page = current_page
        self.all_pager = all_pager
        self.all_item=all_item

    @property
    def start(self):
        return (self.current_page - 1) * 5

    @property
    def end(self):
        return self.current_page * 5

    def current_list_info(self):
        current_list = LIST_INFO[self.start:self.end]
        return current_list

    def page_str(self, baseurl):
        list_page = []

        # 上一页  --上一页 页码 下一页 必须按照顺序添加到list
        if self.current_page > 1:
            last_page = '<a href="%s%s">上一页</a>' % (baseurl, self.current_page - 1)
        else:
            last_page = '<a href="%s%s">上一页</a>' % (baseurl, self.current_page)
        list_page.append(last_page)

        # 页码页
        if self.all_pager < 11:
            s = 1
            t = 11
        else:
            if self.current_page <= 6:
                s = 1
                t = 11
            elif self.current_page > 6:
                if self.current_page + 5 > self.all_pager:
                    s = self.all_pager - 11
                    t = self.all_pager
                else:
                    s = self.current_page - 5
                    t = self.current_page + 5

        for p in range(s, t + 1):

            if p == self.current_page:
                tmp = '<a class="active" href="%s%s">%s</a>' % (baseurl, p, p)
            else:
                tmp = '<a href="%s%s">%s</a>' % (baseurl, p, p)
            list_page.append(tmp)

        # if self.current_page>1:
        #     syyp = self.current_page - 1
        # else:
        #     syyp = self.current_page
        #
        # if self.current_page > len(self.all_item):
        #     xyyp = len(self.all_item)
        # else:
        #     xyyp = self.current_page
        #
        # syy = '<a href="%s%s">上一页</a>' % (baseurl, syyp)
        # xyy = '<a href="%s%s">下一页</a>' % (baseurl, xyyp)
        # list_page.insert(0,syy)
        # list_page.append(xyy)



        # 下一页
        if self.current_page< self.all_pager:
            next_page = '<a href="%s%s">下一页</a>' % (baseurl, self.current_page+1)
        else:
            next_page = '<a href="%s%s">下一页</a>' % (baseurl, self.current_page)
        list_page.append(next_page)

        # 跳转到
        ## 获取本次点击前框里输入的数据, 拼接 baseurl + 数字 就可以实现跳转
        jump = """<input type="text" /><a onclick="Jump('%s',this);">GO</a>"""%(baseurl,)
        script="""<script>
            function Jump(baseUrl,ths){
                var val = ths.previousElementSibling.value;
                if(val.trim().length>0){
                    location.href = baseUrl+val;
                }
            }
            </script>
        """
        list_page.append(jump)
        list_page.append(script)
        return "".join(list_page)


class IndexHandler(tornado.web.RequestHandler):
    # def get(self, *args, **kwargs):
    def get(self, page):
        page_obj = Pagination(page, LIST_INFO)
        current_list = page_obj.current_list_info()
        str_page = page_obj.page_str("/index/")
        self.render("home/index.html", list_info=current_list, current_page=page, str_page=str_page)

    def post(self, page):
        username = self.get_argument("username", "")
        email = self.get_argument("email", "")

        tmp = {"username": username, "email": email}
        LIST_INFO.append(tmp)
        self.redirect("/index/" + page)