如何停止Tornado Web服务器?

37
我一直在玩弄Tornado web服务器,现在我想停止Web服务器(例如在单元测试期间)。以下简单示例存在于Tornado网页上
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()

一旦调用tornado.ioloop.IOLoop.instance().start(),它将阻塞程序(或当前线程)。在查看IOLoop对象的源代码时,文档中为stop函数给出了以下示例:

To use asynchronous methods from otherwise-synchronous code (such as
unit tests), you can start and stop the event loop like this:
  ioloop = IOLoop()
  async_method(ioloop=ioloop, callback=ioloop.stop)
  ioloop.start()
ioloop.start() will return after async_method has run its callback,
whether that callback was invoked before or after ioloop.start.

然而,我不知道如何将其集成到我的程序中。实际上,我有一个封装了 Web 服务器的类(拥有自己的 startstop 函数),但是一旦我调用 start,程序(或测试)肯定会被阻塞。

我尝试在另一个进程中启动 Web 服务器(使用 multiprocessing 包)。这是包装 Web 服务器的类:

class Server:
    def __init__(self, port=8888):
        self.application = tornado.web.Application([ (r"/", Handler) ])

        def server_thread(application, port):
            http_server = tornado.httpserver.HTTPServer(application)
            http_server.listen(port)
            tornado.ioloop.IOLoop.instance().start()

        self.process = Process(target=server_thread,
                               args=(self.application, port,))

    def start(self):
        self.process.start()

    def stop(self):
        ioloop = tornado.ioloop.IOLoop.instance()
        ioloop.add_callback(ioloop.stop)

然而,stop 方法似乎并不能完全停止 Web 服务器,因为在下一个测试中它仍然在运行,即使使用了此测试设置:

def setup_method(self, _function):
    self.server = Server()
    self.server.start()
    time.sleep(0.5)  # Wait for web server to start

def teardown_method(self, _function):
    self.kstore.stop()
    time.sleep(0.5)

如何在Python程序内部启动和停止Tornado Web服务器?


更现代的Tornado 从app.listen()返回一个HTTPServer,该服务器具有.close()方法以停止服务器!请参见停止tornado应用程序 - ti7
9个回答

31

我刚遇到这个问题,并且在使用本帖子中的信息后自己解决了它。我只是将我的可工作的独立Tornado代码(从所有示例中复制)移动到一个函数中。 然后,我将该函数作为线程线程调用。 我的情况不同,因为线程调用是从现有代码中完成的,我只是导入了startTornado和 stopTornado例程。

上面的建议似乎非常有效,所以我想提供缺失的示例代码。我在FC16系统的Linux下测试了这段代码(并修复了我的初始打字错误)。

import tornado.ioloop, tornado.web

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

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

def startTornado():
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

def stopTornado():
    tornado.ioloop.IOLoop.instance().stop()

if __name__ == "__main__":
    import time, threading
    threading.Thread(target=startTornado).start()
    print "Your web server will self destruct in 2 minutes"
    time.sleep(120)
    stopTornado()

希望这能帮到下一个人。


29
这不是线程安全的。可能会一直运行,直到你即将开始为期五周的假期的那个周五晚上。 - Schildmeijer
1
这个在多进程中不起作用。ioloop.instance.stop() 方法的问题在于它只能在之前运行 ioloop.instance() 的线程内部工作。如果你使用 multiprocessing 来包装它,它就无法正常工作。 - Nisan.H
@Schildmeijer,您能详细说明为什么以及如何会出现故障吗?谢谢! - Zaar Hai
1
这确实不是线程安全的,正如Schilmeijer所指出的那样。我在下面发布了一个线程安全的答案。 - Zaar Hai
@ZaarHai,您能详细解释一下您发布的线程安全答案与这个答案之间的区别吗?您似乎已经学会了答案。 - Brōtsyorfuzthrāx
显示剩余3条评论

20
以下是如何从另一个线程停止Tornado的解决方案。Schildmeijer提供了一个很好的提示,但我花了一段时间才真正找到最终可行的示例。
请参见下文:
import threading
import tornado.ioloop
import tornado.web
import time


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

