如何在接收网络数据时运行程序并执行代码?

0 投票
1 回答
827 浏览
提问于 2025-04-18 06:21

我写了一个小程序,用Python编写,基本上做了以下几件事:

  1. 从用户那里获取一个热词(关键字)。如果这个热词和预设的关键字匹配,就继续执行。
  2. 输入正确的热词后,程序会要求用户输入一个命令。
  3. 程序读取命令后,会检查一个命令文件,看看是否有与输入的命令匹配的内容。
  4. 如果找到匹配的命令,就执行那个命令。

我想增加一个功能,让程序可以通过网络执行命令,具体步骤如下(同时我也想学习使用Twisted这个库):

  1. 客户端 #1 输入一个针对客户端 #2 的命令。
  2. 这个命令会发送到一个服务器,服务器会把命令转发给客户端 #2。
  3. 客户端 #2 接收到命令后,如果命令有效,就执行它。

注意:本地输入命令(使用下面的代码)和远程输入命令都应该是可行的。

经过一番思考,我想不出其他实现这个功能的方法,除了:

  1. 让上面的程序作为进程 #1 运行(就是我一开始写的那个本地程序)。
  2. 一个Twisted客户端作为进程 #2 运行,接收来自远程客户端的命令。每当接收到命令时,Twisted客户端会启动一个线程,解析命令,检查它的有效性,如果有效就执行它。

由于我对线程的经验不多,对网络编程更是没有经验,所以我想不出其他让我觉得合理的方案。

我上面提到的方案是不是太复杂了? 在我尝试这样实现之前,希望能得到一些建议。

这个Python程序的代码(不包括客户端)是:

主程序(也就是start()方法):

class Controller:
    def __init__(self,listener, executor):
        self.listener = listener
        self.executor = executor


    def start(self):
        while True:
            text = self.listener.listen_for_hotword()

            if self.executor.is_hotword(text):
                text = self.listener.listen_for_command()
                if self.executor.has_matching_command(text):
                    self.executor.execute_command(text)
                else:
                    tts.say("No command found. Please try again")

监听器(获取用户输入):

class TextListener(Listener):
    def listen_for_hotword(self):
        text = raw_input("Hotword: ")
        text =' '.join(text.split()).lower()
        return text

    def listen_for_command(self):
        text = raw_input("What would you like me to do: ")
        text = ' '.join(text.split()).lower()
        return text

执行器(执行给定命令的类):

class Executor:
    #TODO: Define default path
    def __init__(self,parser, audio_path='../Misc/audio.wav'):
        self.command_parser = parser
        self.audio_path = audio_path

    def is_hotword(self,hotword):
        return self.command_parser.is_hotword(hotword)

    def has_matching_command(self,command):
        return self.command_parser.has_matching_command(command)

    def execute_command(self,command):
        val = self.command_parser.getCommand(command)
        print val
        val = os.system(val) #So we don't see the return value of the command

命令文件解析器:

class KeyNotFoundException(Exception):
    pass


class YAMLParser:
    THRESHOLD = 0.6

    def __init__(self,path='Configurations/commands.yaml'):
        with open(path,'r') as f:
            self.parsed_yaml = yaml.load(f)

    def getCommand(self,key):
        try:
            matching_command = self.find_matching_command(key)
            return self.parsed_yaml["Commands"][matching_command]
        except KeyError:
            raise KeyNotFoundException("No key matching {}".format(key))

    def has_matching_command(self,key):
        try:
            for command in self.parsed_yaml["Commands"]:
                if jellyfish.jaro_distance(command,key) >=self.THRESHOLD:
                    return True
        except KeyError:
            return False

    def find_matching_command(self,key):
        for command in self.parsed_yaml["Commands"]:
            if jellyfish.jaro_distance(command,key) >=0.5:
                return command

    def is_hotword(self,hotword):
        return jellyfish.jaro_distance(self.parsed_yaml["Hotword"],hotword)>=self.THRESHOLD

示例配置文件:

Commands:
  echo : echo hello


Hotword: start

1 个回答

4

我发现很难理解你问题的背景,但我会尽量回答你的问题。

如何在接收到网络数据时运行程序并执行代码?

正如你在问题中提到的,通常写一个“边走边吃糖”的应用程序的方法是采用线程或事件循环的方式来设计代码。

因为你提到了线程和Twisted(这是一种事件循环的风格),我担心你可能在考虑将这两者混合在一起。

我认为它们是两种根本不同的编程风格(各有优缺点),混合使用通常会导致麻烦。

让我给你一些背景知识来解释一下。

背景:线程与事件编程

