• [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
    感兴趣的童鞋能够參考,请大家多提意见。




  • 相关阅读:
    CORS详解
    JBoss 系列九十九:Rest WebService jBPM 6 集成演示样例
    atitit。浏览器缓存机制 and 微信浏览器防止缓存的设计 attilax 总结
    4G时代来临,运营商为谁搭台献唱?
    Pascal's Triangle II
    cocos2d-x 3.6版连连看载入资源
    SlidingMenu导入编译用法--Eclipse和IDEA
    【解决】hive动态添加partitions不能超过100的问题
    AngularJS clone directive 指令复制
    AndroidStudio文件夹结构视图讲解
  • 原文地址:https://www.cnblogs.com/lytwajue/p/6893545.html
Copyright © 2020-2023  润新知