猫哥⽹络编程系列:H TTPPEM 万能调试法
注:本⽂内容较长且细节较多,建议先收藏再阅读,原⽂将在 。在 HTTP 接⼝开发与调试过程中,我们经常遇到以下类似的问题:为什么本地环境接⼝可以调⽤成功,但放到⼿机上就跑不起来?这个接⼝很复杂,内部调⽤了好⼏个其他接⼝,如何定位问题究竟出在哪⼀步?
后端开发还没有把接⼝提供好,前端开发任务⽆法推进……
「猫哥⽹络编程系列」最核⼼的任务便是向各位分享⼀个我从多年的前后端项⽬中总结⽽来的「万能」HTTP 调试法,掌握并从⽹络编程原理上理解它,能让我们顺利定位并解决所有 HTTP 接⼝问题。由于该⽅法主要涉及到的知识点包括 HTTP 代理(Proxy)、编辑(Edit)与数据模拟(Mock),因此我称之为「HTTP PEM 调试法」。
接下来,我们就针对前⾯提出的⼏个问题,详细讲解下 PEM 调试法的思路。
如何调试线上 App 中的 H5 页⾯?
在上⼀期《》中,我们有介绍到 Windows 下的 Fiddler 和 Mac 下的 Charles 这两款 HTTP 抓包⼯具,其实它们就是两个 HTTP 代理服务器(HTTP Proxy Server)。由于 HTTP 是⼀种符合 REST 架构风格(Representational State Transfer)的协议,具有⽆状态(Stateless)与统⼀接⼝(Uniform Interface)
的架构约束,因此其代理机制的实现⼗分的简单。
打个⽐⽅,我们可以把 Proxy Server 理解成⼀个快递中转站,当⼀个包裹经过中转站时,包裹的信息(发件⼈、收件⼈与包裹⾥的货物)通常不会做任何的改动,直接发往下⼀个中转站或顾客⼿中。但中转站完全有能⼒修改快递单信息、拆箱检查货物,甚⾄是私吞或调换货物。
当我们需要快速定位「线上产品的接⼝问题」时,如果没有源码、数据、依赖服务和⾜够的时间去搭建⼀个测试环境,则通常会使⽤ HTTP 代理服务器来进⾏快速抓包调试。
Fiddler 默认只允许本地 IP(127.0.0.1)使⽤代理服务,通过设置「Tools -> Connections -> Allow remote computers to connect」可以开启其他IP(通常是同⼀局域⽹内的其他设备)使⽤代理服务。
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 的基础与⾼级⽤法请参考《》,Fiddler 教程可以参考 OSChina 专题《》,以下介绍 Fiddler 的部分常见⽤法。
抓到⼿机 HTTP 请求之后,通过编辑(Unlock For Editing)和重发(Replay)操作可以不断地调试接⼝的响应是否符合预期。
通过设置⾃动响应规则(AutoResponder Rules)可以将响应头设置成常见状态码的返回,或将响应体映射成本地⽂件,通过外部编辑器修改⽂件内容进⾏调试。其中,若设置响应为 *bpu 或 *bpafter 可以在请求前与响应前的事件触发时进⾏断点调试,⼗分⽅便。
需要注意的是,在 Fiddler 中使⽤ Replay 功能重发请求时,请求由 Fiddler 代理重新发起⽽⾮⼿机,因
此⼿机 App 中的 H5 不会有任何变化。只有重新刷新 App 的 H5 页⾯,配合 HTTP 断点调试(Breakpoints )的⽅式才可以让修改后的 HTTP 响应体在 App中⽣效。这⾥介绍另外⼀种配合 的调试⽤法。Weinre 属于知名 Hybrid 框架 中的⼀款 Web App 远程调试⼯具。通过在页⾯中注⼊⼀段 JS 脚本,可以在 PC 和⼿机端的 H5 页⾯之间建⽴⼀个 Socket 双向数据传输通道。原理上可以理解为,当我们在 PC 端的后台进⾏ debug 时,相关的操作被序列化成⼀组 JSON 字符串,数据经由通道传输给⼿机端中的H5 页⾯,页⾯在接收到这些数据之后反序列化成相应的 JS 脚本操作,在其 window 上下⽂中执⾏,并将执⾏的结果回传给通道,PC 端的 Chrome 通过监听通道获取到相应的数据在 debug 后台中展现出来。
「HTTP PEM 调试法」之 Proxy
#「HTTP PEM 调试法」之 Edit
#Fiddler Edit 与 AutoResponder
#Weinre 基本⽤法
#
以下介绍 Weinre 的基本⽤法:
1. 通过 全局安装 weinre: npm install -g weinre
2. 在本地 8081 端⼝上启动 weinre 服务:weinre --boundHost 0.0.0.0 --httpPort 8081 。通常在 Node.js 的服务中绑定 IP 为 0.0.0.0 ⽽⾮
127.0.0.1(本地 IP),意味着可以让任意来源的 IP 访问该服务
3. 通过上⽂介绍的 ipconfig (Mac 为 ifconfig )命令获取本机 IP 后,在本机 Chrome 浏览器中访问 Weinre 管理后台: (本例中我的 IP 为
10.2.69.47,请注意将其替换成⾃⼰的局域⽹ IP)
4. 在管理后台我们能看到相关使⽤说明,要求将以下脚本插⼊需要调试的 H5 页⾯中:<script src="10.2.69.47:8081/target/target-script-
min.js#anonymous"></script>
5. 将以上脚本插⼊进 H5 页⾯后,我们在 PC 端 Chrome 中,通过 后台点击进⼊相应的客户端调试界⾯
问题是,我们「如何将 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 && sponseCode == 304) {
oSession["ui-hide"] = "true";
}
}
使⽤的编程语⾔是 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="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 && sponseCode == 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 模拟器中调试新浪微博界⾯的截图:
HTTP Script 注⼊
#
使⽤该⽅法可以调试 Android 和 iOS 中「任意 App 的 H5 页⾯」,但由于主要使⽤了 weinre 服务,其原理决定了该⽅法⽆法像真正的 Chrome DevTools ⼀样⽀持 JS 断点调试、Profiles 性能分析等功能,具有⼀定的局限性。在实际 Web App 开发过程中,推荐使⽤以下⼯具进⾏调试 : 调试基于的 Web App 调试 Android Web App
调试 iOS Web App
由此可见,「HTTP PEM 调试法」是⼀个通⽤的 HTTP 接⼝调试⽅案,可以⽤来快速定位线上接⼝问题,对于开发⼈员来说掌握其背后的 HTTP 协议及其代理机制的原理更加重要,接下来我们聊聊常见的 HTTP 接⼝开发协作⽅法与 Mock 思路。
我的开发任务没法推进,因为某某的接⼝还没提供给我。
希望新⼿程序员在看完这⼀章节之后,不要再向你的项⽬组和上级反馈这样的说法,因为 HTTP Mock(接⼝数据模拟)是⼀项⽹络编程的基础技能,从实际项⽬经验来看,⼤部分基于 HTTP 接⼝的任务都可以并⾏开发。
不同岗位(例如前端开发与后台开发)或不同业务(例如订单系统与账户系统)的开发⼈员开始并⾏开
发任务之前,⾸先要做的应该是对耦合和相互依赖的任务进⾏边界划分与规则约定。具体到某个 HTTP API 接⼝的约定上,⾄少应该明确以下信息:
1. 是否按照 的约定来设计接⼝
2. 接⼝的路径、提交⽅法、参数、编码类型(Enctype/Content-Type)
3. 接⼝返回的错误码(code)、消息说明(message)、业务数据(data)
针对以上三条信息,我设想的「最简」 HTTP API 包含以下⼏条原则,供各位参考:
RESTful API 实际上是利⽤ HTTP 协议的语义(提交类型、返回码、)来将所有接⼝操作抽象化为⼀系列资源对象。这要求 API 的设计者与调⽤者都具备深厚的 HTTP 协议功底、语义化与抽象化能⼒。
RESTful 作为⼀个 Buzzword(流⾏词),其含义已经被曲解。HTTP 协议和 REST 架构的设计者 Roy Fielding 很反感这⼀点,还专门开了。⼤多数⼈只将 来使⽤(既成事实),并不能真正理解 REST 架构风格;
RESTful API 将所有请求抽象化为资源名词(Resources)的做法争议很⼤。这种做法总会让我回想起上个世纪⽤ FrontPage 做⽹页的经历,「设置⼀个超链接,从某个资源跳到另外⼀个资源」。在经过 W
eb 2.0 浪潮,进⼊移动互联⽹时代后,这种 API 设计容易给⼈带来困惑。例如「登录、注册」这样的「动词」如何抽象成「名词」(还好有 可以参考 )。⽽刻意的使⽤ 「HTTP CRUD」(POST/GET/PUT/DELETE Method)操作「资源化」之后的接⼝,并未带来更多实质上的收益;
HTTP 状态码的分层思路在 RESTful API 模式下被破坏了。已经⾜够⽹络中间组件(代理、⽹关、路由)使⽤,HTTP 1.1 中加⼊的很多状态码缺乏实际场景(例如 ),它们增加了中间组件以及浏览器对规范理解与实现的要求。尽可能的将状态码交给相应的接⼝逻辑层⽽⾮ HTTP 协议层,能够将问题简化;
对⽐以英⽂为母语的国外开发者⽽⾔,国内开发者对语义化的认知难度更⾼,例如 RESTful 建议资源命名⽤复数形式,那收货地址单词 address 的复数形式是什么?address or addresses ?address-list or address-lists?(没过英语⼋级的同学已经哭晕在厕所 T_T)
每个⼈对 RESTful API 的理解都不同,在 HTTP 协议层⾯做扩展与实现,不如交给接⼝设计者与调⽤者⾃⼰来约定数据结构(或者参考 规范)。把HTTP 只当做传输协议来使⽤的好处是,当后端服务间的接⼝需要直接基于 TCP 传输层来做性能优化时,可以⼗分⽅便的切换成 Socket 的实现(之前在腾讯做微博相关项⽬时,微博开放平台对外只提供 HTTP 的 Open API,但对内可以提供更⾼频率与频次调⽤的原⽣ Socket 协议)。
由于 HTTP 1.0 尤其是 HTML 的规范与应⽤已经深⼊⼈⼼。⼤部分开发者能够很⾃然的这样理解:「GET」 表⽰「读」操作,「POST」 表⽰「写」操作。这样既可以保证中间组件与浏览器很好的利⽤ GET 的缓存机制,⼜能降低接⼝设计的复杂度。HTTP 之⽗ Roy Fielding 也说过「」:
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」。这是⼀个容易被新⼿忽略的细节,也是接⼝设计中经常出错的点。在上⼀期的《》中有问到:
⼀个 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
「HTTP PEM 调试法」之 Mock
#最简 HTTP API
#1、不使⽤ RE ST f ul A PI 来设计接⼝
#2、只使⽤ G E T /POST Me thod
#
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 协议的内容协商原则()对响应体进⾏预处理,以提升开发体验。例如,Python 的 封装了request.json、request.form、request.data 等⼀系列属性⽤于存放不同类型的来源数据。API URI 应该全⼩写。屏蔽掉 Linux/Windows 操作系统对⽂件名⼤⼩写敏感度不⼀致的问题;
URI 命名上应该使⽤连字符「-」来间隔,⽽不是使⽤下划线「_」或驼峰式。这是出于视觉美观度和英⽂语义⽅⾯的考虑,英⽂域名规范规定可以使⽤连字符,但不能使⽤下划线,API 路径应该和 Domain 命名风格⼀致;
URI 使⽤「动词+名词」或者「名词+动词」均可,但选定⼀种之后应该保持⼀致。接⼝风格的⼀致性,可以降低使⽤者的理解成本,好的 API 命名风格能让⼈「以⼀知万」,能从⼀个 API 猜测出所有其他 API 的命名形式;参数命名上应该使⽤下划线「_」⽽⾮连接符「-」。这点主要是从数据库字段设计的
统⼀性和后台应⽤程序框架的易⽤性来考虑;
不同接⼝的相同参数命名应保持统⼀,并考虑扩展要求。例如,收集⽤户信息的参数可以统⼀叫「ua」,为了便于扩展可以约定将客户端分辨率、浏览器型号等信息使⽤「||」字符串连接,如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。
当然,也有许多其他的⽅案可以解决上⾯提到的问题,但出于「最简」的原则,这样约定的理解成本最低。
3、接⼝ URI 与参数命名风格的⼀致性
#4、返回数据结构的⼀致性
#最简 Mock Server
#
有了最简 API 的约定之后,实现最简 Mock Server 就相对简单多了。
⾸先,我们按照 API 接⼝约定来新建⼀些模拟数据⽂件。例如新建⼀个 「mock-data.json」 的⽂件,将以上返回体数据保存其中。
在命令⾏模式下运⾏ php 命令,Mac ⽤户直接打开终端即可,Windows ⽤户需要先安装 套件,并将 所在的⽬录中,再使⽤ CMD 运⾏以下命令:
php -S 0.0.0.0:8080 mock-data.json
当然,也可以⾃⼰编写⼀个 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 的构想:
1. API 录⼊后台。包含⼀个按项⽬(⼀般是 Domain)维度进⾏ API 管理的后台。可以在后台上录⼊「请求 URI、参数、多种业务数据响应体、全局错误
码、API 错误码」等接⼝信息;
2. API 接⼝⽂档。能够基于 API 后台数据,⽣成在线的 API ⽂档平台;
3. Postman 导⼊/导出。能够基于 API 数据导出⽣成 ,以便导⼊ Postman 中进⾏ API 调试;
4. Mock Server。能够基于 API 数据快速搭建类似 的本地服务,或提供远程模拟接⼝服务。
这个接⼝很复杂,内部调⽤了好⼏个其他接⼝,如何定位问题究竟出在哪⼀步?
对于新⼈来说,最快的成长⽅式是不断地在新项⽬中实践,从头到尾参与到项⽬的每个系统细节的设计与讨论。如果能参与到重点、⼤型项⽬中,甚⾄幸运地得到⼤⽜的亲⾃指导,成长速度将会突飞猛进。app接口测试工具
但更多的情况是,新⼈作为离职程序员的补充⼒量来接⼿⼀个⽼项⽬甚⾄是烂摊⼦。⾯对⼀个复杂的陌⽣系统,吐槽与抱怨⽆济于事。这时,如果能使⽤
「HTTP PEM 调试法」,从接⼝设计与调⽤的⾓度来剖析、理解整个系统的设计,就能快速上⼿业务。例如,PHP 程序员可以在项⽬代码中所有的 curl 调⽤点,将「」设置成 Fiddler/Charles 的代理服务,然后⼀步步调试,从接⼝字段上理解数据库设计和 Controller 背后的业务逻辑。
最后,欢迎各位给我留⾔分享更多关于「HTTP PEM」和其他调试⽅法的经验与体会。1、编写返回的模拟数据
#2、运⾏ p hp 内置服务器
#使⽤ Fiddler/Charles 的 Map Local 功能
#⾃动化 Mock System 构想
#「HTTP PEM」系统分析利器
#
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论