跨平台PHP调试器设计及使用方法——协议解析
在《跨平台PHP调试器设计及使用方法——探索和设计》一文中,我介绍了将使用pydbgp作为和Xdebug的通信库,并让pydbgp以(孙)子进程的方式存在。《跨平台PHP调试器设计及使用方法——通信》解决了和pydbgp通信的问题,本文将讲解和pydbgp通信协议的问题。(转载请指明出于breaksoftware的csdn博客)
和Xdebug的通信协议不同,和pydbgp的通信协议其实就是对其调用规则和对返回结果解析的规则。这块技术并没有什么高深之处,只是pydbgp的资料很少,其规则也没有相关说明,只能靠查看源码和实践来收集和分析。我尽量以调用顺序来讲解相关协议。
首先,我们需要设置IDE Key参数。这步操作我放在start_debugger函数中
def start_debugger(self):if self._pydbgpd:return {"ret":1}self._pydbgpd = pydbgpd_stub()self._pydbgpd.start()self._pydbgpd.query('key netbeans-xdebug')def stop_debugger(self):if self._pydbgpd:self._pydbgpd.stop()del self._pydbgpdself._pydbgpd = Nonedef is_session(self):if not self._pydbgpd:return Falsereturn self._pydbgpd.is_session()
pydbpgd_stub是《跨平台PHP调试器设计及使用方法——通信》一文介绍的父程序中的“桩”,对它的调用就如同对pydbgpd(子进程中)的调用一样,感受不到跨进程带来的各种不便。stop_debugger用于关闭调试,is_session用于判断调试器是否处在“session阶段”。这些都是通过对pydbgpd_stub对象操作实现的。之后我们所有要和调试器通信的地方都会看到它。
接下来,我们需要告知调试器,我们需要在哪个端口开启监听。这样Xdebug可以通过在配置文件中的配置信息连接到我们开启的端口。
def start_listen(self, param):if False == self._listening:data = self._pydbgpd.query('listen -p localhost:9000 start')#ERROR: dbgp.server: the debugger could not bind on port 9000.if "ERROR" in data:return {"ret":0}self._listening = Truereturn {"ret":1}
start_listen中,我们通过上述第三行的命令启动端口监听。如果调用成功,则没有任何数据返回。如果调用失败,则会返回错误,比如待绑定的端口被占用时,会返回上述第四行的信息。我们通过返回信息中是否包含ERROR来判断该操作是否成功。
如果此时有PHP执行触发了调试,则我们需要查看有哪些调试连接已经接入
def sessions(self,param):data = self._pydbgpd.query('sessions')sessions = []arr = data.split('\n')#data = "#2344:<dbgp.server.application instance at 0x025839E0>"for item in arr:try:if not len(item):continueres = self._sessions_info_pattern.search(item).groups()sessions.append(res[0])except Exception,errinfo:print errinfo, "sessions error:" + data + "\n"return sessions
sessions函数中,我们通过向pydbgp发送“sessions”指令来查看调试接入(会话)信息。上述第五行是一个接入信息的返回数据,如果此时有多条调试接入,则会产生多行信息。我们通过对换行符切分,并对每条数据通过正则提取,获取所有会话号。上述例子中的会话号就是2344。
我们获知会话号后,需要挑选一个需要调试的会话号进行调试,这个时候就需要调用下面的方法
def select(self,param):select_cmd = "select " + paramret = self._pydbgpd.query(select_cmd)if self.is_session():return {"ret":1}return {"ret":0}
select方法传入的是会话号,pydbgp在执行上述第二行的指令后,不会返回任何数据。此时我们可以通过is_session判断调试器是否进入session阶段,如果进入了,则证明执行成功,否则失败。
进入调试后,我们可能需要设置断点(其实没有调试状态也存在设置断点的可能性,而且可能性非常大,所以这种预设性的断点设计也包含在我的设计中,这块在之后的博文中会有介绍。)。Xdebug提供的断点有多种方式,目前我测试的版本尚不支持watch类型,所以这种类型我们也不讨论。我们看下支持的类型:
- 行号断点。这种断点方式非常常见,就是我们需要设定文件路径和断点行号。如果设置成功,则程序执行到该文件该行时将会被中断。
- 调用断点。这种断点需要设置被调用的函数函数名,它将使得程序中断在该函数被调用前。
- 返回断点。这种断点也需要设置被调用的函数函数名,它将使得程序中断在该函数被调用后。
- 异常断点。这种断点需要设置异常的类型名,它将使得程序中断在该种异常被抛出前。
- 条件断点。这种断点需要设置中断时发生的条件。比如我们调试一个循环,我们可以设置索引值等于某个值时被中断。
我们看下这些断点的设置方法
def add_breakpoint(self,breakpointinfo):breakpoint_set_type_keys = {"line" : {"filename":"-f","lineno":"-n"},"call" : {"function":"-m"},"return" : {"function":"-m"},"exception" : {"exception":"-x"},"conditional" : {"filename":"-f","lineno":"-n","expression":"-c"},"watch" : {},}query = "breakpoint_set -t " + breakpointinfo["type"]for (key,value) in breakpoint_set_type_keys[breakpointinfo["type"]].items():if value == "-c":expression_de = base64.b64decode(breakpointinfo[key])query = query + " " + value + " '" + expression_de + " '" #maybe bug if expression_de has 'else:query = query + " " + value + " " + breakpointinfo[key]data = self._pydbgpd.query(query)iteminfo = self._parse_breakpoint_info(data)if not iteminfo:ret = 0else:ret = 1return {"ret":ret, "breakpoint":iteminfo}
以设置行号断点为例,我们最终的调用方式是breakpoint_set -t line -f file:///home/work/xxxx.php -n 10。这儿有点特别的是条件断点的设置,因为条件的内容我们无法控制,所以需要使用base64对其进行编码。pydbgp执行新增断点的请求后会返回该断点的信息(实际信息不全,这也将导致我们之后断点相关的逻辑设计的比较曲折)。
设置完断点后,我们需要查看我们设置了哪些断点。
def breakpoint_list(self, param):data = self._pydbgpd.query("breakpoint_list")#data ="""<dbgp.server.breakpoint: id:11900002 type:line filename:file:///var/www/html/index.php lineno:8 function: state:enabled exception: expression: temporary:0 hit_count:0 hit_value:None hit_condition:None>
#<dbgp.server.breakpoint: id:11900003 type:line filename:file:///var/www/html/index.php lineno:9 function: state:enabled exception: expression: temporary:0 hit_count:0 hit_value:None hit_condition:None>"""info = []arr = data.split('\n') for item in arr:if not len(item):continueiteminfo = self._parse_breakpoint_info(item)if iteminfo:info.append(iteminfo)return info
第三行给出了断点的样例,我们继而调用_parse_breakpoint_info和parse_breakpoint_info方法去提取断点信息
def _parse_breakpoint_info(self, info):iteminfo = {}try:iteminfo = self.parse_breakpoint_info(info)except Exception,errinfo:print errinfo, "_parse_breakpoint_info error:" + info + "\n"return iteminfo#data = "<dbgp.server.breakpoint: id:65920004 type:conditional filename:file:///D:/nginx-1.11.3/html/index.php lineno:30 function: state:enabled exception: expression:$i ==6 temporary:0 hit_count:0 hit_value:None hit_condition:None>"def parse_breakpoint_info(self, data):breakpoint_info = {}keys = ["id","type","filename","lineno","function","state","exception","expression","temporary","hit_count","hit_value","hit_condition"]data_end = data.rfind(">")for key_index in range(0, len(keys)):search_key = " " + keys[key_index] + ":"index_start = data.find(search_key) + len(search_key)if -1 == index_start:raise debugger_exception("parse_breakpoint_info error: no keys" + keys[key_index] )if key_index < len(keys) - 1:next_key_index = key_index + 1search_key = " " + keys[next_key_index] + ":"index_end = data.find(search_key)if -1 == index_end:raise debugger_exception("parse_breakpoint_info error: no keys" + keys[index_end] )else:index_end = data_endbreakpoint_info[keys[key_index]] = data[index_start:index_end]return breakpoint_info
上述第12行列出了断点信息的类型,它们分别是:标识号、类型、文件路径、行号(为行号断点时有效)、函数名(调用和返回断点时有效)、状态(有效还是失效)、异常类型名(异常断点时有效)、表达式、是否为临时断点(只断一次)、命中次数、命中值(猜测,实际没发现有什么数据)和命中条件。由于实际返回的数据信息不全,我们不能全以其信息为准,这块我们将在之后介绍。
有新增断点就有删除断点,删除断点比较简单,我们只要传入断点ID即可
def remove_breakpoint(self,breakpointid):query = "breakpoint_remove -d " + breakpointiddata = self._pydbgpd.query(query)if "breakpoint removed" in data:ret = 1else:ret = 0return {"ret":ret}
如果删除成功,则会返回breakpoint removed。我们通过返回值判断操作是否成功。
设置完断点后,我们需要通过“步过”、“步入”,“步出”,“执行”等操作控制程序执行,它们的执行逻辑很简单,且没有返回值
def step_over(self, param):return self._step_cmd("step over")def step_in(self, param):return self._step_cmd("step in")def step_out(self, param):return self._step_cmd("step out")def run(self, param):return self._step_cmd("run")def _step_cmd(self,cmd):if False == self._pydbgpd.is_session():return {}data = self._pydbgpd.query(cmd)if len(data):return {"ret":0}else:return {"ret":1}
如果我们执行run之后,程序被中断了,我们可以通过查看状态的命令查看断点调试器的状态
#0 out of session 1 starting 2 break 3 stopping 4 stopped 5 waitingdef status(self,param):if not self._pydbgpd.is_session():return {"ret":1, "status":0}data = self._pydbgpd.query('status')out_of_sesion_status = "invalid cmd"starting_status = "Current Status: status [starting] reason[ok]"break_status = "Current Status: status [break] reason[ok]"stopping_status = "Current Status: status [stopping] reason[ok]"stopped_status = "command sent after session stopped"waiting_status = "session timed out while waiting for response"status = -1status_map = {out_of_sesion_status:0,starting_status:1,break_status:2,stopping_status:3,stopped_status:4,waiting_status:5 };for (key,value) in status_map.items():if key in data:status = valuebreakif not len(data):status = 0return {"ret":1,"status":status}
starting状态是启动调试后的第一个状态,此时还没进入PHP代码。break状态就是被我们断点中断的状态,或者我们执行“步过”、“步入”和“步出”后的调试器状态。stopping状态是已经不在PHP代码中,但是即将结束的状态。对于一个没有断点的程序,执行了“run”之后就进入stopping状态,而中间不会经过break状态。stopped状态表示该会话已经彻底结束,我们可以退出该会话了。waiting状态在调用非常耗时的操作时会出现。
如果调试器处于break状态,则我们可以通过查看调用堆栈的方式查看程序执行路径。
def stack_get(self,param):return {"ret":1, "data":self._get_stack_info()}def _get_stack_info(self, frame = ""):if False == self._pydbgpd.is_session():return []query = 'stack_get ' + framedata = self._pydbgpd.query(query)#data = "frame: 0 file:///var/www/html/index.php(8) file {main}"frame_list = []arr = data.split('\n')for item in arr:if not len(item):continuetry:res = self._stack_get_pattern.search(item).groups()info = {}info['frame'] = res[0]info['filename'] = res[1]#info['path'] = info['path'].replace('/', os.sep)info['filename_last'] = info['filename'].split('/')[-1]info['lineno'] = res[2]info['function'] = res[3]m1 = md5.new() m1.update(info['filename']) info['file_id'] = m1.hexdigest()frame_list.append(info)except Exception,errinfo:print errinfo, "stack_get error:" + data + "\n"return
如果我们执行stack_get,则会返回全部的调用堆栈信息。如果给stack_get传入堆栈号,则返回该调用栈的信息。一般堆栈信息包含堆栈号、所处的文件路径、所处的行号和函数名。我们在之后的UI层通过这个函数可以动态的更新代码的执行情况。
我们调试的一个重要的目的就是可以随时查看变量值,所以查看变量也是调试器的重点。通过Xdebug获取所有栈上的变量要分为三步:
- 获取调用堆栈深度
- 获取context_names
- 获取指定堆栈深度的指定context_names下的所有变量
这一系列操作通过如下操作完成
def _get_all_variables(self, cur = False):all_data = self._get_stack_variables(cur)return {"ret":1, "data":all_data}def _get_stack_variables(self, cur = False):info = {}data = self._pydbgpd.query('stack_depth')#'Stack Depth: 3'pattern = re.compile("Stack Depth: (\d+)")try:res = pattern.search(data).groups()for index in range(0, int(res[0])):iteminfo = self._get_context_variables(index)key = "Frame " + str(index)info[key] = iteminfoif cur:breakexcept Exception,errinfo:print errinfo, "_get_stack_variables error:" + data + "\n"return infodef _get_context_variables(self, depth_id):data = self._pydbgpd.query('context_names')#data='''0: Locals#1: Superglobals#2: User defined constants'''info = {}arr = data.split('\n')for item in arr:if not len(item):continuetry:res = self._context_names_pattern.search(item).groups()iteminfo = self._get_context(depth_id, res[0])info[res[1]] = iteminfoexcept Exception,errinfo:print errinfo, "context_names error:" + item + "\n"return infodef _get_context(self, depth_id, context_id):query = 'context_get -d ' + str(depth_id) + ' -c ' + str(context_id)data = self._pydbgpd.query(query)#data = '''name: $a type: string value: 123#name: $b type: int value: 234'''info = []arr = data.split('\n')for item in arr:if not len(item):continuetry:res = self._context_get_pattern.search(item).groups()iteminfo = {}iteminfo["name"] = res[0]iteminfo["type"] = res[1]iteminfo["value"] = res[2]info.append(iteminfo)except Exception,errinfo:print errinfo, "context_get error:" + item + "\n"return info
context_names可能用户不大理解,其实它就是变量类型。比如全局变量里我们可以看到Http请求的相关信息。这步操作相对于其他操作需要多次查询和解析,所以它的效率是非常低的。所以我在设计时没有让其自动更新(除非用户选择的展现页为变量页,这样每步操作都要更新变量),也没让变量对比功能自动开启。
如果调试会话结束,我们可以通过下面的方法退出调试
def quit(self,param):return self._step_cmd("quit")def stop(self,param):return self._step_cmd("stop")def exit(self,param):return self._step_cmd("exit")
这样主流的一些操作我们讲解完了,我们再讲解些不太能用到的。比如查看当前执行到的代码上下文,可以使用source命令
def source(self,param):src = self._pydbgpd.query("source")if "(u'stack depth invalid', 301)" in src:return {"ret": 0}return {"ret": 1, "data": src}
比如我们在break的情况下,需要修改某个变量值,则可以使用eval指令进行代码执行,其实这块功能非常重要
def eval(self, param):query = "eval " + paramdata = self._pydbgpd.query(query)return {"ret":1}
我还开放了命令行式的调试方式,这样用户就可以自己输入调试命令进行调试,这个和dbg很像,于是我要做的就是命令的传导
def query(self, cmd):return self._pydbgpd.query(cmd)
有了上述的方法,我们可以构建一个简单的调试器。但是由于pydbgp断点信息返回不全,而且我们需要一些高阶功能,比如调试器状态机、预设断点等,使得更高一层的封装整合成为必需。下一博文我们将重点介绍高阶封装相关的内容。
相关文章:

