首先引用《跨平台PHP调试器设计及使用方法——探索和设计》中的结构图(转载请指明出于breaksoftware的csdn博客)

本文要介绍的是我们逻辑和pydbgp通信的实现(图中红框内内容)。

设计通信之前,我需要先设计一种通信协议,其实就是一个数据打包和解包的协议。因为我们的数据非常简单,所以只是用“”长度+数据“”的结构。我们规定一个包的前8个字节表示数据的总长度(包括这个8个字节的长度),然后跟着的就是数据。

class socket_protocol:def __init__(self):self.response = ""passdef pack_request(self, request):request_len = len(request) + 8package = '{:0>8}'.format(request_len)package += requestreturn packagedef input_response(self, data):self.response += datadef data_valid(self):if len(self.response) < 8:return Falselength = self.response[:8]if int(length) == len(self.response):return Trueelse:return Falsedef clear(self):self.response = ""def get_response(self):if False == self.data_valid():return ""return self.response[8:]

pack_request用于将数据打包组装;input_response是为了让数据接收方可以一直接收数据(因为不是每次调用input_response就可以把所有数据都读取,这在数据量很大是比较常见);data_valid用于检测接收到的数据是否已经接收完毕,因为数据接收并非一次性完成,所以我们需要一个逻辑判断是否还需要接收数据。因为我们的数据有严密的结构,我们可以通过接收的数据长度判断数据是否接收完毕。get_response则是在数据接收完毕后,接收方调用获取完整的数据。
        因为服务端和客户端都存在数据打包发送和解包的工作,所以socket_protocol将是整个通信数据的基础类。

我们再看下稍微简单点的服务器代码

import os
import time
import socket
import threading
from socket_protocol import socket_protocoldef deal_data(data):send_data = "recv" + datareturn send_dataclass socket_server:def __init__(self, deal_func = None):self._stop_event = threading.Event()self._communicate_thread = Noneif deal_func:self.deal_func = deal_funcelse:self.deal_func = deal_datapassdef __del__(self):self.Stop()passdef Start(self):if self._communicate_thread:returnself._communicate_thread = threading.Thread(target=self._worker)self._communicate_thread.start()def _worker(self):self._stop_event.clear()s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.bind(("localhost", 9001))s.listen(1)con,addr = s.accept()s_data = socket_protocol()while False == self._stop_event.wait(0.1):data = con.recv(1024)s_data.input_response(data)if s_data.data_valid():send_data = self.deal_func(s_data.get_response())con.sendall(s_data.pack_request(send_data))s_data.clear()con.close()def Stop(self):self._stop_event.set()while self._communicate_thread.is_alive():time.sleep(0.01)self._communicate_thread = None

socket_server类在构造函数中暴露了一个参数,用于指定处理接收到数据的函数入口地址。这样我们就让服务器通信这块逻辑和数据处理业务解耦。而全局deal_data方法,则是在用户没有传入处理数据的函数指针时的一个替代品,它没有任何作用,只是为了保证代码的严谨性。在Start函数中,我们启动了一个用于接收和处理数据的线程。相应的Stop方法则是终止该线程执行。这个类的核心是线程函数_worker的实现。它在本地绑定了9001端口,然后不停的从该端口读取数据。如果协议类socket_protocol的对象判断本次读取数据已经完毕,它就会调用构造函数中传入的方法处理获取的数据,然后将该方法返回的数据打包后发给请求方。
        客户端的实现则稍微复杂点

import os
import time
import socket
import threading
from socket_protocol import socket_protocolclass socket_client:def __init__(self):self._response_ready = threading.Event()self._stop_event = threading.Event()self._lock_excute = threading.Lock()self._communicate_thread = Noneself._cmd = ""self._result = ""passdef __del__(self):self.Stop()passdef Query(self,cmd):self._lock_excute.acquire()self._cmd = cmdself._response_ready.clear()self._response_ready.wait()result = self._resultself._lock_excute.release()return resultdef _worker(self):self._stop_event.clear()s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.connect(("localhost", 9001))s_data = socket_protocol()while False == self._stop_event.wait(0.1):query = ""if len(self._cmd) == 0:continuequery = self._cmd self._cmd = ""send_data = s_data.pack_request(query)s.sendall(send_data)while False == self._stop_event.wait(0.1):data = s.recv(1024)s_data.input_response(data)if s_data.data_valid():self._result = s_data.get_response()s_data.clear()self._response_ready.set()breaks.close()def Start(self):if self._communicate_thread:returnself._communicate_thread = threading.Thread(target=self._worker)self._communicate_thread.start()def Stop(self):self._stop_event.set()while self._communicate_thread.is_alive():time.sleep(0.01)self._communicate_thread = None

