跨平台PHP调试器设计及使用方法——通信
? ? ? ? 首先引用《跨平臺PHP調試器設計及使用方法——探索和設計》中的結構圖(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 本文要介紹的是我們邏輯和pydbgp通信的實現(圖中紅框內內容)。
? ? ? ? 設計通信之前,我需要先設計一種通信協議,其實就是一個數據打包和解包的協議。因為我們的數據非常簡單,所以只是用“”長度+數據“”的結構。我們規(guī)定一個包的前8個字節(jié)表示數據的總長度(包括這個8個字節(jié)的長度),然后跟著的就是數據。
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則是在數據接收完畢后,接收方調用獲取完整的數據。
? ? ? ? 因為服務端和客戶端都存在數據打包發(fā)送和解包的工作,所以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類在構造函數中暴露了一個參數,用于指定處理接收到數據的函數入口地址。這樣我們就讓服務器通信這塊邏輯和數據處理業(yè)務解耦。而全局deal_data方法,則是在用戶沒有傳入處理數據的函數指針時的一個替代品,它沒有任何作用,只是為了保證代碼的嚴謹性。在Start函數中,我們啟動了一個用于接收和處理數據的線程。相應的Stop方法則是終止該線程執(zhí)行。這個類的核心是線程函數_worker的實現。它在本地綁定了9001端口,然后不停的從該端口讀取數據。如果協議類socket_protocol的對象判斷本次讀取數據已經完畢,它就會調用構造函數中傳入的方法處理獲取的數據,然后將該方法返回的數據打包后發(fā)給請求方。
? ? ? ? 客戶端的實現則稍微復雜點
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函數分別用于啟動和關閉一個發(fā)送請求接收數據的線程。線程函數_worker也和socket_server中類似,只是它要不停判斷self._cmd是否有數據,也就是是否有請求進來。如果有請求進來,它就將該請求通過socket_protocol打包發(fā)送給服務端,然后在從服務端取回結果并解包,把解包后的結果放入self._result中。通過self._response_ready.set()設置事件告知業(yè)務方請求完畢,可以來拿結果了。Query函數就是業(yè)務方調用的入口,它使用鎖操作保證每次只能有一次查詢行為。然后通過設置self._cmd告知線程要向服務器發(fā)送該指令,然后通過等待線程設置self._response_ready事件來等待請求返回,并把結果返回給調用方。客戶端設計的比較復雜的一個重要原因是我們這個模型要求請求是有序的。因為在測試過程中,我發(fā)現pydbgp是非常脆弱的,往往因為一些不合常理的查詢順序導致整個程序都死掉。所以我把“有序”的特性設計在了客戶端基礎類中。
? ? ? ? 看完基礎類,我們再來看看我們需要控制的pydbgp是怎么被調用的。在《跨平臺PHP調試器設計及使用方法——探索和設計》一文中,我說明過我只是想把pydbgp當成一個工具來使用,而盡量不要對其源碼有任何改動——除非有bug。因為pydbgp不能像API一樣使用,所以我只能模擬標準輸入輸出來達到和它的交互。而如果標準輸入輸出被改變,將影響整個程序,所以為了避開這種設計對我們自己的代碼及其他第三方庫的影響。我們需要將pydbgp作為一個獨立的進程來執(zhí)行。
? ? ? ?我們需要重定向標準輸入和輸出,于是我設計了一個重定向之后的輸入類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在調試過程中分為兩種狀態(tài),一種是調試某個session的階段,就是下圖中4的過程,以后我們稱該階段為session階段;另外一種是不調試任何session的階段,即除去4之外的其他階段,之后我們稱該階段為no_session階段。 因為pydbgp比較脆弱,不能在不同階段調用另一個階段的命令,輕則告知出錯,重則整個程序都死掉。所以我們必須在執(zhí)行每條指令后判斷其所處于的階段,而這種判斷規(guī)則則和其返回的數據特征有關。
? ? ? ? 而作為命令發(fā)起方,在發(fā)起一個命令后,可以獲取命令執(zhí)行的結果。而對于當前pydbgp處于什么階段,則也需要知道,否則不能保證發(fā)送的下條命令會不會把pydbgp搞掛掉。所以我就在返回結果中加入一些特征,使得命令發(fā)起方可以得知指令執(zhí)行后的調試器階段信息。具體的做法就是在數據后加入特征碼,這個邏輯是在_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所處的階段。我們發(fā)現如果處在session階段,則返回的數據是以“[dbgp-”開頭的;如果是no_session階段,則是“[dbgp]”開頭的。利用這個特征,我們在write函數中分析出所處階段,并告知_send_data發(fā)送什么樣的數據。
? ? ? ? 剩下的工作便是讓整個程序的標準輸入和輸出被重定向,還有就是啟動通信的服務端。
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在啟動服務器進程時,區(qū)分了不同的操作系統。這也是沒有辦法的事,因為不同系統里,終止子進程和孫子進程的方法不能通用。
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方法用于告知調用方當前調試器處在什么階段(我用的是階段而非狀態(tài),狀態(tài)我將用于session階段中調試器的情況描述)。query方法則是請求服務端獲取請求結果并更改調試器階段信息。于是調用方只要調用query方法就可以發(fā)起調試命令,就像調用本地方法一樣。
總結
以上是生活随笔為你收集整理的跨平台PHP调试器设计及使用方法——通信的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跨平台PHP调试器设计及使用方法——探索
- 下一篇: 跨平台PHP调试器设计及使用方法——协议