测试客户端发图图
转载于:https://blog.51cto.com/ericsong/116942
搜狐、美团、小米都在用的Apache Doris有什么好? | BDTC 2019
【导读】12 月 5-7 日,由中国计算机学会主办,CCF 大数据专家委员会承办,CSDN、中科天玑协办的中国大数据技术大会(BDTC 2019)在北京长城饭店隆重举行。100 顶尖技术专家、1000 大数据从业者齐聚于此,以“大…

cacti邮件告警设置
功能说明对指定流量图形(指定接口)设置最高或最低流量阀值,当流量出现异常偏高或偏低触发阀值,系统自动将异常信息以邮件形式通知指定收件人。如果收件人邮箱是139邮箱,还可以增设短信通知功能。设置前准备设置该功能之…
跨平台PHP调试器设计及使用方法——高阶封装
在《跨平台PHP调试器设计及使用方法——协议解析》一文中介绍了如何将pydbgp返回的数据转换成我们需要的数据。我们使用该问中的接口已经可以构建一个简单的调试器。但是由于pydbgp存在的一些问题,以及调试器需要的一些高级功能,我们还需要对这些接口进行…

Oracle的口令文件(passwordfile)的讲解(摘录)
初学oracle,很多概念迷糊,今天看到这文章,让我有一个比较清晰的认识。转载[url]http://www.itpub.net/viewthread.php?tid906008&extra&page1[/url]1、os认证oracle安装之后默认情况下是启用了os认证的,这里提到的os认证…
如何优雅地使用pdpipe与Pandas构建管道?
作者 | Tirthajyoti Sarkar译者 | 清儿爸编辑 | 夕颜出品 | AI科技大本营(ID: rgznai100) 【导读】Pandas 是 Python 生态系统中的一个了不起的库,用于数据分析和机器学习。它在 Excel/CSV 文件和 SQL 表所在的数据世界与 Scikit-learn 或 Te…

