注:本文内容较长且细节较多,建议先收藏再阅读,原文将在 Github 上维护与更新。php
在 HTTP 接口开发与调试过程当中,咱们常常遇到如下相似的问题:html
- 为何本地环境接口能够调用成功,但放到手机上就跑不起来?
- 这个接口很复杂,内部调用了好几个其余接口,如何定位问题究竟出在哪一步?
- 后端开发尚未把接口提供好,前端开发任务没法推动……
「猫哥网络编程系列」最核心的任务即是向各位分享一个我从多年的先后端项目中总结而来的「万能」HTTP 调试法,掌握并从网络编程原理上理解它,能让咱们顺利定位并解决全部 HTTP 接口问题。因为该方法主要涉及到的知识点包括 HTTP 代理(Proxy)、编辑(Edit)与数据模拟(Mock),所以我称之为「HTTP PEM 调试法」。前端
接下来,咱们就针对前面提出的几个问题,详细讲解下 PEM 调试法的思路。node
如何调试线上 App 中的 H5 页面?android
在上一期《猫哥网络编程系列:详解 BAT 面试题》中,咱们有介绍到 Windows 下的 Fiddler 和 Mac 下的 Charles 这两款 HTTP 抓包工具,其实它们就是两个 HTTP 代理服务器(HTTP Proxy Server)。因为 HTTP 是一种符合 REST 架构风格(Representational State Transfer)的协议,具备无状态(Stateless)与统一接口(Uniform Interface)的架构约束,所以其代理机制的实现十分的简单。ios
打个比方,咱们能够把 Proxy Server 理解成一个快递中转站,当一个包裹通过中转站时,包裹的信息(发件人、收件人与包裹里的货物)一般不会作任何的改动,直接发往下一个中转站或顾客手中。但中转站彻底有能力修改快递单信息、拆箱检查货物,甚至是私吞或调换货物。git
当咱们须要快速定位「线上产品的接口问题」时,若是没有源码、数据、依赖服务和足够的时间去搭建一个测试环境,则一般会使用 HTTP 代理服务器来进行快速抓包调试。程序员
Fiddler 默认只容许本地 IP(127.0.0.1)使用代理服务,经过设置「Tools -> Connections -> Allow remote computers to connect」能够开启其余 IP(一般是同一局域网内的其余设备)使用代理服务。github
Charles 默认开放代理服务,但陌生设备首次链接时须要受权确认,经过如下配置能够设置成无需受权。面试
以上两款软件默认的代理端口均是 8888 ,软件开启以后,咱们能够在对应的平台终端下经过ipconfig
(Windows) 或 ifconfig
(Mac)命令查询本机的局域网 IP,还可使用 telnet
命令检查代理通道是否可用。(注:Win7 下如何开启 telnet 命令请参考百度经验。)
如下是 Windows 下 CMD 终端的使用截图,Mac 系统下请类比参考。
接下来,咱们将手机的 Wi-Fi 代理设置为上述的 IP 与 端口号,如下是 iOS 的设置截图( Android 系统一般是长按已链接的 Wi-Fi ,在弹出的高级设置菜单中配置代理服务器)。
至此,手机上任意应用发起的 HTTP 请求都将会被代理服务器(本例中的 Fiddler/Charles 软件)监听到。
经过代理服务器监听到 HTTP 请求以后,咱们能够经过浏览报文的详细信息,定位出可能的接口问题。Fiddler 与 Charles 都具备一样强大的 HTTP 编辑(Edit)、重发(Replay/Repeat)、断点(Breakpoints)功能。Charles 的基础与高级用法请参考《Charles 从入门到精通》,Fiddler 教程能够参考 OSChina 专题《HTTP调试代理 Fiddler》,如下介绍 Fiddler 的部分常见用法。
抓到手机 HTTP 请求以后,经过编辑(Unlock For Editing)和重发(Replay)操做能够不断地调试接口的响应是否符合预期。
经过设置自动响应规则(AutoResponder Rules)能够将响应头设置成常见状态码的返回,或将响应体映射成本地文件,经过外部编辑器修改文件内容进行调试。其中,若设置响应为 *bpu
或 *bpafter
能够在请求前与响应前的事件触发时进行断点调试,十分方便。
须要注意的是,在 Fiddler 中使用 Replay 功能重发请求时,请求由 Fiddler 代理从新发起而非手机,所以手机 App 中的 H5 不会有任何变化。只有从新刷新 App 的 H5 页面,配合 HTTP 断点调试(Breakpoints )的方式才可让修改后的 HTTP 响应体在 App中生效。这里介绍另一种配合 Weinre 的调试用法。
Weinre 属于知名 Hybrid 框架 Cordova 中的一款 Web App 远程调试工具。经过在页面中注入一段 JS 脚本,能够在 PC 和手机端的 H5 页面之间创建一个 Socket 双向数据传输通道。原理上能够理解为,当咱们在 PC 端的后台进行 debug 时,相关的操做被序列化成一组 JSON 字符串,数据经由通道传输给手机端中的 H5 页面,页面在接收到这些数据以后反序列化成相应的 JS 脚本操做,在其 window 上下文中执行,并将执行的结果回传给通道,PC 端的 Chrome 经过监听通道获取到相应的数据在 debug 后台中展示出来。
如下介绍 Weinre 的基本用法:
npm install -g weinre
weinre --boundHost 0.0.0.0 --httpPort 8081
。一般在 Node.js 的服务中绑定 IP 为 0.0.0.0 而非 127.0.0.1(本地 IP),意味着可让任意来源的 IP 访问该服务ipconfig
(Mac 为 ifconfig
)命令获取本机 IP 后,在本机 Chrome 浏览器中访问 Weinre 管理后台:http://10.2.69.47:8081 (本例中个人 IP 为 10.2.69.47,请注意将其替换成本身的局域网 IP)<script src="http://10.2.69.47:8081/target/target-script-min.js#anonymous"></script>
问题是,咱们「如何将 Weinre Script 自动注入到手机的 H5 页面中」?
想必用过中国电信宽带的同窗都有过这样的体验:在刚开始浏览网页时,会自动跳出一些「宽带升级优惠」、「宽带缴费提醒」之类的页面。这种耍流氓的方式即是宽带运营商在 HTTP 代理层面的 Script 注入行为。前面已经提到 HTTP 协议是一种 REST 风格的架构,而且他的头部与主体报文为字符串文本流(对比二机制、十六进制数据流),在不使用 HTTPS 的状况下,很容易被中间路由或代理网关进行消息篡改。
经过 Fiddler Script 特性,咱们能够自动对通过 Fiddler 的 HTTP 流量进行二次修改,注入任意内容(Mac 用户若已了解相关知识点,请直接跳至下方的 Charles 截图)。
打开 Fiddler 菜单「Rules -> Customize Rules… 」,若是是首次开启会要求先下载安装 Fiddler ScriptEditor。打开 Fiddler ScriptEditor 以后,找到如下代码块(或使用菜单「Go -> to OnBeforeResponse」):
static function OnBeforeResponse(oSession: Session) { if (m_Hide304s && oSession.responseCode == 304) { oSession["ui-hide"] = "true"; } }
Fiddler Script 使用的编程语言是 JScript.NET(JavaScript 和 C# 的混合语法,相似 TypeScript),OnBeforeResponse
是 HTTP Response 响应前的事件函数,咱们只须要在这里判断「若是开启了 Weinre Debug 功能,那么就在全部的 HTML 响应体中注入 Weinre Script」,如下是我修改的示例代码,覆盖以上代码块便可。
public static RulesOption("Enable Weinre Script") var m_EnableWeinreScript: boolean = true; public static var g_weinreScriptString: String = '<script src="http://127.0.0.1:8080/target/target-script-min.js#anonymous"></script>'; public static ToolsAction("Config Weinre Script") function ConfigWeinreScript(){ g_weinreScriptString = FiddlerObject.prompt("Text beblow will inject into HTML pages when 'Enable Weinre Script' rule is Enabled.", g_weinreScriptString , "Please Input the Weinre Script"); } static function OnBeforeResponse(oSession: Session) { if (m_Hide304s && oSession.responseCode == 304) { oSession["ui-hide"] = "true"; } if (m_EnableWeinreScript && oSession.oResponse.headers.ExistsAndContains("Content-Type","text/html")){ oSession.utilDecodeResponse(); if(oSession.utilFindInResponse("</html>", false)>-1){ oSession["ui-backcolor"] = "#5E30B5"; oSession["ui-color"] = "white"; oSession.utilReplaceRegexInResponse("<\/html>", g_weinreScriptString + '</html>'); } } }
修改保存后重启 Fiddler(或使用菜单「Tools -> Reset Script」)以生效规则,接下来运行「Tools」菜单中新出现的「Config Weinre Script」,将 127.0.0.1:8080 替换成本身本机的局域网 IP 与 weinre 服务端口号,同时开启菜单「Rules -> Enable Weinre Script」。至此,全部 HTML 页面将会被自动注入 Weinre Script,以后咱们就能够在 weinre 后台中开始调试相关页面。如下是参考截图:
能够看到 HTTP 响应体中已经被动态注入 Weinre Script。
在 Mac Charles 下的 Script 注入配置更加容易,只需利用其 「Rewrite」功能进行简单的配置便可,参看下图:
经过 Fiddler/Charles 代理工具将 JS 脚本注入成功后,咱们即可以经过前文提到的 weinre 后台开始 debug 相应的页面,如下是在 iPhone 模拟器中调试新浪微博界面的截图:
使用该方法能够调试 Android 和 iOS 中「任意 App 的 H5 页面」,但因为主要使用了 weinre 服务,其原理决定了该方法没法像真正的 Chrome DevTools 同样支持 JS 断点调试、Profiles 性能分析等功能,具备必定的局限性。在实际 Web App 开发过程当中,推荐使用如下工具进行调试 :
因而可知,「HTTP PEM 调试法」是一个通用的 HTTP 接口调试方案,能够用来快速定位线上接口问题,对于开发人员来讲掌握其背后的 HTTP 协议及其代理机制的原理更加剧要,接下来咱们聊聊常见的 HTTP 接口开发协做方法与 Mock 思路。
个人开发任务无法推动,由于某某的接口还没提供给我。
但愿新手程序员在看完这一章节以后,不要再向你的项目组和上级反馈这样的说法,由于 HTTP Mock(接口数据模拟)是一项网络编程的基础技能,从实际项目经验来看,大部分基于 HTTP 接口的任务均可以并行开发。
不一样岗位(例如前端开发与后台开发)或不一样业务(例如订单系统与帐户系统)的开发人员开始并行开发任务以前,首先要作的应该是对耦合和相互依赖的任务进行边界划分与规则约定。具体到某个 HTTP API 接口的约定上,至少应该明确如下信息:
针对以上三条信息,我设想的「最简」 HTTP API 包含如下几条原则,供各位参考:
RESTful API 其实是利用 HTTP 协议的语义(提交类型、返回码、Hypermedia Link)来将全部接口操做抽象化为一系列资源对象。这要求 API 的设计者与调用者都具有深厚的 HTTP 协议功底、语义化与抽象化能力。
因为 HTTP 1.0 尤为是 HTML 的规范与应用已经深刻人心。大部分开发者可以很天然的这样理解:「GET」 表示「读」操做,「POST」 表示「写」操做。这样既能够保证中间组件与浏览器很好的利用 GET 的缓存机制,又能下降接口设计的复杂度。HTTP 之父 Roy Fielding 也说过「It is okay to use POST」:
Some people think that REST suggests not to use POST for updates. Search my dissertation and you won’t find any mention of CRUD or POST. (不少人认为 RESTful 建议不要使用 POST 用于提交更新,去翻一翻个人论文,压根就没提到过 POST 和其余「增查改删」方面的内容。)
但使用 POST 方法时尤为要注意:「使用统一的 Content-Type」。这是一个容易被新手忽略的细节,也是接口设计中常常出错的点。在上一期的《猫哥网络编程系列:详解 BAT 面试题》中有问到:
一个 POST 请求的 Content-Type 有多少种,传输的数据格式有何区别?
如下举例一些常见类型的 HTTP POST Request 报文,请注意其中的 Content-Type
与 Body 的对应关系(已手动删除无关 HTTP Header)
POST /test.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 54 Content-Type: application/json {"weixin_id":"imgXQB","weixin_name":"猫哥学前班"}
POST /test.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 74 Content-Type: application/x-www-form-urlencoded weixin_id=imgXQB&weixin_name=%E7%8C%AB%E5%93%A5%E5%AD%A6%E5%89%8D%E7%8F%AD
POST /test.php HTTP/1.1 Host: 127.0.0.1:8080 Content-Length: 259 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl60ti7CVoBj2kxfX ------WebKitFormBoundaryl60ti7CVoBj2kxfX Content-Disposition: form-data; name="weixin_id" imgXQB ------WebKitFormBoundaryl60ti7CVoBj2kxfX Content-Disposition: form-data; name="weixin_name" 猫哥学前班 ------WebKitFormBoundaryl60ti7CVoBj2kxfX--
只有客户端 POST 请求体的消息格式与其请求头声明的 Content-Type 一致时,服务端才能正确的接收与响应。由于许多后端的 Web 应用框架会遵守 HTTP 协议的内容协商原则(Content Negotiation)对响应体进行预处理,以提高开发体验。例如,Python 的 Flask 框架 封装了request.json、request.form、request.data 等一系列属性用于存放不一样类型的来源数据。
ua=1280x768||chrome
,当须要添加操做系统字段时,客户端只需按规则追加信息到原来的参数上,如ua=1280x768||chrome||windows
。该条原则还有许多其余的方法来实现,再也不一一举例。基本的返回体结构,可参考如下示例代码。
{ "code": "0", "message": "success", "data": { "id" : "1", "list" : [] } }
寥寥的几行代码饱含了几部深入的血泪史:
code
表示返回码(也能够理解成错误码),成功时返回 "0"
,出错时按预设的错误码规则返回(微信的返回码规范设计的并很差,由于没有内建的规律和语义);message
与 data
的设计。须要注意的是 data 只具备 Object 一种类型。无数据的时候返回一个空对象 {}
(而非 null
),有多条数据的时候将 Array 类型数据放在其内部的 list
之类的属性中;"0"
和 "1"
。缘由是先后端对浮点数运算精度不一致,会致使商品价格的计算与展现出错;iOS/Android 客户端对 JSON null、布尔类型转换的不一致会致使频繁的 App Crash。固然,也有许多其余的方案能够解决上面提到的问题,但出于「最简」的原则,这样约定的理解成本最低。
有了最简 API 的约定以后,实现最简 Mock Server 就相对简单多了。
首先,咱们按照 API 接口约定来新建一些模拟数据文件。例如新建一个 「mock-data.json」 的文件,将以上返回体数据保存其中。
在命令行模式下运行 php
命令,Mac 用户直接打开终端便可,Windows 用户须要先安装 XAMPP 套件,并将 php.exe 所在的目录配置到系统环境变量中,再使用 CMD 运行如下命令:
php -S 0.0.0.0:8080 mock-data.json
开启以后访问任意 API 地址(http://127.0.0.1:8080/any-api-uri-you-want/)均会返回 mock-data.json 的数据响应体。经过将 8080 端口换成 80 端口(Mac 须要使用 sudo 权限),再设置相似 127.0.0.1 www.example.com
的 HOST 配置,即可以模拟 API 的 Domain Host(http://www.example.com/any-api-uri-you-want/)形式。
固然,也能够本身编写一个 index.php 的入口文件来实现一个基于 URL Path 规则的简单 Rewrite 功能,用来同时支持多个 API 的数据模拟。
Fiddler/Charles 的 Map Local(本地映射)不光是用于 HTTP Edit,一样能够用于 HTTP Mock,当一个 404 请求(还未真正实现的 API)被代理服务器捕获后,能够设置映射到本地自定义的 mock-data.json 模拟数据文件,从而被模拟成一个正常的 200 请求。
迄今为止,我还未发现一个理想中的 Mock API 开源系统,若有哪位同窗有见到过请在 Github 上留言周知,如下是我对最理想 Mock System 的构想:
这个接口很复杂,内部调用了好几个其余接口,如何定位问题究竟出在哪一步?
对于新人来讲,最快的成长方式是不断地在新项目中实践,从头至尾参与到项目的每一个系统细节的设计与讨论。若是能参与到重点、大型项目中,甚至幸运地获得大牛的亲自指导,成长速度将会日新月异。
但更多的状况是,新人做为离职程序员的补充力量来接手一个老项目甚至是烂摊子。面对一个复杂的陌生系统,吐槽与抱怨无济于事。这时,若是能使用「HTTP PEM 调试法」,从接口设计与调用的角度来剖析、理解整个系统的设计,就能快速上手业务。例如,PHP 程序员能够在项目代码中全部的 curl 调用点,将「CURLOPT_PROXY」设置成 Fiddler/Charles 的代理服务,而后一步步调试,从接口字段上理解数据库设计和 Controller 背后的业务逻辑。
最后,欢迎各位给我留言分享更多关于「HTTP PEM」和其余调试方法的经验与体会。