1. 从一个真实联调场景说起:为什么二进制流请求值得单独拎出来讲
做过接口联调的人大概率都遇到过这种局面:后端同事甩过来一个接口文档,上面写着“请求体为二进制流,Content-Type: application/octet-stream”,然后你打开 Postman,习惯性地切到 raw 里的 JSON 模式,把参数拼成{"file": "xxx"}发过去,结果对面直接返回 400 或者一堆乱码。更让人抓狂的是,有时候接口在 Swagger 上点一下能通,换成 Postman 就死活不行,排查半天发现根本不是参数问题,而是请求体的编码方式从根上就错了。
这篇内容就是围绕“把数据转成二进制流文件,再用 Postman 发二进制流请求”这条链路展开的。它看起来只是一个小操作,但背后牵扯到的东西其实不少:数据为什么要转成二进制、二进制流和普通表单上传到底差在哪、Postman 里那几个 Body 选项分别对应什么场景、服务端拿到流之后又是怎么解析的。我打算把这些年在这块踩过的坑、总结出来的套路,一次性讲清楚。
适合谁看?如果你是需要做文件上传、大文件传输、设备数据上报、跨系统数据对接的后端或前端开发,或者你只是单纯被 Postman 的 binary 选项搞晕过,那这篇内容应该能帮你省下不少翻文档的时间。我会尽量用大白话把原理讲透,同时给出可以直接抄的操作步骤,保证你看完就能上手。
先说一个最容易被忽略的前提:二进制流请求不是“另一种发 JSON 的方式”,它和 JSON、form-data 是并列的、互斥的请求体形态。理解这一点,后面很多困惑就自然解开了。
2. 二进制流请求到底解决什么问题:和 JSON、form-data 的边界在哪
2.1 三种请求体形态的本质区别
很多人把 Postman 里的raw、form-data、binary当成三个可以随便换的选项,其实它们对应的是完全不同的数据组织方式。
raw模式(配合 JSON、XML、Text)是把数据当成文本来发,请求体是一串字符,服务端按字符编码去解析。JSON 就是最典型的例子,它的本质是“结构化的文本”。
form-data模式是多部分表单,它会把每个字段用 boundary 分隔开,既能传文本字段,也能传文件字段,服务端按 multipart 协议解析。浏览器里那个<input type="file">提交表单,走的就是这套。
binary模式则是把整个请求体当成一段纯粹的字节流,没有任何字段名、没有分隔符、没有结构,就是一坨连续的二进制数据。服务端拿到的是InputStream,读到什么就是什么。
这三者的关系可以用一个类比来理解:JSON 像是一封格式规范的信,有抬头有正文;form-data 像是一个快递箱,里面分格子装了不同的东西,还贴了标签;而 binary 就是直接把一袋散装原料倒给你,不贴标签不分格,全靠你自己知道这袋是什么。
2.2 什么时候必须用二进制流
那什么时候非用 binary 不可?我总结了几类典型场景。
第一类是文件直传。比如你要把一个图片、音频、PDF 的原始字节直接推给服务端,服务端不做表单解析,直接落盘或者转发。这时候用 binary 最干净,省去了 multipart 的封装开销。
第二类是大文件或流式数据。multipart 在传输大文件时,服务端往往需要先把整个请求缓冲下来才能解析 boundary,内存压力大。而纯二进制流可以边读边处理,天然适合流式场景。
第三类是设备或协议对接。很多硬件设备、工业协议上报的数据本身就是二进制帧,比如定长的字节数组,里面按位定义了各种状态。这种数据你没法用 JSON 表达,只能原样透传。
第四类是自定义序列化格式。有些系统之间约定用 Protobuf、MessagePack、Thrift 这类二进制序列化协议通信,序列化出来的就是字节流,直接 binary 发送最合适。
2.3 一个常见的认知误区
这里要特别纠正一个误区:binary 不等于“文件上传”。文件上传只是 binary 的一种应用,binary 的本质是“请求体即字节流”。你完全可以用 binary 发送一段根本不是文件的数据,比如一个序列化后的对象、一段加密后的密文、甚至一段纯文本(只是用二进制的方式发)。
反过来,文件上传也不一定非要用 binary。如果你用的是 multipart 表单,文件是作为其中一个 part 传的,那走的是 form-data 而不是 binary。所以判断标准不是“是不是传文件”,而是“服务端期望的请求体是不是一整段裸字节”。
搞清楚这个边界,你在 Postman 里选 Body 类型的时候就不会再犹豫了。
3. 把数据转成二进制流文件:几种主流做法和它们的取舍
3.1 为什么需要先“转成文件”
标题里有个动作叫“将数据转二进制流文件”,这个“转成文件”其实有两层含义。一层是真的在磁盘上生成一个.bin文件,方便用 Postman 的 binary 模式选中它发送;另一层是把内存里的数据序列化成字节数组,不一定落盘。
为什么要落盘成文件?因为 Postman 的 binary 模式只支持“选择一个文件”作为请求体,它不给你直接粘贴字节数组的入口。所以如果你手上是一段程序生成的数据,想用 Postman 测,最直接的办法就是先把它写成文件,再在 Postman 里选这个文件。
这就引出了几种常见的转换方式,我按使用频率从高到低排一下。
3.2 用代码把对象序列化成字节流
如果你是在写代码的过程中顺手要造测试数据,最自然的做法就是在代码里序列化。以 Java 为例,把一个对象转成字节数组有几种路子:
// 方式一:Java 原生序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(myObject); byte[] bytes = bos.toByteArray(); Files.write(Paths.get("payload.bin"), bytes);// 方式二:JSON 转字节(本质还是文本,但按字节写) String json = objectMapper.writeValueAsString(myObject); byte[] bytes = json.getBytes(StandardCharsets.UTF_8); Files.write(Paths.get("payload.bin"), bytes);// 方式三:Protobuf 序列化 byte[] bytes = myProtoObject.toByteArray(); Files.write(Paths.get("payload.bin"), bytes);Python 这边更简单:
import struct # 按自定义格式打包二进制 data = struct.pack('<IHH', 12345, 1, 2) with open('payload.bin', 'wb') as f: f.write(data)这里有个关键点:写文件一定要用二进制模式(Java 的Files.write默认就是字节,Python 要加'wb')。用文本模式写二进制数据,遇到0x0A、0x0D这类字节会被做换行转换,数据就毁了。这个坑我在早期项目里踩过,一个 CRC 校验死活对不上,查了一下午才发现是写文件时用了文本模式。
3.3 用命令行工具快速造二进制文件
有时候你不想写代码,只是想快速造一个测试用的二进制文件。Linux 和 macOS 下有几个现成工具很好用。
dd可以生成指定大小的随机或零填充文件:
# 生成 1MB 的零填充文件 dd if=/dev/zero of=payload.bin bs=1024 count=1024 # 生成 1MB 的随机数据 dd if=/dev/urandom of=payload.bin bs=1024 count=1024printf可以精确构造字节序列:
# 写入 4 字节魔数 + 2 字节版本号 printf '\x89PNG\r\n\x1a\n' > payload.binWindows 下可以用 PowerShell:
# 生成 1KB 随机字节 $bytes = New-Object byte[] 1024 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) [System.IO.File]::WriteAllBytes("payload.bin", $bytes)这些命令的好处是快,不用起 IDE,随手就能造一个文件出来测接口。
3.4 十六进制编辑器:手工微调字节的利器
如果你需要精确控制每一个字节,比如构造一个特定协议帧,十六进制编辑器是绕不开的。像 HxD、010 Editor 这类工具,能让你直接看到文件的字节内容,按位修改。
我处理过一个设备上报协议,帧头是固定的0xAA 0x55,后面跟长度、命令字、数据、校验和。这种帧用代码生成当然可以,但调试阶段用十六进制编辑器手工改几个字节,比改代码重新编译快得多。改完保存,Postman 里重新选一下文件就能发,效率很高。
提示:用十六进制编辑器改完文件后,记得确认文件大小有没有变化。有些编辑器会在末尾自动补字节,导致长度字段和实际不符,服务端校验就会失败。
3.5 转换方式的选择建议
把上面几种方式对比一下,方便你按场景选:
| 方式 | 适用场景 | 优点 | 注意点 |
|---|---|---|---|
| 代码序列化 | 造真实业务数据 | 数据准确、可复用 | 注意二进制写模式 |
| 命令行工具 | 快速造测试文件 | 无需写代码 | 字节内容不可控 |
| 十六进制编辑器 | 精确构造协议帧 | 逐字节可控 | 防止误改文件长度 |
| 抓包导出 | 复现真实请求 | 数据最真实 | 需要能抓到原始流 |
我的习惯是:日常测试用命令行造个随机文件就够了;要复现线上问题时,优先抓包导出原始请求体;只有构造特定协议帧时才动用十六进制编辑器。
4. Postman 发送二进制流请求的完整操作链路
4.1 请求方法和 URL 的确认
二进制流请求通常用POST或PUT。POST用于创建或提交,PUT用于整体替换。具体用哪个,看接口文档怎么定义,别自己猜。
URL 这块没什么特别的,但要注意一点:有些接口会把参数放在 URL 上,请求体只放二进制数据。比如POST /upload?filename=test.bin&type=raw,文件名和类型走 query,字节流走 body。这种设计很常见,因为二进制体里没法再塞元数据。
4.2 Body 选 binary 之后发生了什么
在 Postman 里,选中Body标签,然后选binary单选按钮,界面会变成一个文件选择框。你点“Select File”选一个文件,Postman 就会把这个文件的全部字节作为请求体发出去。
这里有个细节值得说:Postman 不会对你选的文件做任何加工。它不会加 boundary,不会做 base64 编码,不会加字段名,就是原样把字节读出来塞进 HTTP body。这一点和 form-data 完全不同,form-data 会帮你封装 boundary 和字段头。
所以你在 Postman 里选的文件是什么字节,服务端收到的就是什么字节。这也意味着,如果你选错了文件,服务端拿到的就是错的数据,没有任何“容错”空间。
4.3 Content-Type 头到底要不要手动设
这是问得最多的一个问题。答案是:要设,而且要和接口约定一致。
Postman 在 binary 模式下,默认可能会带一个Content-Type,也可能不带,取决于版本。稳妥的做法是手动在 Headers 里加一条:
Content-Type: application/octet-streamapplication/octet-stream是“未知二进制流”的通用 MIME 类型,意思是“我也不知道这是什么,反正是一堆字节”。如果接口约定了更具体的类型,比如image/png、application/pdf、application/x-protobuf,那就按约定的写。
为什么这个头重要?因为服务端框架往往靠它来决定用哪个解析器。Spring MVC 里,如果Content-Type是application/octet-stream,你才能用@RequestBody byte[]或InputStream接;如果头不对,框架可能直接拒绝或者按错误的方式解析。
注意:有些接口对 Content-Type 做严格校验,多一个字符都不行。比如约定
application/octet-stream,你写成application/octet-stream; charset=utf-8,某些严格的网关就会拦下来。联调时如果一直 415,先检查这个头。
4.4 其他可能需要的请求头
除了 Content-Type,还有几个头在二进制场景下经常出现。
Content-Length一般不用手动设,Postman 会根据文件大小自动算。但如果你在代码里手写 HTTP 请求,这个头必须准确,否则服务端读流会出问题。
Content-MD5或自定义的校验头,有些接口要求对请求体做摘要,防止传输损坏。这种头需要你根据文件内容算出来填进去,Postman 里可以用 Pre-request Script 动态计算。
Authorization之类的鉴权头,按接口要求加就行,和二进制本身无关。
4.5 发送与响应观察
点 Send 之后,重点看两件事:状态码和响应体。二进制接口的响应可能是 JSON,也可能是二进制流。如果是二进制流,Postman 的响应区会显示乱码,这时候可以点“Save Response”把它存成文件,再用十六进制编辑器看。
如果返回 400,先怀疑请求体内容不对;返回 415,怀疑 Content-Type 不对;返回 500,多半是服务端解析流的时候抛异常了,这时候要去看服务端日志。
5. 服务端是怎么接住这坨字节的:解析逻辑与常见异常
5.1 不同框架接收二进制流的方式
服务端接收二进制流,核心就是拿到那个InputStream。不同框架写法不一样,但思路一致。
Spring MVC 里,最直接的方式是用@RequestBody接字节数组:
@PostMapping(value = "/upload", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE) public String upload(@RequestBody byte[] data) { // data 就是完整的请求体字节 return "received " + data.length + " bytes"; }如果数据量大,不想一次性读进内存,可以用InputStream:
@PostMapping(value = "/upload", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE) public String upload(InputStream inputStream) throws IOException { byte[] buffer = new byte[8192]; int len; long total = 0; while ((len = inputStream.read(buffer)) != -1) { total += len; // 边读边处理,比如写文件、算摘要 } return "received " + total + " bytes"; }Node.js 的 Express 里,需要先挂一个 raw body 解析中间件:
app.post('/upload', express.raw({ type: 'application/octet-stream', limit: '50mb' }), (req, res) => { const buffer = req.body; // Buffer 对象 res.send(`received ${buffer.length} bytes`); });Python 的 Flask 里更简单:
@app.route('/upload', methods=['POST']) def upload(): data = request.get_data() # 原始字节 return f'received {len(data)} bytes'5.2 流式读取与内存控制
这里要重点说一下流式读取。很多人图省事,直接@RequestBody byte[]一把梭,小文件没问题,但遇到几百 MB 甚至上 G 的文件,JVM 直接 OOM。
正确的做法是流式处理:开一个固定大小的缓冲区(比如 8KB 或 64KB),循环读,读一块处理一块。处理可以是写磁盘、算哈希、转发给下游,总之不要全攒在内存里。
缓冲区大小也有讲究。太小了系统调用频繁,性能差;太大了内存占用高。8KB 到 64KB 是常见区间,具体看你的 IO 特性。如果是网络转发,64KB 通常比较合适;如果是本地磁盘写,8KB 到 32KB 就够。
5.3 长度校验与完整性检查
服务端拿到流之后,第一件事往往是校验长度。如果请求头里有Content-Length,而实际读到的字节数和它对不上,说明传输过程中出了问题,应该直接拒绝。
第二件事是校验完整性。如果客户端带了Content-MD5或自定义校验和,服务端要重新算一遍对比。这一步能挡住网络传输中的位翻转,也能挡住客户端造数据时的错误。
我遇到过一种情况:客户端用文本模式写文件,把0x0D 0x0A变成了0x0A,长度少了一个字节,MD5 自然对不上。服务端报校验失败,客户端还一脸懵说“我数据没动啊”。所以校验这一步非常值得做,它能帮你快速定位是数据本身错了还是传输错了。
5.4 常见异常对照表
把二进制流接口常见的异常整理成一张表,方便排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 415 Unsupported Media Type | Content-Type 不匹配 | 检查请求头与接口 consumes 是否一致 |
| 400 Bad Request | 请求体为空或格式不符 | 确认 Postman 选了文件且文件非空 |
| 500 解析异常 | 服务端读取流时出错 | 看服务端日志,检查缓冲区逻辑 |
| 校验失败 | 字节内容被改动 | 对比客户端和服务端的字节数、MD5 |
| 收到乱码 | 编码方式不一致 | 确认是否误用了文本模式 |
这张表基本覆盖了我这些年遇到的大部分问题,联调时对着查能省不少时间。
6. 踩坑实录:那些年二进制流联调翻过的车
6.1 文本模式写文件导致字节被篡改
这是最经典的一个坑,前面提过但我还想再强调一次。Windows 平台上,用文本模式写文件时,\n(0x0A)会被自动转成\r\n(0x0D 0x0A)。如果你写的是二进制数据,里面恰好有 0x0A 字节,写出来就多了一个 0x0D,数据全乱。
解决办法只有一个:写二进制数据永远用二进制模式。Java 用Files.write或FileOutputStream,Python 用'wb',C 用"wb"。别偷懒用文本模式,出了问题很难查。
6.2 Postman 缓存了旧文件内容
Postman 选文件之后,如果你在磁盘上修改了这个文件,Postman 不一定会重新读取。它可能还拿着旧的内容发。这个行为在不同版本里表现不一样,但确实坑过我好几次。
规避方法:改完文件后,在 Postman 里重新点一次“Select File”重新选一遍,或者干脆换个文件名。别指望它自动刷新。
6.3 Content-Type 被 Postman 自动覆盖
有时候你明明在 Headers 里设了Content-Type: application/octet-stream,发出去却变成了别的值。这通常是因为 Postman 的某些设置或者环境变量把它覆盖了。
排查方法:用 Postman 的 Console(View -> Show Postman Console)看实际发出的请求头。Console 里能看到最终生效的头,比界面上的更可信。
6.4 大文件发送时 Postman 卡死
Postman 处理超大文件(比如几个 G)时,可能会卡死或者内存爆掉。因为它要把文件读进内存再发。这种场景建议换工具,比如用curl:
curl -X POST \ -H "Content-Type: application/octet-stream" \ --data-binary @payload.bin \ http://localhost:8080/upload--data-binary是关键,它保证文件按二进制原样发送,不做任何转换。如果用-d,curl 可能会做一些处理,二进制场景下要用--data-binary。
6.5 服务端把流读了两遍
有些服务端代码里,先读一遍流算 MD5,再读一遍流写文件。但 HTTP 请求的InputStream通常只能读一次,读完就到底了。第二遍读会读到空,导致文件为空或者校验失败。
解决办法:要么读一遍的同时算 MD5 和写文件(一次遍历干两件事),要么先把流缓存到临时文件再处理。千万别指望流能重置。
7. 让二进制流联调更顺手的几个实战技巧
7.1 用 Pre-request Script 动态算校验头
如果接口要求Content-MD5,手动算太麻烦。Postman 的 Pre-request Script 可以帮你自动算。不过 Postman 的脚本环境里读文件不太方便,一个变通做法是先把文件内容以 base64 形式存到环境变量,脚本里解码后算 MD5。这个方案适合小文件,大文件还是建议用代码或 curl。
7.2 保存响应流用于对比
服务端如果返回二进制流,Postman 里点“Save Response”存成文件,然后用cmp或fc命令和原始文件对比:
cmp original.bin response.bin如果完全一致,说明链路没问题;如果不一致,用十六进制编辑器看第一个不同的字节在哪,往往能定位到问题环节。
7.3 用 curl 做交叉验证
Postman 出问题时,用 curl 发同样的请求,能快速判断是 Postman 的问题还是服务端的问题。如果 curl 能通 Postman 不通,那就是 Postman 配置问题;如果两个都不通,那就是服务端或网络问题。这个交叉验证思路非常实用。
7.4 构造边界测试数据
二进制接口的健壮性,很大程度上取决于边界情况处理。建议准备几组测试数据:空文件(0 字节)、1 字节文件、刚好等于缓冲区大小的文件、比缓冲区大 1 字节的文件、超大文件。这几组能覆盖大部分边界 bug。
我印象很深的一次,服务端代码在缓冲区刚好读满时漏处理了最后一块,导致文件末尾丢数据。用“刚好等于缓冲区大小”的文件一测就现原形了。
7.5 日志里打印字节数和摘要
服务端处理二进制流时,建议在日志里打印收到的字节数和内容摘要(比如 MD5 前 8 位)。这样出问题时,对比客户端和服务端的日志,一眼就能看出是数据没发对还是没收到。这个习惯帮我省了无数次来回扯皮。
8. 关于这套流程我个人的几点体会
二进制流这个东西,说难不难,说简单也不简单。它的难点不在于技术本身,而在于“看不见”——JSON 出问题你能肉眼看出字段不对,二进制出问题你看到的是一堆乱码,只能靠字节数、摘要、十六进制对比来定位。
我的经验是,处理二进制接口时一定要建立“字节意识”。你要时刻清楚:我发出去的是哪些字节,服务端收到的是哪些字节,中间有没有被转换过。只要这个链条上任何一环做了隐式转换(文本模式写文件、编码转换、框架自动解析),数据就可能被悄悄改掉。
另外,工具的选择也很重要。Postman 适合小规模、交互式的调试,直观方便;但涉及大文件、批量测试、自动化时,curl 和代码脚本更靠谱。别死磕一个工具,什么场景用什么工具。
最后说一个心态上的建议:二进制联调出问题时,不要急着怀疑服务端,先用最小可复现的用例把问题隔离出来。一个 4 字节的文件就能测出很多问题,没必要一上来就拿真实业务数据去试。把变量控制到最少,问题自然就浮出来了。