• [Python网络编程]浅析守护进程后台任务的设计与实现


        在做基于B/S应用中。常常有须要后台执行任务的需求,最简单比方发送邮件。在一些如防火墙,WAF等项目中,前台仅仅是为了展示内容与各种參数配置。后台守护进程才是重头戏。所以在防火墙配置页面中可能会常常看到调用cgi。但真正做事的一般并非cgi,比方说执行关机命令,他们的逻辑例如以下:


       (ps:上图所说的前台界面包括通常web开发中的后端,不然也没有socket一说)


        为什么要这么设计

    你可能疑惑为什么要这么设计,我认为理由例如以下:
    首先有一点说明,像防火墙等基本上都执行在类Linux平台上
        1.安全问题  cgi一般也就拥有www权限,但执行关键等命令须要root。所以须要让后台守护进程去干
        2.一般相似防火墙的后台守护进程是C/C++写的,在消息格式上非常方便处理。如填充一个消息结构体发送出去,后台进程仅仅须要强制转换为定义的结构体。就轻松获得传递的參数值。


    那可不能够去掉中间的cig模块。直接发送消息给后台守护进程呢?
    我认为是能够的。本文的重点也是实现这个方法。


    怎样实现

    因为近期一直在windows下,所以我们的守护进程是执行在windows下的。但事实上windows并没有守护进程的概念。相相应的是服务的概念。这里须要安装pywin32包。

    class MgrService(win32serviceutil.ServiceFramework): 
        """
        Usage: 'python topmgr.py install|remove|start|stop|restart'
        """
        #服务名
        _svc_name_ = "Mgr"
        #服务显示名称
        _svc_display_name_ = "Daemon Mgr"
        #服务描写叙述
        _svc_description_ = "Daemon Mgr"
    
        def __init__(self, args): 
            win32serviceutil.ServiceFramework.__init__(self, args) 
            self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
    
        def SvcDoRun(self):
            self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
            INFO("mgr startting...")
            self.ReportServiceStatus(win32service.SERVICE_RUNNING)
            self.start()
            # 等待服务被停止
            INFO("mgr waitting...")
            win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
            INFO("mgr end")
            
        def SvcStop(self): 
            self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
            INFO("mgr stopping...")
            self.stop()
            INFO("mgr stopped")
            # 设置事件
            win32event.SetEvent(self.hWaitStop)
            self.ReportServiceStatus(win32service.SERVICE_STOPPED)
    
        def start(self): pass
    
        def stop(self): pass
    
    非常easy。这样就实现了windows中的服务,也就是说脱离终端。执行于后台。

    INFO等函数仅仅是简单的记录作用。可直接忽略。

    我们要实现自己的后台程序,仅仅须要继承MgrService,并提供start,stop方法就能够了。


    因为我们是通过socket来传递消息的,所以在start方法中要监听端口,等待连接。处理连接。这个大家都非常擅长。在这里我选择了
     单线程。基于协程,底层使用libev(libevent)--- gevent这个高性能网络库。对gevent有兴趣的童鞋能够看看深度分析gevent执行流程

    class Engine(MgrService):
        rbufsize = -1
        wbufsize = 0
    
        def start(self):
            INFO('wait connection')
            self.server = StreamServer((HOST, PORT), self.msg_handle)
            self.server.serve_forever()
    
        def msg_handle(self,socket,address):
            try:
                rfile = socket.makefile('rb', self.rbufsize)
                wfile = socket.makefile('wb', self.wbufsize)
                headers = Message(rfile).dict
    
                INFO('get a connection from:%s,headers:%s' % (str(address), headers))
    
                if 'module' in headers and headers['module'] in MODULES:
                    MODULES[headers['module']].handle(wfile, headers)
            except Exception:
                ERROR('msg_handle exception,please check')
    
        def stop(self):
            if hasattr(self, server):
                self.server.stop()
    
    当有新连接到来,由msg_handle处理,首先读取发送来的消息。消息格式使用了最简单的http的格式,即(键名:键值)的格式,你要问我为什么採用这个格式,哈哈,格式简单,python有现成的库解析。

    考虑到后期模块可能非常多,所以我们的处理流程自己主动依据消息的模块參数,调用相应模块的handle方法。
    上面代码的那个MODULES是个全局变量,当你加入一个模块的时候须要注冊到MODULES中,我提供了module_register方法。
    MODULES = {           # module: handle module class
    }
    
    def module_register(module_name, handle_class):
        if module_name in MODULES:
            WARN('duplicate module_name:' + module_name)
        else:
            MODULES[module_name] = handle_class
    

    到这里一切都非常自然,但貌似仅仅如果模块有handle方法,自己写一个模块还是非常费事,你须要自己去想怎么调用,最有返回什么格式的数据,这都是一件头疼的事情,所以最好提供一个基类模块。
    class Module(object):
        SECRE_KEY = "YI-LUO-KEHAN"
        MODULE_NAME = "BASE_MODULE"
        PREFIX = "do_"  # method prefix
    
        def __init__(self, wfile, headers):
            self.wfile = wfile
            self.headers = headers
    
        def __getattr__(self, name):
            try:
                return self.headers[name]
            except Exception:
                ERROR("%s has no attr:%s,please check" %(self.MODULE_NAME, name))            
    
        @classmethod
        def handle(cls, wfile, headers):
            module_obj = cls(wfile, headers)
            module_obj.schedule_default()
    
        def verify(self):
            if hmac.new(self.SECRE_KEY, self.MODULE_NAME).hexdigest() == self.signature:
                return True
            else:
                WARN("client verify failed,signature:%s" % str(self.signature))
    
        def schedule_default(self):
            err_code = 0
            if self.verify() and self.action:
                func_name = self.PREFIX + self.action
                try:
                    getattr(self, func_name)()
                except AttributeError:
                    err_code = 1
                    ERROR("%s has no method:%s" %(self.MODULE_NAME, func_name))
                except Exception:
                    err_code = 2
                    ERROR("module:%s,method:%s,exception" % (self.MODULE_NAME, func_name))              
            else:
                err_code = 3
    
            if err_code:
                self.send_error({'err_code':err_code})
    
        def send_success(self, msg=''):
            data = {'success':True,'msg':msg}
            self.wfile.write(json.dumps(data))
    
        def send_error(self, msg=''):
            data = {'success':False,'msg':msg}
            self.wfile.write(json.dumps(data))

    在基类模块中我们提供了默认的处理流程,即依据消息中action,调用do_action方法。并提供了一个简单但非常有效的认证方法,通过消息的signature字段,可能有些简陋。但没关系,你能够定义自己的认证方法。

    以下该写我们自己的模块了,
    TASK = {}  # task_id: pid
    class ScanModule(Module):
        MODULE_NAME = "SCAN_MODULE"
    
        def do_start(self):
            self.send_success('start ok')
            DEBUG('------------task start------------')
            task_ids = [int(task_id) for task_id in self.task_ids.split(',') if int(task_id) not in TASK]
    
            for task_id in task_ids:
                try:
                    cmd = 'python scan.py -t %s' % task_id
                    DEBUG(cmd)
                    self.sub = Popen(cmd, shell=True, cwd=CWD)
                    pid = int(self.sub.pid)
                    TASK[task_id] = pid
                    INFO('%s start a new task,task_id:%s,pid:%s' %(self.MODULE_NAME, task_id, pid))
                except Exception:
                    ERROR('%s start a new task,task_id:%s failed' % (self.MODULE_NAME, task_id))
    
        def do_stop(self):
            self.send_success('stop ok')
            DEBUG('------------task stop------------')
            task_ids = [int(task_id) for task_id in self.task_ids.split(',') if int(task_id) in TASK]
    
            for task_id in task_ids:
                pid = TASK.pop(task_id)
                try:
                    INFO('%s stop a new task,task_id:%s,pid:%s' %(self.MODULE_NAME, task_id, pid))
                    call(['taskkill', '/F', '/T', '/PID', str(pid)])
                except Exception:
                    ERROR('%s taskkill a task failed,task_id:%s,pid:%s' %(self.MODULE_NAME, task_id, pid))
    
    
    module_register(ScanModule.MODULE_NAME, ScanModule)
    
    上面实现了一个简单的扫描模块。支持两个action,start,stop。
    start非常easy。调用gevent的subprocess.Popen执行子进程,并记录pid,stop则使用taskkill直接杀掉该进程。
    这里有两点须要注意:
        1.不要用原生的subprocess模块。因为原生的subprocess是堵塞的,这可能导致主处理逻辑也堵塞,不能服务很多其它的请求
    最后别忘了调用module_register注冊相应模块。
        2.方法一開始最好就返回结果。因为前台非常可能在等待返回。

    所以说as soon as possible


    以下提供一个客户端用于測试。client.py
    #!/usr/bin/env python
    #-*-encoding:UTF-8-*-
    
    import hmac
    import gevent
    from gevent import monkey
    monkey.patch_socket()
    
    addr = ('localhost', 6667)
    
    
    def send_request(module_name,request_headers):
        SECRE_KEY = "YI-LUO-KEHAN"
        socket = gevent.socket.socket()
        socket.connect(addr)
        request_headers['module'] = module_name
        request_headers['signature'] = hmac.new(SECRE_KEY, module_name).hexdigest()
        h = ["%s:%s" %(k, v) for k,v in request_headers.iteritems()]
        h.append('
    ')
        request = '
    '.join(h)
        socket.send(request)
        print socket.recv(8192)
        socket.close()
    
    if __name__ =="__main__":
        import sys
        if sys.argv[1] == 'start':
            send_request('SCAN_MODULE',{'action':'start','task_ids':'1'})
        else:
            send_request('SCAN_MODULE',{'action':'stop','task_ids':'1'})
    
        
        

    我们来简单的測试一下:
    注意:因为要注冊到服务,cmd须要管理员权限
    至于start中调用的scan.py随便写一个就能够

    截图例如以下,我们看到成功!



    本文代码已放到github,https://github.com/Skycrab/pymgr
    感兴趣的童鞋能够參考,请大家多提意见。




  • 相关阅读:
    Benelux Algorithm Programming Contest 2016 Preliminary K. Translators’ Dinner(思路)
    Benelux Algorithm Programming Contest 2016 Preliminary Target Practice
    Benelux Algorithm Programming Contest 2016 Preliminary I. Rock Band
    Benelux Algorithm Programming Contest 2016 Preliminary A. Block Game
    ICPC Northeastern European Regional Contest 2019 Apprentice Learning Trajectory
    ICPC Northeastern European Regional Contest 2019 Key Storage
    2018 ACM ICPC Asia Regional
    2018 ACM ICPC Asia Regional
    Mybatis入库出现异常后,如何捕捉异常
    优雅停止 SpringBoot 服务,拒绝 kill -9 暴力停止
  • 原文地址:https://www.cnblogs.com/lytwajue/p/6893545.html
Copyright © 2020-2023  润新知