第 十 天 : 添 加 硬 盘 和 分 区 挂 载 等
小Q:狼若回头,必有缘由,不是报恩,就是***; 事不三思必有败,人能百忍则无忧。今天的进度虽然慢了,但是学习状态还是一如往常,只不过今天遇到了不少新的知识点,需要好好想想…
从4个月到7天,Netflix开源Python框架Metaflow有何提升性能的魔法?
作者 | Rupert Thomas译者 | 凯隐编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导语】Metaflow 是由 Netflix 开发,用在数据科学领域的 Python框架,于 2019 年 12 月正式对外开源。据介绍,Metaflow 解决…
SOA标准发展混乱 国内业务缺少经验
近年来,SOA已经成为国际及我国信息技术领域的重大热点之一。从2005年至今,SOA逐渐成为影响中国IT系统构建的主导思想。从2006年开始,SOA的建设方法已在我国部分行业信息化项目中开始得以越来越广泛的应用。 但热潮背后, SOA概念在…
跨平台PHP调试器设计及使用方法——界面设计和实现
一个优秀的交互设计往往会影响一个产品的命运。在设计这款调试器时,我一直在构思这款调试器该长什么样子。简单、好用是我设计的原则,于是在《跨平台PHP调试器设计及使用方法——立项》一文中,我给出了一个Demo。之后实现的效果也与之变化并不…

