1. 为什么要把数据转成二进制流来传做过接口联调的人多半遇到过这种场景后端定义了一个上传接口Content-Type 写的是application/octet-stream然后甩过来一句你直接把文件流发过来就行。这时候如果你还在用 form-data 或者 JSON 去凑大概率会收到 400 或者一堆乱码。二进制流传输不是什么高深技术但它在文件上传、大文件分片、跨系统数据交换里出现的频率极高尤其是当对方系统只认原始字节流、不接受任何包装格式的时候。所谓把数据转二进制流本质上是把内存里的字符串、对象、文件内容按照某种编码规则UTF-8、GBK、Base64 解码后的原始字节等变成一串byte[]再通过 HTTP 请求体直接写出去。它和普通 JSON 请求最大的区别在于JSON 是文本协议有明确的结构和字符集而二进制流是裸的接收方拿到什么就是什么没有任何自描述信息。这就意味着发送端和接收端必须提前约定好编码方式、字节序、字段分隔规则否则收到的就是一堆无法解析的字节。这篇文章适合三类人看一是刚接触接口联调、被octet-stream卡住的后端或测试同学二是需要模拟客户端上传行为、用 PostMan 做接口验证的开发者三是想搞清楚流到底是怎么回事、不想再靠复制粘贴混过去的人。我会从数据转换的底层逻辑讲起一直讲到 PostMan 里怎么配置、怎么排查乱码和 400 错误把踩过的坑都摊开说。2. 二进制流传输的核心原理拆解2.1 文本、字节、流的三角关系要理解二进制流先得把这三个概念理清楚。文本是给人看的比如hello这五个字符字节是给机器看的hello用 UTF-8 编码后是 5 个字节68 65 6C 6C 6F流则是字节的传输形式它不关心内容是什么只负责把字节从 A 搬到 B。很多人卡在第一步是因为把字符串和字节数组混为一谈。在 Java 里String.getBytes()得到的是byte[]在 Python 里abc.encode()得到的是bytes在 JavaScript 里要用Buffer.from()或者TextEncoder。这些转换动作看起来简单但编码选错了后面全盘皆输。比如中文用 ISO-8859-1 编码一个汉字会变成?因为那个字符集根本不认识中文。流的概念可以用水管来类比你不需要知道水从哪里来、要到哪里去只需要保证管子接对了、水压够、别漏水。HTTP 请求体就是那根管子Content-Type: application/octet-stream就是在告诉对方我管子里流的是原始字节你自己按约定解析。2.2 为什么不用 JSON 或 form-data有人会问既然 JSON 这么方便为什么还要费劲搞二进制流答案在几个具体场景里文件上传图片、视频、压缩包本身就是二进制硬塞进 JSON 要 Base64 编码体积膨胀 33%大文件直接爆炸。性能敏感场景二进制省去了序列化和反序列化的开销几 MB 的数据用流传输比 JSON 快一个数量级。对接老系统很多传统系统比如某些金融、医疗接口只认固定格式的字节流字段按位排列多一个空格都不行。分片传输大文件切成固定大小的块每块作为独立的二进制流发送接收端按顺序拼接。反过来说如果只是传几个结构化字段用 JSON 完全够用没必要为了显得高级去搞二进制。工具选型要看场景不是越底层越好。2.3 Content-Type 与请求体的约定application/octet-stream是 HTTP 协议里最模糊的 Content-Type 之一它的字面意思是任意二进制数据。这意味着接收方无法从请求头推断出内容格式必须依赖双方事先约定。常见的约定方式有几种约定方式说明适用场景固定字段顺序按字节偏移量解析如前 4 字节是长度后面是内容协议固定的老系统前置元数据请求体开头放一段 JSON 描述后续内容自定义协议文件名约定通过 URL 参数或 Header 传递文件名和类型文件上传纯内容流请求体就是文件本身无额外包装简单文件上传我见过最多的坑是后端说你发二进制就行但没说编码是 UTF-8 还是 GBK结果中文全乱。所以联调前一定要问清楚三件事编码格式、字段排列规则、是否有长度前缀。这三样问明白了后面基本不会出大问题。3. 数据转二进制流的实操要点3.1 字符串转字节数组的正确姿势不同语言里字符串转字节的方式不一样但核心都是指定编码。以 Java 为例String data 订单编号:12345,金额:99.5; // 正确做法明确指定 UTF-8 byte[] bytes data.getBytes(StandardCharsets.UTF_8); // 错误做法依赖平台默认编码跨系统必乱 byte[] badBytes data.getBytes();Python 里更直接data 订单编号:12345,金额:99.5 raw data.encode(utf-8) # 得到 bytes # 如果要发十六进制可以再转 hex_str raw.hex()JavaScript 在 Node 环境用 Bufferconst data 订单编号:12345,金额:99.5; const buf Buffer.from(data, utf-8); // 浏览器环境用 TextEncoder const encoder new TextEncoder(); const uint8 encoder.encode(data);这里有个细节值得说Java 的getBytes()不带参数时会用系统默认编码在 Windows 上可能是 GBK在 Linux 上可能是 UTF-8。本地测试通过、部署到服务器就乱码十有八九是这个原因。养成显式指定编码的习惯能省掉大量排查时间。3.2 结构化数据如何拼成字节流如果数据不是简单字符串而是多个字段就需要自己定义拼接规则。常见的有两种定长字段拼接每个字段占固定字节数不足补空格或零。比如协议规定姓名 20 字节 年龄 4 字节 金额 8 字节那就按这个规则拼ByteArrayOutputStream baos new ByteArrayOutputStream(); baos.write(padRight(name, 20).getBytes(StandardCharsets.UTF_8)); baos.write(padLeft(age, 4).getBytes(StandardCharsets.UTF_8)); baos.write(padLeft(amount, 8).getBytes(StandardCharsets.UTF_8)); byte[] payload baos.toByteArray();分隔符拼接字段之间用特定字符如|或\u0001分隔接收端按分隔符切分。这种方式灵活但要求分隔符不能出现在内容里否则会解析错位。提示定长拼接时中文字符的字节数取决于编码。UTF-8 下一个汉字占 3 字节GBK 下占 2 字节。如果协议规定姓名 20 字节用 UTF-8 最多只能放 6 个汉字超了要么截断要么报错这个边界一定要测。3.3 文件读取为字节流的几种方式文件本身就是二进制读取时要注意别用文本模式打开。Java 里用Files.readAllBytes()最省事byte[] fileBytes Files.readAllBytes(Paths.get(/path/to/file.pdf));大文件不要一次性读进内存用流式读取try (InputStream is new FileInputStream(file)) { byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { // 写入输出流或发送 } }Python 里读文件要加rbwith open(/path/to/file.pdf, rb) as f: file_bytes f.read()这里有个容易忽略的点文件读取后如果要做 Base64 编码再传输体积会膨胀但如果接口要求的就是 Base64 字符串那也没办法。判断标准是看接口文档里的 Content-Type如果是application/octet-stream就直接发原始字节如果是application/json且字段是字符串那才需要 Base64。4. PostMan 发送二进制流请求全流程4.1 请求配置的关键三步PostMan 发二进制流请求核心就三步但每一步都有坑。第一步选对 Method 和 URL。通常是 POST 或 PUTURL 按接口文档填。这一步没什么好说的但要注意有些接口对 URL 末尾的斜杠敏感/api/upload和/api/upload/可能返回不同结果。第二步设置 Body 类型为 binary。在 PostMan 的 Body 选项卡里有 none、form-data、x-www-form-urlencoded、raw、binary、GraphQL 几个选项。选binary然后点击 Select File 选择要上传的文件。这是最直接的方式PostMan 会把文件内容作为请求体原样发出。但如果你要发的不是文件而是自己拼的字节数据比如前面说的定长字段拼接结果PostMan 的 binary 模式就不够用了因为它只支持选文件。这时候有两个办法把拼好的字节写到临时文件再用 binary 模式选这个文件用 raw 模式但 Content-Type 手动改成application/octet-stream然后在 raw 里填内容。第二种方式有个陷阱raw 模式下的内容会被 PostMan 当作文本处理如果你填的是十六进制字符串它发出去的就是这些字符的 ASCII 码而不是你期望的字节。所以raw 模式只适合发纯文本内容的二进制流真正的字节数据还是走文件方式靠谱。第三步设置 Content-Type 请求头。在 Headers 选项卡里加一行Content-Type: application/octet-stream。虽然 PostMan 选 binary 模式时会自动加这个头但手动确认一下没坏处有些接口对 Header 大小写敏感。4.2 用 Pre-request Script 动态生成字节流实际测试中数据往往不是固定的需要根据变量动态生成。PostMan 的 Pre-request Script 可以帮上忙。比如要把一个 JSON 对象转成 UTF-8 字节流写入文件const data { orderId: 12345, amount: 99.5, remark: 测试订单 }; const jsonStr JSON.stringify(data); // PostMan 的脚本环境里可以用 Buffer基于 Node const buf Buffer.from(jsonStr, utf-8); // 把字节转成十六进制字符串方便后续处理 const hexStr buf.toString(hex); pm.environment.set(payloadHex, hexStr);但这里有个限制PostMan 的脚本无法直接把字节写入请求体binary 模式只认文件。所以动态生成字节流的实际做法通常是脚本生成数据后通过pm.sendRequest发到某个中转服务或者干脆用 NewmanPostMan 的命令行工具配合脚本文件来实现。如果只是测试固定格式的字节流更实用的做法是提前用代码生成好二进制文件然后在 PostMan 里选这个文件发送。生成文件的代码可以写成一个小工具每次改数据就重新生成一次。4.3 验证请求是否发对了发出去之后怎么知道对不对三个地方看PostMan 的 Console点左下角 Console 按钮能看到实际发送的请求头和请求体大小。如果 Body 大小是 0说明文件没选上或者路径错了。后端日志让后端同学打印收到的字节长度和前几个字节的十六进制值和你发送的对比。长度对不上说明传输过程中被截断或追加了内容。响应内容如果接口返回 200 但内容不对多半是编码或字段顺序问题如果返回 400先检查 Content-Type 和 Body 是否为空。我习惯在发送前用xxd命令Linux/Mac或十六进制编辑器看一眼文件内容确认开头几个字节符合预期。比如 PDF 文件开头应该是25 50 44 46即%PDF如果看到的是别的说明文件选错了。5. 常见问题与排查技巧实录5.1 乱码问题的三层排查法乱码是二进制流传输里最高频的问题排查要按层次来第一层确认发送端编码。用十六进制工具看实际发出的字节。如果中文变成了3F问号的 ASCII 码说明编码时就已经丢了信息问题在发送端。第二层确认传输过程无篡改。对比发送端和接收端拿到的字节长度如果长度不一致可能是中间有代理或网关做了转换。有些网关会把application/octet-stream当成文本处理强行转码。第三层确认接收端解码方式。发送端用 UTF-8接收端用 GBK 解必然乱码。这种情况需要双方对齐编码或者发送端在请求头里加charset信息虽然application/octet-stream标准上不带 charset但有些系统认这个。现象可能原因排查方法中文变问号发送端编码不支持中文检查getBytes()是否指定 UTF-8中文变方块接收端字体或解码问题用十六进制对比字节部分字段乱码字段边界计算错误检查定长拼接的字节数整体乱码编码方式不一致双方统一为 UTF-8末尾多出字符流未正确关闭或缓冲区问题检查流的 flush 和 close5.2 请求体为空或截断的处理PostMan 发 binary 请求时如果 Body 显示为空常见原因有文件路径包含中文或空格PostMan 读取失败。解决办法是把文件放到纯英文路径下。文件被其他程序占用比如 Excel 打开着 xlsx 文件。关掉再试。文件大小为 0选了个空文件。检查文件本身。PostMan 版本问题某些老版本对 binary 模式支持不好。升级到最新版。请求体截断则通常是服务端限制。Nginx 默认的client_max_body_size是 1MB超过就返回 413。Spring Boot 的spring.servlet.multipart.max-file-size默认也是 1MB。这些限制在传大文件时一定要提前确认。5.3 接口返回 400/415 的定位思路400 Bad Request通常意味着服务端解析请求体失败。可能原因Content-Type 不对、字节流格式不符合约定、必填字段缺失。排查时先看响应体里的错误信息很多框架会返回具体的解析失败原因。415 Unsupported Media Type则是 Content-Type 不被接受。比如接口只认application/octet-stream你发了text/plain就会 415。这种情况改 Header 即可。还有一种隐蔽的情况接口文档写的是application/octet-stream但实际代码里用的是RequestParam MultipartFile这种接口其实要的是multipart/form-data。文档和实现不一致的情况太常见了遇到 415 先和后端确认实际接收方式。提示联调时养成先问三件事的习惯——Content-Type 是什么、请求体格式是什么、有没有大小限制。这三个问题问清楚能省掉 80% 的返工。5.4 大文件传输的内存与超时问题传大文件时PostMan 本身会把整个文件读进内存几百 MB 的文件可能让 PostMan 卡死。这种情况建议用命令行工具如 curl代替curl -X POST http://example.com/api/upload \ -H Content-Type: application/octet-stream \ --data-binary /path/to/largefile.bin--data-binary和-d的区别在于前者原样发送文件内容后者会去掉换行符。传二进制必须用--data-binary。超时方面PostMan 默认的请求超时是 0不限制但服务端和网关可能有超时设置。大文件传输建议分片每片控制在几 MB既避免超时也方便断点续传。6. 从字节流到实际业务的落地经验6.1 一个典型的文件上传接口联调过程假设有个接口POST /api/v1/file/upload文档写着 Content-Type 是application/octet-stream请求体是文件内容文件名通过 HeaderX-File-Name传递。联调步骤准备一个测试文件比如test.txt内容写hello world。PostMan 新建请求Method 选 POSTURL 填接口地址。Headers 里加Content-Type: application/octet-stream和X-File-Name: test.txt。Body 选 binary选中test.txt。点 Send看响应。如果返回成功再换一个中文内容的文件测试编码。如果中文乱码检查后端读取时用的编码。这个流程看起来简单但实际联调时经常在 Header 名称大小写、文件名编码上卡住。比如X-File-Name里的中文文件名有些框架默认用 ISO-8859-1 解码需要额外配置。6.2 二进制流在分片上传中的应用大文件分片上传是二进制流的典型应用。思路是把文件按固定大小如 5MB切成 N 片每片作为独立的二进制流发送带上分片序号和总片数服务端收齐后合并。int chunkSize 5 * 1024 * 1024; byte[] fileBytes Files.readAllBytes(Paths.get(filePath)); int totalChunks (int) Math.ceil((double) fileBytes.length / chunkSize); for (int i 0; i totalChunks; i) { int start i * chunkSize; int end Math.min(start chunkSize, fileBytes.length); byte[] chunk Arrays.copyOfRange(fileBytes, start, end); // 发送 chunk带上 i 和 totalChunks }分片的好处是单次请求体积小、超时风险低、失败可重传。但要注意分片顺序和合并时的边界处理尤其是最后一片的大小计算Math.min那行不能省否则数组越界。6.3 用脚本批量生成测试数据手工造测试数据效率太低写个脚本批量生成更实际。比如要测试一个接收定长字节流的接口可以用 Python 生成一批测试文件import struct def build_payload(order_id, amount, remark): # 订单号 10 字节金额 8 字节 double备注 50 字节 order_bytes order_id.ljust(10).encode(utf-8) amount_bytes struct.pack(d, amount) remark_bytes remark.ljust(50).encode(utf-8)[:50] return order_bytes amount_bytes remark_bytes with open(test_payload.bin, wb) as f: f.write(build_payload(ORD12345, 99.5, 测试订单))struct.pack(d, amount)里的表示大端序d表示 double。字节序这个问题在跨平台传输时特别重要x86 是小端某些嵌入式设备是大端不约定清楚就会解析出完全错误的数值。生成好文件后在 PostMan 里选这个文件发送即可。如果要批量测试可以生成多个文件用 PostMan 的 Collection Runner 依次发送。6.4 接收端如何正确读取字节流发送端搞定了接收端也不能掉链子。以 Spring Boot 为例接收application/octet-stream的写法PostMapping(value /upload, consumes MediaType.APPLICATION_OCTET_STREAM_VALUE) public String upload(RequestBody byte[] body, RequestHeader(X-File-Name) String fileName) { // body 就是原始字节数组 String content new String(body, StandardCharsets.UTF_8); return received body.length bytes; }关键点是RequestBody byte[]配合consumes指定类型。如果写成RequestParam MultipartFile那就变成 multipart 协议了和 octet-stream 不兼容。读取时如果数据量大不要用byte[]一次性接收改用InputStreamPostMapping(value /upload, consumes MediaType.APPLICATION_OCTET_STREAM_VALUE) public String upload(InputStream inputStream) throws IOException { byte[] buffer new byte[8192]; int total 0; int len; while ((len inputStream.read(buffer)) ! -1) { total len; // 处理 buffer 中的 len 个字节 } return received total bytes; }这样内存占用恒定不会因为文件大而 OOM。7. 几个容易被忽略的细节7.1 换行符的坑文本文件在不同系统上的换行符不一样Windows 是\r\n0D 0ALinux 是\n0A。如果接口对字节内容敏感换行符差异会导致校验失败。测试时最好用十六进制编辑器确认文件内容别用记事本随便存。7.2 空字节与填充定长字段拼接时不足的部分通常补0x00或空格。补0x00的话接收端解析字符串时要注意截断否则会带出一串不可见字符。Java 里可以用new String(bytes, StandardCharsets.UTF_8).trim()去掉尾部空格但0x00用 trim 去不掉需要手动处理。7.3 PostMan 的环境变量与文件路径PostMan 的 binary 模式选文件时路径是写死在请求里的。如果团队协作别人拉取你的 Collection 后文件路径对不上请求就发不出去。解决办法是把测试文件放在项目仓库里用相对路径或者用 Newman 跑的时候通过命令行参数指定文件路径。7.4 编码声明的传递虽然application/octet-stream标准上不带 charset但实际项目中可以通过自定义 Header 传递编码信息比如X-Encoding: UTF-8。这样接收端就不用猜了。这个做法不是标准但双方约定好就行比事后排查乱码省事得多。8. 我个人的实操体会二进制流这东西第一次接触会觉得别扭因为看不到结构全是字节。但用多了会发现它其实是最诚实的传输方式——你发什么对方就收到什么没有任何隐藏的转换。JSON 看起来友好但序列化框架可能在背后做各种小动作比如日期格式转换、null 值处理反而容易出意外。我踩过最深的坑是编码问题。早期做接口联调本地用 Windows 开发测试通过部署到 Linux 服务器就乱码。查了半天才发现是getBytes()没指定编码Windows 默认 GBKLinux 默认 UTF-8。从那以后凡是涉及字符串转字节的地方我一律显式写StandardCharsets.UTF_8再也没出过这类问题。另一个体会是联调前把约定问清楚比事后排查省十倍时间。编码、字段顺序、长度前缀、大小限制这四个问题问明白了二进制流传输基本不会翻车。最怕的是双方都以为对方知道结果一个用 UTF-8 一个用 GBK来回折腾一整天。PostMan 发二进制流请求本身不难难的是理解背后的字节逻辑。把字符串、字节、编码、流这几个概念理清楚剩下的就是配置问题。配置错了看 Console 和日志对比字节长度和十六进制值大部分问题都能定位。真正需要经验的是那些文档没写、但实际必须遵守的隐性约定这些只能靠多问、多试、多记录来积累。