def start_tornado(*args, **kwargs):
    application = tornado.web.Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    print "Starting Torando"
    tornado.ioloop.IOLoop.instance().start()
    print "Tornado finished"

def stop_tornado():
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.add_callback(ioloop.stop)
    print "Asked Tornado to exit"

def main():

    t = threading.Thread(target=start_tornado)  
    t.start()

    time.sleep(5)

    stop_tornado()
    t.join()

if __name__ == "__main__":
    main()

2
ioloop.add_callback(lambda x: x.stop(), ioloop) 或者更简单的写法:ioloop.add_callback(ioloop.stop) - ereOn
9
这是否旨在真正停止Web服务器?它似乎仍保持端口打开。 - rakslice

12

如果你不想处理线程,你可以捕获一个键盘中断信号:

try:
    tornado.ioloop.IOLoop.instance().start()
# signal : CTRL + BREAK on windows or CTRL + C on linux
except KeyboardInterrupt:
    tornado.ioloop.IOLoop.instance().stop()

2
在Python 2.7/Windows 10下:这很有帮助:CTRL-BREAK确实让我跳出了tornado.ioloop。然而,它绝对没有引发KeyboardInterrupt。事实上,在CTRL-BREAK之后,没有任何语句被执行。所以无论它在做什么,它都会快速而彻底地杀死进程。 - Dan H
1
我想SIGINT(Ctrl+C)和SIGBREAK(Ctrl+Break)的处理方式是不同的。 - Ninjakannon

4

在 Zaar Hai 的解决方案中存在一个问题,即它会保持套接字处于打开状态。我寻找停止 Tornado 的解决方案的原因是我正在对我的应用服务器运行单元测试,并且我需要一种在测试之间启动/停止服务器的方法以获得清晰的状态(空会话等)。通过保持套接字处于打开状态,第二个测试总是遇到 Address already in use 错误。所以我想出了以下解决方案:

import logging as log
from time import sleep
from threading import Thread

import tornado
from tornado.httpserver import HTTPServer


server = None
thread = None


def start_app():
    def start():
        global server
        server = HTTPServer(create_app())
        server.listen(TEST_PORT, TEST_HOST)
        tornado.ioloop.IOLoop.instance().start()
    global thread
    thread = Thread(target=start)
    thread.start()
    # wait for the server to fully initialize
    sleep(0.5)


def stop_app():
    server.stop()
    # silence StreamClosedError Tornado is throwing after it is stopped
    log.getLogger().setLevel(log.FATAL)
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.add_callback(ioloop.stop)
    thread.join()

这里的主要思路是保持对 HTTPServer 实例的引用,并调用其 stop() 方法。而 create_app() 只是返回一个已配置处理程序的 Application 实例。现在,您可以在单元测试中像这样使用这些方法:

class FoobarTest(unittest.TestCase):

    def setUp(self):
        start_app()

    def tearDown(self):
        stop_app()

    def test_foobar(self):
        # here the server is up and running, so you can make requests to it
        pass

server.stop() 实际上是关闭套接字。对我来说,这里的最有用的答案。 - sebix

3

当您完成单元测试时,要停止整个ioloop,只需调用ioloop.stop方法即可。(请记住,唯一(记录的)线程安全方法是ioloop.add_callback,即如果单元测试由另一个线程执行,则可以将停止调用包装在回调中)

如果仅需要停止http web服务器,则调用httpserver.stop()方法


1
但是调用 ioloop.start() 会阻塞,那我怎么调用 ioloop.stop() 呢?我应该在另一个线程中运行 ioloop.start() 吗? - Adam Lindberg
这是一个解决方案。(记得将ioloop.stop包装在ioloop回调中,以避免并发修改)。另一个解决方案是从ioloop本身停止ioloop。 - Schildmeijer
1
非常抱歉打扰您,但我遇到了同样的问题(我在一个线程中运行tornado,但无法停止它)。我阅读了您的答案和评论,但我真的不明白该如何解决。您能否请发几行代码来说明您的方法?谢谢。 - Konstantin
@Konstantin 我认为Robert Anderson在下面给出的答案详细说明了我采取的解决方案的近似值。如果不够,请告诉我。 - Adam Lindberg
1
@Schildmeijer,我在模块“httpserver”中找不到方法“stop”,Python 报告了同样的问题。 - jondinham