Start和Stop函数分别用于启动和关闭一个发送请求接收数据的线程。线程函数_worker也和socket_server中类似,只是它要不停判断self._cmd是否有数据,也就是是否有请求进来。如果有请求进来,它就将该请求通过socket_protocol打包发送给服务端,然后在从服务端取回结果并解包,把解包后的结果放入self._result中。通过self._response_ready.set()设置事件告知业务方请求完毕,可以来拿结果了。Query函数就是业务方调用的入口,它使用锁操作保证每次只能有一次查询行为。然后通过设置self._cmd告知线程要向服务器发送该指令,然后通过等待线程设置self._response_ready事件来等待请求返回,并把结果返回给调用方。客户端设计的比较复杂的一个重要原因是我们这个模型要求请求是有序的。因为在测试过程中,我发现pydbgp是非常脆弱的,往往因为一些不合常理的查询顺序导致整个程序都死掉。所以我把“有序”的特性设计在了客户端基础类中。

看完基础类,我们再来看看我们需要控制的pydbgp是怎么被调用的。在《跨平台PHP调试器设计及使用方法——探索和设计》一文中,我说明过我只是想把pydbgp当成一个工具来使用,而尽量不要对其源码有任何改动——除非有bug。因为pydbgp不能像API一样使用,所以我只能模拟标准输入输出来达到和它的交互。而如果标准输入输出被改变,将影响整个程序,所以为了避开这种设计对我们自己的代码及其他第三方库的影响。我们需要将pydbgp作为一个独立的进程来执行。
       我们需要重定向标准输入和输出,于是我设计了一个重定向之后的输入类input_redirection,其核心的就两个函数

    def readlines(self, size=-1):while len(self._data) == 0:time.sleep(0.01)self._lock_excute.acquire()ret_data = self._dataself._data = ""self._lock_excute.release()return ret_datadef write(self, data):self._lock_excute.acquire()logging.debug("reqeust: " + data)self._data = dataself._lock_excute.release()

write函数用于从服务器中接收请求的内容,然后重定向之后的输入通过readlines读取内容。从而达到模拟请求的目的。

而重定向标准输出类则相对复杂点,因为它要牵扯到数据的内容。pydbgp在调试过程中分为两种状态,一种是调试某个session的阶段,就是下图中4的过程,以后我们称该阶段为session阶段;另外一种是不调试任何session的阶段,即除去4之外的其他阶段,之后我们称该阶段为no_session阶段。 因为pydbgp比较脆弱,不能在不同阶段调用另一个阶段的命令,轻则告知出错,重则整个程序都死掉。所以我们必须在执行每条指令后判断其所处于的阶段,而这种判断规则则和其返回的数据特征有关。

而作为命令发起方,在发起一个命令后,可以获取命令执行的结果。而对于当前pydbgp处于什么阶段,则也需要知道,否则不能保证发送的下条命令会不会把pydbgp搞挂掉。所以我就在返回结果中加入一些特征,使得命令发起方可以得知指令执行后的调试器阶段信息。具体的做法就是在数据后加入特征码,这个逻辑是在_send_data中实现的。

class output_redirction:_out = None_query_event = None_data = ""_response = ""def __init__(self, query_event):self._out = sys.stdoutself._query_event = query_eventdef write(self, output_stream):if re.match('^\[dbgp-', output_stream):self._send_data(True)elif re.match('^\[dbgp\]', output_stream):self._send_data(False)else:self._data += output_streamdef flush(self):passdef _send_data(self, is_seesion):if (is_seesion):end_ch = "@\n"else:end_ch = ":\n"data = base64.b64encode(self._data) + end_chlogging.debug("response:" + self._data + end_ch)self._response = dataself._query_event.set()self._out.write(data)self._data = "" self._out.flush()def get_reponse(self):return self._response