AJAX安全-Session做Token
个人思路,请大神看到了指点 个人理解token是防止扫号机或者恶意注册、恶意发表灌水,有些JS写的token算法,也会被抓出来被利用,个人感觉还是用会过期的Session做token更好,服务器存储,加载到客户端页面&…
跨平台PHP调试器设计及使用方法——使用
经过之前六篇博文的分析和介绍,大家应该对这套调试器有个初步的认识。本文我将讲解它的使用方法。(转载请指明出于breaksoftware的csdn博客) 上图是该软件界面的布局,我们之后的讲解也将围绕着这些功能展开。 文件夹管理 在查看一…
管理7k+工作流,月运行超10000万次,Lyft开源的Flyte平台意味着什么?
作者 | Allyson Gale译者 | 刘畅编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】Flyte 平台可以更容易的创建并发,可伸缩和可维护的工作流,从而进行机器学习和数据处理。Flyte 已有三年多的训练模型和数据处理经…

Jmeter组件执行顺序与作用域
一、Jmeter重要组件: 1)配置元件---Config Element: 用于初始化默认值和变量,以便后续采样器使用。配置元件大其作用域的初始阶段处理,配置元件仅对其所在的测试树分支有效,如,在同一个作用域…
跨平台PHP调试器设计及使用方法——拾遗
之前七篇博文讲解了跨平台PHP调试器从立项到实现的整个过程,并讲解了其使用方法。但是它们并不能全部涵盖所有重要内容,所以新开一片博文,用来讲述其中一些杂项。(转载请指明出于breaksoftware的csdn博客) 触发调试的…
召唤超参调优开源新神器:集XGBoost、TensorFlow、PyTorch、MXNet等十大模块于一身...
整理 | 凯隐编辑 | Jane出品 | AI科技大本营(ID:rgznai100)【导读】Optuna是一款为机器学习任务设计的自动超参数优化软件框架,是一款按运行定义(define-by-run) 原则设计的优化软件,允许用户动态地调整搜索空间&#…

