如何通过客户端请求干净地退出Pyro守护进程?
我正在尝试使用Pyro来控制一个从属机器。我把需要的Python文件通过rsync同步过去,启动了一个Pyro服务器,进行了一些远程控制的操作,然后我想告诉Pyro服务器关闭。
我在让Pyro守护进程正常关闭时遇到了麻烦。它要么在Daemon.close()
这个调用上卡住,要么如果我把这一行注释掉,它就会在没有正确关闭套接字的情况下退出,这样如果我太快重启服务器,就会出现socket.error: [Errno 98] Address already in use
的错误。
我觉得SO_REUSEADDR并不是正确的解决办法,因为不干净的套接字关闭仍然会导致套接字停留在TIME_WAIT状态,这可能会让一些客户端遇到问题。我认为更好的解决方案是让Pyro守护进程正确关闭它的套接字。
在守护进程内部调用Daemon.shutdown()是否不合适?
如果我启动一个服务器,然后在没有任何客户端连接的情况下按CTRL-C,我就没有遇到任何问题(没有Address already in use
的错误)。这让干净关闭看起来是可能的,大多数情况下(假设客户端和服务器都正常)。
示例:server.py
import Pyro4
class TestAPI:
def __init__(self, daemon):
self.daemon = daemon
def hello(self, msg):
print 'client said {}'.format(msg)
return 'hola'
def shutdown(self):
print 'shutting down...'
self.daemon.shutdown()
if __name__ == '__main__':
daemon = Pyro4.Daemon(port=9999)
tapi = TestAPI(daemon)
uri = daemon.register(tapi, objectId='TestAPI')
daemon.requestLoop()
print 'exited requestLoop'
daemon.close() # this hangs
print 'daemon closed'
示例:client.py
import Pyro4
if __name__ == '__main__':
uri = 'PYRO:TestAPI@localhost:9999'
remote = Pyro4.Proxy(uri)
response = remote.hello('hello')
print 'server said {}'.format(response)
try:
remote.shutdown()
except Pyro4.errors.ConnectionClosedError:
pass
print 'client exiting'
2 个回答
我觉得我快找到解决办法了:就是把 requestloop()
里的 loopCondition
参数和配置值 COMMTIMEOUT
结合起来使用。
server.py
import Pyro4
Pyro4.config.COMMTIMEOUT = 1.0 # without this daemon.close() hangs
class TestAPI:
def __init__(self, daemon):
self.daemon = daemon
self.running = True
def hello(self, msg):
print 'client said {}'.format(msg)
return 'hola'
def shutdown(self):
print 'shutting down...'
self.running = False
if __name__ == '__main__':
daemon = Pyro4.Daemon(port=9999)
tapi = TestAPI(daemon)
uri = daemon.register(tapi, objectId='TestAPI')
def checkshutdown():
return tapi.running
daemon.requestLoop(loopCondition=checkshutdown) # permits self-shutdown
print 'exited requestLoop'
daemon.close()
print 'daemon closed'
不过,有一种情况还是会让一个连接处于 TIME_WAIT 状态。如果客户端在服务器之后关闭了它的连接,那么下次启动服务器时就会出现 Address already in use
的错误。
我找到的唯一解决办法就是把服务器的 COMMTIMEOUT 设置得更长一点(或者在调用 daemon.close()
之前先睡几秒钟),并确保客户端在关闭连接后立即调用 _pyroRelease()
:
client.py
import Pyro4
if __name__ == '__main__':
uri = 'PYRO:TestAPI@localhost:9999'
remote = Pyro4.Proxy(uri)
response = remote.hello('hello')
print 'server said {}'.format(response)
remote.shutdown()
remote._pyroRelease()
print 'client exiting'
我想这样也算可以,但考虑到调度的不公平性和网络延迟,还是让人失望有这样的竞争条件存在。
我觉得这个问题可以通过让你的 shutdown()
调用守护进程的 shutdown
来解决,而不需要使用超时或循环条件。根据这个链接:http://pythonhosted.org/Pyro4/servercode.html#cleaning-up,还有另外一种方法,就是在运行中的 bdaemon 对象上调用 Pyro4.core.Daemon.shutdown()。这样可以跳出请求循环,让你的代码能够干净利落地进行清理,而且在多线程服务器类型上也能正常工作,不需要其他要求。
以下代码在 Windows 上的 Python3.4.2 中可以正常运行。这里的
@Pyro4.oneway
装饰器对于shutdown
并不是必须的,但在某些情况下是需要的。
这是 server.py
的代码:
import Pyro4
# using Python3.4.2
@Pyro4.expose
class TestAPI:
def __init__(self, daemon):
self.daemon = daemon
def hello(self, msg):
print('client said {}'.format(msg))
return 'hola'
@Pyro4.oneway # in case call returns much later than daemon.shutdown
def shutdown(self):
print('shutting down...')
self.daemon.shutdown()
if __name__ == '__main__':
daemon = Pyro4.Daemon(port=9999)
tapi = TestAPI(daemon)
uri = daemon.register(tapi, objectId='TestAPI')
daemon.requestLoop()
print('exited requestLoop')
daemon.close()
print('daemon closed')
这是 client.py
的代码:
import Pyro4
# using Python3.4.2
if __name__ == '__main__':
uri = 'PYRO:TestAPI@localhost:9999'
remote = Pyro4.Proxy(uri)
response = remote.hello('hello')
print('server said {}'.format(response))
remote.shutdown()
remote._pyroRelease()
print('client exiting')