另一个问题就是我们如何判断当前pydbgp所处的阶段。我们发现如果处在session阶段,则返回的数据是以“[dbgp-”开头的;如果是no_session阶段,则是“[dbgp]”开头的。利用这个特征,我们在write函数中分析出所处阶段,并告知_send_data发送什么样的数据。
        剩下的工作便是让整个程序的标准输入和输出被重定向,还有就是启动通信的服务端。

query_event = threading.Event()
out_r = output_redirction(query_event)
in_r = input_redirection()
sys.stdin = in_r
sys.stdout = out_r
sys.stderr = out_rdef Query(cmd):logging.debug("query " + cmd)query_event.clear()in_r.write(cmd)query_event.wait()return out_r.get_reponse()if __name__ == "__main__":cmd_server = socket_server(Query)cmd_server.Start()sys.exit(main([0]))

和服务端相对应的,则存在一个与其交互的客户端。它便是本文最开始结构图中的pydbgpd_stub模块,之所以取名为stub是为了让调用pydbgp像直接调用一样。在pydbgpd_stub中,它明确了pydbgp处于不同阶段可以调用的不同的命令——分别保存在_session_cmd和_no_session_cmd连个数组中。由于命令比较长,这儿就不列出来了。

    def __init__(self):self._exc_cmd = "python pydbgpd_proxy.py"self._lock_excute = threading.Lock()self._cmd_client = socket_client()def _is_cmd_valid(self, cmd, cmd_list):for item in cmd_list:if cmd.startswith(item):return Truereturn Falsedef start(self):if (self._exc_cmd == None):raise NameError("exc_cmd is none")if "Windows" == platform.system():self._process = subprocess.Popen(self._exc_cmd, shell = False)else:self._process = subprocess.Popen(self._exc_cmd, shell = True,  preexec_fn = os.setpgrp)time.sleep(2)self._cmd_client.Start()

pydbgpd_stub在启动服务器进程时,区分了不同的操作系统。这也是没有办法的事,因为不同系统里,终止子进程和孙子进程的方法不能通用。

    def stop(self):self._cmd_client.Stop()if not self._process:raise NameError("subprocess is none")else:if "Windows" != platform.system():pid = self._process.pidpgid = os.getpgid(pid)os.kill(-pgid, 9)self._process.terminate()self._process.kill()self._process = None

接下来我们要看其暴露的最主要的两个方法

    def is_session(self):self._lock_excute.acquire()is_session = self._is_sessionself._lock_excute.release()return is_sessiondef query(self, query_cmd):data = ""if self._is_session:if not self._is_cmd_valid(query_cmd, self._session_cmd):return "invalid cmd"else:if not self._is_cmd_valid(query_cmd, self._no_session_cmd):return "invalid cmd"self._lock_excute.acquire()data = self._cmd_client.Query(query_cmd)if len(data) > 1:if data[-2] == "@":print "Switch to Session \n"self._is_session = Trueelif data[-2] == ":":self._is_session = Falseprint "Switch to No Session \n"data = base64.b64decode(data[:-2])self._lock_excute.release()return data  

is_session方法用于告知调用方当前调试器处在什么阶段(我用的是阶段而非状态,状态我将用于session阶段中调试器的情况描述)。query方法则是请求服务端获取请求结果并更改调试器阶段信息。于是调用方只要调用query方法就可以发起调试命令,就像调用本地方法一样。

跨平台PHP调试器设计及使用方法——通信相关推荐

  1. 跨平台PHP调试器设计及使用方法——协议解析

    在<跨平台PHP调试器设计及使用方法--探索和设计>一文中,我介绍了将使用pydbgp作为和Xdebug的通信库,并让pydbgp以(孙)子进程的方式存在.<跨平台PHP调试器设计及 ...

  2. 跨平台PHP调试器设计及使用方法——界面设计和实现

    一个优秀的交互设计往往会影响一个产品的命运.在设计这款调试器时,我一直在构思这款调试器该长什么样子.简单.好用是我设计的原则,于是在<跨平台PHP调试器设计及使用方法--立项>一文中,我给 ...

  3. 跨平台PHP调试器设计及使用方法——高阶封装

    在<跨平台PHP调试器设计及使用方法--协议解析>一文中介绍了如何将pydbgp返回的数据转换成我们需要的数据.我们使用该问中的接口已经可以构建一个简单的调试器.但是由于pydbgp存在的 ...

  4. 跨平台PHP调试器设计及使用方法——探索和设计

    在<跨平台PHP调试器设计及使用方法--立项>一文中,我确定了使用xdebug作为调试器插件部分的基础组件.xdebug提供了一个远程调试的功能(相关资料可以详见https://xdebu ...

  5. 跨平台PHP调试器设计及使用方法——拾遗

    之前七篇博文讲解了跨平台PHP调试器从立项到实现的整个过程,并讲解了其使用方法.但是它们并不能全部涵盖所有重要内容,所以新开一片博文,用来讲述其中一些杂项.(转载请指明出于breaksoftware的 ...

  6. 跨平台PHP调试器设计及使用方法——立项

    作为一个闲不住且希望一直能挑战自己的人,我总是在琢磨能做点什么.自从今年初开始接触PHP,我也总想能在这个领域内产生点贡献.那能做点什么呢?我经常看到很多phper说自己设计了一个什么框架,或者说自己 ...

  7. 跨平台PHP调试器设计及使用方法——使用

    经过之前六篇博文的分析和介绍,大家应该对这套调试器有个初步的认识.本文我将讲解它的使用方法.(转载请指明出于breaksoftware的csdn博客) 上图是该软件界面的布局,我们之后的讲解也将围绕着 ...

  8. 【MPC5744P】劳特巴赫调试器Trace32的使用方法

    对于大部分MCU来说,官方IDE一般都带有调试功能,配合JTAG接口使用即可,也支持变量查看.断点等功能.绝大多数工业开发的中小型程序,使用自带调试器即可,价格便宜,几十到几千不等. 在某些特殊领域( ...

  9. 西子奥的斯系统服务器进不去,西子奥的斯电梯服务调试器故障及解决方法

    奥的斯电梯服务调试器的 16 个按键分两个部分: 左边部分四个分别是: MODULE . FUNCTION. SET .蓝键 1.MODULE 在服务工具软件的任何地方返回到服务工具主菜单. 2.FU ...

最新文章

  1. PHP中htmlentities跟htmlspecialchars的区别
  2. QQ亿级日活跃业务后台核心技术揭秘
  3. 【C++ 语言】面向对象 ( 类定义 | 限制头文件引用次数 | 构造方法 | 析构方法 )
  4. Mule web service调用中的复杂类型传递
  5. SAP Analytics Cloud导入数据的规模限制
  6. leetcode116. 填充每个节点的下一个右侧节点指针
  7. 并查集——营救(洛谷 P1396)
  8. 引用和指针自增的不同
  9. 点阵汉字的字模读取与显示
  10. 单节磷酸铁锂电池充电管理芯片IC,ASC4055B,SOT23-5
  11. 用诺模图可视化你的模型
  12. QGIS基于多期哨兵2影像遥感指数阈值法提取冬小麦分布(1)-数据预处理
  13. Pandas+Pyecharts | 某APP大学生用户数据分析可视化
  14. 甲骨文数据库基本知识
  15. 递推与递归 (区别)
  16. 如何构建虚拟机Hadoop集群,搭建3台ubuntu虚拟机集群
  17. mysql jdbc batch_JDBC批处理(batch)
  18. css px em rem % vw vh vm 区别
  19. 2015 kitti 数据集_KITTI 数据集
  20. 借助近距离通信,实现手机作为游戏控制器掌控大屏游戏

热门文章

  1. Ubuntu解决开机屏幕默认亮度偏低问题
  2. 图像识别-opencv
  3. opencv获取图像像素值的坑
  4. ros web_video_server的使用及Python获取实时画面
  5. tensorflow-gpu
  6. XOR and Favorite Number CF340E 莫队算法
  7. linux禁止向指定ip发送数据包,Linux下使用iptables封锁端口禁止邮件发送
  8. 在Ubuntu 14.04.5 LTS上安装python模块selenium 3实录
  9. python练习:猜价钱小游戏
  10. 解决 sh: java: command not found 问题