
简介这是一份面向开发者与测试人员的Http请求模拟报文返回工具基于Tomcat等Java Web容器以war包形式运行。工具支持自定义状态码、响应头与响应体可按URL、请求方法等条件匹配规则让前端联调、接口测试、故障注入与压力预演无需依赖真实后端即可快速开展。资源包共45个文件体积约7.25MB包含可直接部署的war包以及23个class、14个jar、3个xml等配置与依赖文件另有txt说明、xlsx表格和properties配置便于理解部署结构和调整模拟规则。目前已有883人学习。借助该工具读者可快速搭建本地Mock服务验证前端逻辑与容错表现也能为自动化测试提供稳定可控的响应样本。1. 为什么你需要一个“Http请求模拟报文返回工具”前后端联调时后端接口还没写好前端拿不到数据整个页面卡死硬件设备上报了一串报文平台侧解析逻辑对不对没人敢拍板压测环境想模拟超时、超流量、异常状态码真实服务又不能随便搞。这三个场景的共同点是你需要一个能拦截HTTP请求、按规则返回预设报文的工具。它本质上是一个Mock服务对外是标准HTTP端点对内是可控的报文仓库。这个工具能解决的不只是「让联调不卡壳」更是把「后端没写好」「网络不稳定」「设备报文异常」这些不可控因素变成你本地可控的测试条件。适合谁前端、后端、测试、嵌入式开发凡是跟HTTP协议打交道的都绕不开它。这篇笔记我从选型、实现到参数配置和踩坑一条线讲完照着做就能在本地跑起来。2. 选型逻辑静态JSON、可编程Mock、报文回放怎么选才不返工2.1 三种方案的适用边界先说清楚常见的三种做法很多人上来就选最重的结果一半功能用不上还拖慢进度。第一类是静态JSON文件方案代表是json-server。它的思路是你写一个JSON文件它自动生成增删改查接口。优点是零代码、启动快适合前端联调CRUD缺点是报文格式写死在文件里想根据参数返回不同内容要写一堆json-server的rewrite规则表达式稍微复杂就卡住。而且它只能模拟「服务端正常返回」异常注入基本靠手动改文件再重启。第二类是可编程Mock服务代表是Node.js的express配合自定义中间件或者Go标准库直接写。它的核心能力是请求进来先走匹配逻辑匹配到了执行一段函数函数决定返回什么报文。状态码、响应头、响应体、延迟全部代码可控能模拟超时、500、空响应等异常。代价是你要写代码但这也是后面所有高级玩法的基础。第三类是报文录制回放用代理把真实请求转发到上游服务同时把请求和响应录下来存成文件之后离线回放。适合线上问题排查现场报文抓下来本地回放复现问题不依赖线上环境。我的选择是第二类里用Go实现。原因有三个一是编译出来单二进制没有运行时依赖丢到测试机、嵌入式Linux板上都能跑正好覆盖设备端联调场景二是标准库net/http足够强大不需要引第三方框架三是对报文匹配的控制粒度最细后面要加延迟、加动态变量都方便。提示如果只是想临时给前端提供几个假接口json-server半小时搞定但如果你要为设备调试、压测、异常注入做准备建议直接上可编程Mock省得后期返工。2.2 报文匹配机制决定工具好不好用的核心报文匹配是这类工具的灵魂。一个HTTP请求进来你怎么知道该返回哪条报文通常按优先级依次匹配这四层路径/api/device/status和/api/device/report是两个不同资源方法同样是/api/deviceGET表示查询POST表示上报返回可以完全不同请求头Content-Type: application/json和text/plain时解析方式不同某些场景还要匹配Authorization区分用户请求体设备上报的数据里cmdstart和cmdstop往往需要不同应答很多Mock工具只做路径方法匹配遇到设备上报这类「同路径不同报文」的场景就抓瞎。我一般会把请求体里的关键字段支持到匹配规则里用「包含」而不是「完全相等」因为真实请求体里往往带着时间戳、随机数完全相等永远匹配不上。另外要提醒的是匹配规则的优先级必须明确。我的习惯是精确路径 带参数约束 正则路径 兜底404或默认报文。否则两个规则同时命中返回哪条纯靠运气这种玄学问题排查起来最耗时间。3. 用Go从零搭一个最小可用的HTTP报文模拟工具3.1 最小实现一个main.go撑起全部功能我直接把一个能用的最小版本贴出来这段代码支撑了我在设备联调和前端Mock两个场景的大部分工作。先建一个mock_server.go文件代码如下package main import ( encoding/json io log net/http os strings time ) // Rule 定义一条报文返回规则 type Rule struct { Method string json:method // 请求方法如 GET/POST空表示所有方法 Path string json:path // 匹配路径支持精确或前缀 PathPrefix bool json:path_prefix,omitempty // true表示前缀匹配 ContentType string json:content_type,omitempty // 返回报文的Content-Type StatusCode int json:status_code // 返回状态码默认200 ResponseBody string json:response_body // 返回报文内容支持变量替换 DelayMs int json:delay_ms,omitempty // 模拟延迟单位毫秒 Headers map[string]string json:headers,omitempty // 额外响应头 } // Config 配置文件结构体 type Config struct { Port string json:port Rules []Rule json:rules } var rules []Rule func main() { // 读取配置文件 data, err : os.ReadFile(config.json) if err ! nil { log.Fatalf(读取配置文件失败: %v, err) } var cfg Config if err : json.Unmarshal(data, cfg); err ! nil { log.Fatalf(解析配置文件失败: %v, err) } rules cfg.Rules // 注册统一处理入口 http.HandleFunc(/, dispatch) addr : : cfg.Port log.Printf(HTTP报文模拟服务已启动监听 %s, addr) if err : http.ListenAndServe(addr, nil); err ! nil { log.Fatalf(服务启动失败: %v, err) } } func dispatch(w http.ResponseWriter, r *http.Request) { // 读取请求体用于后面的匹配判断 body, _ : io.ReadAll(r.Body) bodyStr : string(body) // 遍历规则表按配置顺序匹配 for _, rule : range rules { if !matchMethod(rule.Method, r.Method) { continue } if !matchPath(rule, r.URL.Path) { continue } // 命中规则按配置组装响应 if rule.DelayMs 0 { time.Sleep(time.Duration(rule.DelayMs) * time.Millisecond) } // 设置响应头 ct : rule.ContentType if ct { ct application/json; charsetutf-8 } w.Header().Set(Content-Type, ct) for k, v : range rule.Headers { w.Header().Set(k, v) } // 处理动态变量替换 respBody : replaceVars(rule.ResponseBody) status : rule.StatusCode if status 0 { status 200 } w.WriteHeader(status) w.Write([]byte(respBody)) log.Printf(命中的规则: %s %s - %d, rule.Method, rule.Path, status) _ bodyStr return } // 没有命中任何规则返回404和提示 w.Header().Set(Content-Type, application/json; charsetutf-8) w.WriteHeader(http.StatusNotFound) w.Write([]byte({code:404,msg:no rule matched})) } // matchMethod 方法匹配rule中Method为空时匹配所有方法 func matchMethod(expected, actual string) bool { if expected { return true } return strings.EqualFold(expected, actual) } // matchPath 路径匹配支持精确匹配和前缀匹配 func matchPath(rule Rule, actualPath string) bool { if rule.PathPrefix { return strings.HasPrefix(actualPath, rule.Path) } return actualPath rule.Path } // replaceVars 将报文中的{{now}}替换为当前时间戳 func replaceVars(body string) string { if strings.Contains(body, {{now}}) { body strings.ReplaceAll(body, {{now}}, time.Now().Format(2006-01-02 15:04:05)) } return body }对应的config.json长这样{ port: 8080, rules: [ { method: GET, path: /api/device/status, status_code: 200, content_type: application/json; charsetutf-8, response_body: {\code\:0,\data\:{\status\:\online\,\time\:\{{now}}\}} }, { method: POST, path: /api/device/report, status_code: 200, content_type: application/json; charsetutf-8, response_body: {\code\:0,\msg\:\report received\}, delay_ms: 100 } ] }启动命令很简单go run mock_server.go然后另开一个终端验证curl -X GET http://localhost:8080/api/device/status curl -X POST http://localhost:8080/api/device/report -d {cmd:start}3.2 代码逻辑说明为什么这么写这个最小实现的核心逻辑在dispatch函数读请求体 → 遍历规则 → 依次做方法和路径匹配 → 命中后按配置返回。有几个设计决策值得说明。matchMethod里用了strings.EqualFold这样配置里写get或GET都能匹配省得大小写不一致导致规则失效。matchPath支持前缀匹配模式配置里path_prefix: true后/api/device/就能命中/api/device/status和/api/device/report两个请求这对设备端调试特别有用——设备上报的真实路径往往带版本号前缀匹配能减少配置量。replaceVars目前只替换{{now}}时间戳。实际使用中我还会加上{{uuid}}、{{rand}}实现方式就是strings.ReplaceAll继续叠。注意变量替换发生在响应头设置之后、WriteHeader之前顺序不要乱否则响应头可能已经写入后面再改就不生效了。log.Printf打印命中的规则这一点千万别删。Mock工具的黑匣子问题很严重——你明明配置了规则请求进来却没返回预期报文没有日志你根本不知道规则有没有被遍历到。任何一次联调翻车排查第一步永远是看命中日志而不是改配置。提示如果启动时报listen tcp :8080: bind: address already in use说明端口被占了。用lsof -i :8080或netstat -ano | grep 8080找出占用进程换个端口启动或者把占用进程处理掉。4. 参数配置路由、状态码、延迟、Content-Type全都在config.json里4.1 路由匹配参数从精确路径到正则的边界问题上一章的代码里我只实现了精确匹配和前缀匹配但真实场景中还需要正则。例如设备上报的路径可能是/api/v1/device/SN123456/report中间是动态的设备序列号前缀匹配能做到但要匹配SN开头加8位数字这个格式就做不到了。我给路由规则加一个PathRegexp字段命中逻辑改成先试精确匹配再试前缀匹配最后试正则。一个坑是JSON里写正则表达式必须双反斜杠转义例如path_regexp: ^/api/v\\d/device/.?/report$如果你写单反斜杠编译正则时直接报错。这个坑我第一次踩的时候排查了半小时最后打印出配置才发现反斜杠被JSON转义吃掉了一半。正则匹配还会带来一个性能问题每条请求遍历所有规则时每一条规则都要编译一次正则。如果规则表有几十条高并发下CPU会被正则编译打满。解决方法是启动时预编译把正则对象缓存起来请求处理只做匹配不做编译。这个优化在规则超过20条、QPS过千时效果明显。4.2 响应参数状态码、延迟和Content-Type的配合响应是否正常先看状态码和Content-Type这两个参数。状态码的模拟不只是200和404异常场景反而更常用500模拟服务端内部错误503模拟服务不可用配合延迟参数可以让调用方触发重试逻辑429模拟限流设备端如果做了退避重连这里正好验证302模拟重定向验证客户端是否完整处理了跳转Content-Type是一个经常被忽略、坑却很多的参数。application/json; charsetutf-8和application/json的区别在于前者显式声明了字符集。如果返回报文里有中文不写charsetutf-8有的HTTP客户端尤其是嵌入式平台的库会按ISO-8859-1解码中文字符直接乱码。前端axios的get请求还好浏览器会自动探测但设备端很多HTTP库不做探测完全信任响应头。延迟参数的设置要谨慎。delay_ms是让整个请求处理流程time.Sleep指定的毫秒数模拟的是「网络慢」或者「服务端处理慢」。但它有个副作用Go的http.ListenAndServe默认每个请求开一个goroutineSleep不阻塞其他请求所以延迟不会拖垮整个服务——前提是你没把连接池打满。另外延迟只加在命中的规则上如果规则没命中返回404那延迟参数是不生效的别指望用404来模拟超时。下面整理一个参数速查表参数类型默认值说明portstring无必填监听端口建议避开9000以下系统常用端口methodstring空匹配全部请求方法不区分大小写pathstring无必填精确路径或前缀path_prefixboolfalsetrue时path按前缀匹配path_regexpstring空正则路径匹配优先级低于精确匹配status_codeint200响应状态码0时按200处理content_typestringapplication/json; charsetutf-8响应Content-Typeresponse_bodystring空响应体支持{{now}}变量delay_msint0延迟毫秒数0表示不延迟headersmap空额外的自定义响应头4.3 请求体参数匹配设备报文和web请求的差异处理HTTP请求模拟报文返回工具真正区分「能用」和「好用」的点在请求体匹配。设备上报的场景里同一个路径POST上来的JSONcmd字段不同应答报文必须不同。比如{cmd:start,seq:123}应答{result:starting}{cmd:stop}应答{result:stopped}。我给规则增加一个BodyContains字段用strings.Contains判断请求体是否包含指定字符串。这里有个经验不要用完全相等匹配请求体。因为真实请求里总有seq、timestamp、nonce这类动态字段完全相等意味着你每次都得手工改配置才能对上。用包含匹配只要关键字段在就行其它字段随意变都不影响命中。如果要把cmdstart和cmdstop两条规则区分开配置里都写body_contains: \cmd\:\start\和body_contains: \cmd\:\stop\就行。注意JSON内嵌JSON时要转义内部引号这个转义问题也是高频踩坑点。5. 避坑指南HTTP报文模拟工具最常见的5个翻车现场5.1 现象POST请求永远匹配不上规则明明配置了POST路径curl发过去却返回404翻车现场。排查发现我配置规则时method写了post代码里虽然用了strings.EqualFold做匹配但服务配置文件的字段解析时如果写成POST 带了空格或者写错大小写匹配就失败了。更常见的原因是POST请求体没读出来——代码里io.ReadAll(r.Body)是有长度的如果规则定义在读取body之前做了重定向或者提前写入了响应body可能已经被消费了。解决方法是配置里method统一写成大写请求体的读取放在所有匹配逻辑之前日志里打印实际请求方法、路径和body的前128字节一眼就能看出差异。5.2 现象返回报文里的中文全部乱码这个问题的根源几乎都是Content-Type没带charsetutf-8。我试过把Content-Type设置为application/json浏览器里显示正常但设备端SDK比如某些嵌入式HTTP库强制按Latin-1解码中文全变成问号。解决方法是配置里默认值写成application/json; charsetutf-8并且在代码里兜底——如果规则没指定Content-Type就用这个默认值。我第一次调STM32设备上的HTTP库时就是栽在这个坑上后来养成了习惯只要返回体里有非ASCII字符就显式声明charsetutf-8。5.3 现象改了配置但客户端拿到的还是旧报文这是我在联调时最头疼的一个现象——规则已经修改服务端日志也显示新规则命中但前端axios拿到的响应还是老的。原因分析后确认是HTTP连接复用客户端对同一个域名和端口建立了持久连接连接池里的TCP连接还保持着服务端改了逻辑但客户端侧的连接没有断开后续请求走了旧连接。解决方法是修改配置后重启服务让连接全部断开粗放但有效或者在响应头里加Connection: close强制关闭连接会影响性能最优雅的做法是给响应报文加版本号字段前端联调时对比版本号不一致就知道需要刷新。我的习惯是规则改了之后不仅要重启服务还要确认客户端确实发起了新连接日志里能看到对端端口变化。5.4 现象容器化部署拉取基础镜像失败有人喜欢用Docker部署这类Mock服务但经常碰到error response from daemon: get https://registry-1.docker.io/v2/: net/http这类拉取镜像报错。原因多数是国内环境直连Docker Hub不稳定或者网络策略受限。解决方法是给Docker配置可用的镜像源加速更彻底的做法是直接用二进制部署——Go编译出的可执行文件没有运行时依赖放到服务器上chmod x后直接运行不需要容器这层封装。设备联调场景我基本不用容器单二进制拷贝到设备上更省事。5.5 现象端口被占用提示“请求的资源在使用中”启动服务时listen tcp :8080: bind: address already in useWindows下往往直接提示「请求的资源在使用中」。这通常是你开了多个Mock服务实例或者8080被其它开发工具占用。排查步骤先lsof -i :8080查看占用进程确认是不是自己之前启动的残留进程如果是残留kill掉如果不确定能不能杀直接换端口启动。我一般在配置里用10000以上的端口避开常见的8080、3000冲突概率小很多。6. 进阶玩法报文录制回放 自动化验证把Mock工具变成调试利器6.1 录制真实报文离线回放模拟报文最怕的是「我编的报文跟线上真实情况对不上」。解决办法是做一个录制代理在Mock服务里加一个forward_url配置项请求进来后先用httputil.NewSingleHostReverseProxy把请求原样转发到真实服务拿到真实响应后一边返回给调用方一边把请求和响应写入本地日志文件。这样每一次真实调用都变成了一条可复现的规则。我通常把录制文件按日期滚动存储文件名包含时间戳方便后面的回放和对比。// 录制代理核心逻辑伪代码示意 proxy : httputil.NewSingleHostReverseProxy(forwardURL) recorder : responseRecorder{ResponseWriter: w, status: 200} proxy.ServeHTTP(recorder, r) // recorder.Body 里就是真实响应报文 saveToFile(r, recorder.Body)随后把录到的真实报文导入规则表之前那套匹配逻辑原样处理。实测中最有用的是处理那些「只有线上才出现的奇怪报文」——不用去猜线上报文长什么样录下来直接当作Mock规则的一部分回归时用Mock规则就能复现线上行为。6.2 自动化验证用回放差异找出客户端兼容性问题回放不是终点验证才是。我一般会写一个对比脚本把录制时保存的原始响应和回放时Mock服务产生的响应做结构化对比——不仅比状态码还要比响应体里的关键字段、响应头里的Content-Type和缓存策略。# 用curl对比录制响应和回放响应的差异 curl -s -D - http://localhost:8080/api/device/report | grep -E ^(HTTP|Content-Type) curl -s http://localhost:8080/api/device/report --output replay.json diff recorded.json replay.jsondiff有输出时先判断差异字段是不是{{now}}这类动态替换产生的如果是就正常如果是固定字段不一致说明规则配置或者模板替换有遗漏。这步看似简单但我在实际项目里靠它抓出过不止一个客户端硬编码字段值的问题——设备端判断服务端返回时写死了某个字段值Mock工具换了一条真实报文的模板客户端立马崩。做这套东西我最大的教训是一开始只模拟成功响应异常响应全靠临时改配置结果联调时客户端在超时、5xx、字段缺失这些场景下全部翻车。后来我把高频异常场景做成了内置规则用falcon这个名字给每个异常场景取名请求路径带对应参数就能触发省掉了反复改配置文件的工作。现在每次搭Mock服务我都会先问一句这个接口真正上线时会遇到哪些异常把这些异常写进规则表比写一百条成功响应更有价值。希望这篇笔记能帮你在下次联调或调试时少踩几个坑把这块工具真正用起来。本文还有配套的精品资源点击获取