线程

  • 如何理解这个概念:

    我有多个任务需要同时完成,而我希望操作系统来决定如何以及何时运行这些任务。

  • 优点:

    • 让一个程序同时使用多个处理器核心的'唯一'方法

      在posix环境中,让一个进程同时在多个CPU核心上运行的唯一方法就是通过线程(通常理想的线程数不超过机器的核心数)

    • 更容易上手

      你可以将原本在线运行的代码简单地放入一个线程中,通常不需要重新设计(如果没有GIL,可能需要一些锁,但稍后再说)

    • 处理需要大量CPU的任务时更简单

      也就是说,在大多数情况下,使用线程解决数学问题比使用事件/异步框架要简单得多。

  • 缺点:

    • Python在处理线程时有特殊问题

      在CPython中,全局解释器锁(GIL)会限制线程的多任务能力(使得线程几乎无用)。避免 GIL是麻烦的,可能会抵消使用线程的便利性。

    • 随着线程(和锁)的增加,事情会迅速变得复杂,参见这个SO: 线程最佳实践

    • 对于IO/用户交互任务很少是最佳选择

      虽然线程非常适合处理少量需要大量CPU的任务(理想情况下每个核心一个线程),但对于需要等待的大量任务,线程的效果就差很多。

  • 最佳使用场景:

    计算密集型任务。

    如果你有大量的数学运算需要并行处理,几乎不可能比操作系统更智能地安排CPU的使用。

    (考虑到CPython的GIL问题,线程不应该用于数学运算,而应该使用内部实现了线程的库(比如NumPy))

事件循环/异步编程

  • 如何理解这个概念:

    我有多个任务需要同时完成,但我(程序员)希望直接控制如何以及何时运行我的子任务

  • 你应该如何思考你的代码:

    把所有子任务看作一个大而复杂的整体,你的脑海中应该始终想着“这段代码运行得够快吗?不会影响我管理的其他子任务吧?”

  • 优点:

    • 在网络/IO/UI连接方面非常高效,包括处理大量连接

      事件循环风格的程序是解决c10k 问题的关键技术之一。像Twisted这样的框架可以在一台小机器上处理成千上万的连接。

    • 随着其他连接/任务的增加,复杂度的增加是可预测的(小幅度)

      虽然有相当陡峭的学习曲线(特别是在Twisted中),但一旦理解了基础,添加新的连接类型到项目中时,整体复杂度的增加是最小的。从提供键盘接口的程序转变为提供键盘/telnet/web/ssl/ssh连接的程序,可能只需要为每个接口添加几行代码(...这因框架而异,但无论框架如何,事件循环的复杂度在连接数量增加时更可预测)

  • 缺点:

    • 更难入门。

      事件/异步编程需要从第一行代码开始就采用不同的设计风格(不过你会发现这种设计风格在不同语言之间是可以移植的)

    • 一个事件循环,一个核心

      虽然事件循环可以让你同时处理大量的IO连接,但它本身不能在多个核心上同时运行。通常的解决方法是以这样的方式编写程序,使得可以同时运行多个程序实例,每个核心一个(这种技术可以绕过GIL问题)

    • 多路复用高CPU任务可能很困难

      事件编程需要将高CPU任务切割成小块,使每块占用的CPU时间(理想情况下是可预测的)很小,否则事件系统在运行高CPU任务时就无法进行多任务处理。

  • 最佳使用场景:

    基于IO的任务

总结 - 根据你的问题,考虑使用Twisted而不是线程

虽然你的应用程序似乎并不完全基于IO,但也没有明显的CPU密集型任务(看起来你目前是通过system调用播放音频的,system每次调用时都会启动一个独立的进程,所以它的工作不会占用你进程的CPU - 不过system会阻塞,因此在Twisted中不推荐使用 - 你需要使用Twisted中的不同调用)。

你的问题也没有表明你关心如何最大化多个核心的使用。

因此,考虑到你特别提到了Twisted,而事件循环解决方案似乎最适合你的应用,我建议你使用Twisted而不是线程。

鉴于上面列出的“最佳使用场景”,你可能会觉得混合使用Twisted和线程是个好主意,但如果这样做,一旦你做错了任何稍微的事情,你就会失去事件循环(会阻塞)和线程(GIL不允许线程多任务处理)的优势,结果会变得非常复杂,且没有任何好处。

上面提到的方案是否过于复杂?在尝试以这种方式实现之前,我希望能得到一些见解。

你给出的“方案”是:

经过一些思考,我想不出其他实现方式,除了:

  1. 让上述程序作为进程#1运行(我在开头写的本地运行的程序)。
  2. 一个Twisted客户端作为进程#2运行,接收来自远程客户端的命令。每当接收到命令时,Twisted客户端将初始化一个线程来解析命令,检查其有效性,并在有效时执行。

由于我对线程没有太多经验,对网络编程更是一无所知,所以我想不出其他合理的方案。

对于“这个方案是否过于复杂”,我几乎可以肯定地说是的,因为你提到了Twisted线程。(见上面的总结)

根据我对你想要构建的东西的理解(虽然可能不够完整和清晰),我想一个Twisted的解决方案可能会是:

  1. 仔细研究krondo Twisted入门(这真的需要逐行跟踪示例代码,但如果你愿意花时间去做,这是一个很棒的学习工具,适合Twisted和事件编程)
  2. 从头开始用你在krondo指南中学到的知识重写你的“热词”功能,开始时只提供你当前拥有的接口(键盘?)
  3. 将其他通信接口添加到代码中(telnet、web等),以便让你访问为键盘接口编写的处理代码。

如果,正如你在问题中所说的,你真的需要一个服务器,你可以编写第二个Twisted程序来提供这个功能(你会在krondo指南中看到所有这些示例)。不过我猜,当你理解了Twisted的库支持后,你会意识到你不需要构建额外的服务器,只需在基础代码中包含所需的协议即可。

撰写回答