2
如果您需要对单元测试进行此行为,请查看tornado.testing.AsyncTestCase
默认情况下,每个测试都会构建一个新的IOLoop,并且可以通过self.io_loop获得。在构建HTTP客户端/服务器等时应使用此IOLoop。如果被测试的代码需要全局IOLoop,则子类应重写get_new_ioloop以返回它。
如果您需要为其他目的启动和停止IOLoop,并且由于某些原因无法从回调中调用ioloop.stop(),则可以实现多线程实现。但是,为了避免竞争条件,需要同步访问ioloop,这实际上是不可能的。以下内容将导致死锁:
线程1:
with lock:
    ioloop.start()

线程 2:
with lock:
    ioloop.stop()

因为线程1永远不会释放锁定(start()是阻塞的),而线程2将等待直到锁定被释放才停止ioloop。
唯一的方法是让线程2调用ioloop.add_callback(ioloop.stop),这将在事件循环的下一个迭代中调用stop()以停止线程1。请放心,ioloop.add_callback()是线程安全的。

1
Tornado的IOloop.instance()在multiprocessing.Process下运行时,有问题无法停止外部信号。
我想到的唯一解决方案是使用Process.terminate(),这是唯一能够始终如一地工作的方法。
import tornado.ioloop, tornado.web
import time
import multiprocessing

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

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

class TornadoStop(Exception):
    pass
def stop():
    raise TornadoStop
class worker(multiprocessing.Process):
    def __init__(self):
        multiprocessing.Process.__init__(self)
        application.listen(8888)
        self.ioloop = tornado.ioloop.IOLoop.instance()

    def run(self):
        self.ioloop.start()

    def stop(self, timeout = 0):
        self.ioloop.stop()
        time.sleep(timeout)
        self.terminate()



if __name__ == "__main__":

    w = worker()
    print 'starting server'
    w.start()
    t = 2
    print 'waiting {} seconds before stopping'.format(t)
    for i in range(t):
        time.sleep(1)
        print i
    print 'stopping'
    w.stop(1)
    print 'stopped'

0

我们想要使用 multiprocessing.Processtornado.ioloop.IOLoop 来解决 cPython GIL 的性能和独立性问题。为了访问 IOLoop,我们需要使用 Queue 通过传递关闭信号来实现。

这里是一个最简示例:

class Server(BokehServer)

    def start(self, signal=None):
        logger.info('Starting server on http://localhost:%d'
                    % (self.port))

        if signal is not None:
            def shutdown():
                if not signal.empty():
                    self.stop()
            tornado.ioloop.PeriodicCallback(shutdown, 1000).start()

        BokehServer.start(self)
        self.ioloop.start()

    def stop(self, *args, **kwargs):  # args important for signals
        logger.info('Stopping server...')
        BokehServer.stop(self)
        self.ioloop.stop()

流程

import multiprocessing as mp
import signal

from server import Server  # noqa

class ServerProcess(mp.Process):
    def __init__(self, *args, **kwargs):
        self.server = Server(*args, **kwargs)
        self.shutdown_signal = _mp.Queue(1)
        mp.Process.__init__(self)

        signal.signal(signal.SIGTERM, self.server.stop)
        signal.signal(signal.SIGINT, self.server.stop)

    def run(self):
        self.server.start(signal=self.shutdown_signal)

    def stop(self):
        self.shutdown_signal.put(True)

if __name__ == '__main__':
    p = ServerProcess()
    p.start()

干杯!


-1

只需在start()之前添加以下内容:

IOLoop.instance().add_timeout(10,IOLoop.instance().stop)

它将会在循环中注册停止函数作为回调函数,并在开始后10秒启动。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接