Linux下的Silverlight:Moonlight 1.0 Beta 1发布了
Moonlight是微软Silverlight的一个开源实现,其目标平台是Linux与Unix/X11系统。自从2007年9月开始,Moonlight就在Mono项目下进行了开发,它是由Novell发起并资助的。现在,Moonlight 1.0 Beta 1已经向公众发布了。 Novell和Mono宣布…
在visual studio 2010中调用ffmpeg
转自:http://blog.sina.com.cn/s/blog_4178f4bf01018wqh.html 最近几天一直在折腾ffmpeg,在网上也查了许多资料,费了不少劲,现在在这里和大家分享一下。 一、准备工作本来是想自己在windows下编译ffmpeg生成lib、dll等库文件的&am…
无线路由器与无线AP的区别
摆脱线缆的羁绊,手捧一杯香醇的咖啡在家中的任何角落都可以无拘无束和网友谈天说地──这就是无线的魅力!在无线网络迅猛发展的今天,无线局域网(Wireless Local-Area Network,简称WLAN)已经成为许多SOHO家庭…

Simple Dynamic Strings(SDS)源码解析和使用说明一
SDS是Redis源码中一个独立的字符串管理库。它是由Redis作者Antirez设计和维护的。一开始,SDS只是Antirez为日常开发而实现的一套字符串库,它被使用在Redis、Disque和Hiredis等作者维护的项目中。但是作者觉得这块功能还是比较独立的,应该让其…
“不会Linux,到底有多危险?”骨灰级成程序员:基本等于自废武功!
说起程序员的必备技能,我想大家都可以说很多,比如:算法、数据结构、数学、编程语言等等。对于程序员来讲,这些底层能力固然重要,但是,工具同样也是如此,比如常被大家所忽视的:Linux。…

