在Python中CTRL-C行为不同
我最近开始学习Python(之前是Java程序员),现在正在写一些简单的服务器程序。问题是,对于一段看起来很相似的代码,Java的版本能够正确响应
// Java code
package pkg;
import java.io.*;
import java.net.*;
public class ServerTest {
public static void main(final String[] args) throws Exception {
final Thread t = new Server();
t.start();
}
}
class Server extends Thread {
@Override
public void run() {
try {
final ServerSocket sock = new ServerSocket(12345);
while(true) {
final Socket clientSock = sock.accept();
clientSock.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
这是Python的代码:
# Python code
import threading, sys, socket, signal
def startserver():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 12345))
s.listen(1)
while True:
csock, caddr = s.accept()
csock.sendall('Get off my lawn kids...\n')
csock.close()
if __name__ == '__main__':
try:
t = threading.Thread(target=startserver)
t.start()
except:
sys.exit(1)
在上面的两个代码片段中,我创建了一个简单的服务器,它在指定的端口上监听TCP请求。当在Java代码中按下Ctrl+C时,JVM会退出,而在Python代码中,我只是在终端看到一个^C
。我唯一能停止服务器的方法是按Ctrl+Z,然后手动结束这个进程。
所以我想了个主意;为什么不设置一个信号处理器,监听Ctrl+Z并退出应用程序呢?听起来不错,于是我写了:
import threading, sys, socket, signal
def startserver():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 12345))
s.listen(1)
while True:
csock, caddr = s.accept()
csock.sendall('Get off my lawn kids...\n')
csock.close()
def ctrlz_handler(*args, **kwargs):
sys.exit(1)
if __name__ == '__main__':
try:
signal.signal(signal.SIGTSTP, ctrlz_handler)
t = threading.Thread(target=startserver)
t.start()
except:
sys.exit(1)
但是现在,情况似乎更糟了!现在在终端按Ctrl+C会显示'^C',而按Ctrl+Z则显示'^Z'。
所以我的问题是,为什么会有这种奇怪的行为?我可能会在同一个进程中运行多个服务器进程作为单独的线程,所以有没有什么好的方法可以在进程收到
谢谢大家,
sasuke
2 个回答
你创建的线程不是守护线程,所以当父线程结束时,它不会自动退出。
主线程在启动子线程后立刻结束,Python进程会等待子线程结束。
顺便提一下,别忘了关闭套接字。
你应该为子线程实现一个停止机制。一个简单的解决办法是,在接受客户端连接的循环中检查一个
stopped
标志(可以用模块级别的变量,或者扩展threading.Thread
类来封装它),当你按下Ctrl+C或Ctrl+Z时,把这个标志设为True,并通过连接来“踢掉”服务器套接字;在主线程中等待while t.isAlive(): time.sleep(1)
。下面是一个示例代码:
import threading, sys, socket, signal, time
class Server(threading.Thread): def init(self): threading.Thread.init(self) self._stop = False self._port =12345
def stop(self): self.__stop = True self.kick()
def kick(self): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('', self.__port)) s.close() except IOError, e: print "踢掉服务器失败:", e
def run(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('', self._port)) s.listen(1) try: while True: csock, caddr = s.accept() if self._stop: print "已停止,退出循环" break print "客户端连接来自", caddr try: csock.sendall('小子们,快离开我的草坪...\n') finally: csock.close() finally: s.close()
if name == 'main': t = Server() # 如果线程没有正确停止,它将退出 t.setDaemon(True) t.start() try: while t.isAlive(): time.sleep(1) except: # Ctrl+C会让我们来到这里 print "也许,你打断了这个程序?" t.stop() # 这里可以调用t.join(),或者需要其他同步机制来 # 确保服务器正确停止
我不知道为什么它把stop、kick、run方法显示为模块级别,并且多了几行空行,抱歉。
这里有几个问题:
首先,不要捕捉所有的错误然后悄悄退出。这是最糟糕的做法。
除非你真的很清楚自己在做什么,否则不要捕捉SIGTSTP信号。这个信号不是给应用程序用来拦截的,如果你在捕捉它,很可能你做错了什么。
KeyboardInterrupt信号只会发送给程序的主线程,而不会发送给其他线程。这是有几个原因的。通常,你希望在一个地方处理^C,而不是在一个随机的、恰好接收到这个信号的线程中。此外,这样做可以确保在正常情况下,大多数线程不会接收到异步异常;这是很有用的,因为在所有编程语言中(包括Java),处理这些异常是非常困难的。
在这里你永远不会看到KeyboardInterrupt,因为你的主线程在启动次线程后立即退出。没有主线程来接收这个异常,它就被简单地丢弃了。
解决办法有两个:保持主线程的存在,这样KeyboardInterrupt就有地方去处理;并且只捕捉KeyboardInterrupt,而不是所有的异常。
import threading, sys, socket, signal, time
def startserver():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 12345))
s.listen(1)
while True:
csock, caddr = s.accept()
csock.sendall('Get off my lawn kids...\n')
csock.close()
if __name__ == '__main__':
try:
t = threading.Thread(target=startserver)
t.start()
# Wait forever, so we can receive KeyboardInterrupt.
while True:
time.sleep(1)
except KeyboardInterrupt:
print "^C received"
sys.exit(1)
# We never get here.
raise RuntimeError, "not reached"