“Uncaught TypeError: string is not a function”
http://www.cnblogs.com/haitao-fan/archive/2013/11/08/3414678.html 今天在js中写了一个方法叫做search(),然后点击按钮的时候提示: “Uncaught TypeError: string is not a function” 百思不得其解啊,我的js木有问题啊啊.... 后来才发现酱…

关于Nikon Ai AF 28mm F1.4D遮光罩的问题
-- 好不容易找到百变妖,确实比较妖!!遮光罩不好找,原厂推荐的HK-7基本属于古董中的古董。 爬文很久,终于找到一篇国外的介绍,说可以用HK-4代替,比HK-7效果更好,而且可以用85mm 1.4D-…

Simple Dynamic Strings(SDS)源码解析和使用说明二
在《Simple Dynamic Strings(SDS)源码解析和使用说明一》文中,我们分析了SDS库中数据的基本结构和创建、释放等方法。本文将介绍其一些其他方法及实现。(转载请指明出于breaksoftware的csdn博客) 字符串连接 SDS库提供下面两种方法进行字符串…
亚马逊机器学习服务:深入研究AWS SageMaker
作者 | Manish Manalath译者 | Shawn编辑 | Carol出品 | AI科技大本营(ID: rgznai100) 机器学习是一个从数据中发现模式的强大概念。但是,如果您尝试过从零开始构建机器模型,那么您一定知道设计一个可扩展的机器学习工作流是多大的…

Java Timer 定时器的使用
一、延时执行首先,我们定义一个类,给它取个名字叫TimeTask,我们的定时任务,就在这个类的main函数里执行。 代码如下:package test;import java.util.Timer;public class TimeTaskTest { public static void main(Str…
Redis源码解析——前言
今天开启Redis源码的阅读之旅。对于一些没有接触过开源代码分析的同学来说,可能这是一件很麻烦的事。但是我总觉得做一件事,不管有多大多难,我们首先要在战略上蔑视它,但是要在战术上重视它。除了一些高大上的技术,我们…

asp.net客户端脚本验证小技巧
通用的客户端脚本验证 Code//验证客户端function checkclient() { var list document.all; for(var i0 ;i<list.length; i) { var h list[i].hint; if(h ! null && h ! "") { if(list[i].isdrop"…
5个可以帮助你提高工作效率的新AI工具
作者 | Kyrylo Lyzanets译者 | 火火酱编辑 | Carol出品 | AI科技大本营(ID: rgznai100) 毫无意义的新闻、故事和活动会占用你每天多少的工作时间?假如你是一名需要高绩效的高管或专业人士,如果在工作中可以不分心,那你…

Centos6.5更换163源 epel源
想必大家都遇到过,安装新的centos系统,使用yum去安装软件的时候,要么找不到,要么慢的让人发疯。网上其实办法很多,直接更换163源就ok,但是基本所有的文章都是直接wget下163的源,但是不知道为什么…