Multipart/form-data 的流式数据解析 - InputStream 用法笔记

最近在做项目时碰到一个用到 InputStream 的场景,在这里做个记录。场景是做了一个 Http2 请求之后,返回内容是chunked传输(分帧返回),Multipart/form-data(数据中包含了协议规定的一些占位符等) 。用流的原因是,服务端的内容是分段一段一段生成返回的,用流的方式能够读取一段数据后就对该段数据进行处理,并在界面呈现,最快的方式显示先回来的数据,体验更好。下面是响应内容的一个示例。

--ABCD 
Content-Disposition: form-data; name="1.txt"; filename="C:\1.txt" 
Content-Type: application/json
Content-Length: 147  // 如果这段数据是 json 则会有长度描述
\r\n
// 一段json数据
--ABCD
Content-Disposition: form-data; name="title" 
Content-Type:application/octet-stream
\r\n 
// 一段音频数据 二进制格式
--ABCD-- //结束的占位符
\r\n

可以看到,协议规定的数据格式是这样:首先是一个--占位符,占位符之后一个\r\n换行紧跟描述符,各描述符之间通过换行隔开,描述符以两个\r\n 换行结束,紧跟着是真实数据,数据以一个\r\n 换行结束,然后紧跟着是下一段数据。所有数据结束之后会有一个结束的占位符作为结束标志。

我要做的,是根据协议规定的特殊字符,从流当中解析出每一段完整的数据,下面直接贴解析数据的代码:

import java.io.IOException;
import java.io.InputStream;

public class Test { // 仅包含必须的数据

    // 当前循环执行的动作类型
    enum ActionType{
        TO_FIND_BOUNDARY_START, // 要找其实占位符 "--zhanweifu"
        TO_FIND_BOUNDARY_DESC_END, // 要找描述头的计数标志 "\r\n\r\n"
        TO_FIND_CONTENT_END // 要找数据的结束标志 "\r\n"
    }

    // 当前取到的数据类型
    enum DataType {
        JSON_TYPE,
        AUDIO_TYPE,
        ZERO_TYPE
    }

    public void parse(InputStream inputStream) throws IOException {
        // 读取出来的数据存储两个对象
        // 一个是StringBuilder对象,主要用 index 函数来查找 关键的特征字符串,如起始zhanweifu, \r\n, \r\n\r\n 等
        // 一个是unprocessedArr,保存所有读到de buffer
        byte[] unprocessedArr = new byte[22000];
        StringBuilder unprocessedSB = new StringBuilder();

        int unprocessedCount = 0; // 记录当前未做处理的 索引值
        int totalReadCount = 0; // 记录当前总读出的字节数

        // 初始化actionType, dataType, 和 循环跳出条件
        ActionType actionType = ActionType.TO_FIND_BOUNDARY_START;
        DataType dataType = DataType.ZERO_TYPE;
        boolean notTerminated = true;

        byte[] buffer = null;
        int availableCount = 0;
        int readCount = 0;

        while(notTerminated){
            // 读取新的buffer出来,并存入到 unprocessedSB 和 unprocessedArr
            availableCount = inputStream.available();
            buffer = new byte[availableCount];
            readCount = inputStream.read(buffer, 0, availableCount);
            if (readCount == -1){
                notTerminated = false;
                // break 读到EOF,结束
            }
            if (readCount == 0){
                continue;
            }
            String bufferStr = new String(buffer,"ascii");
            unprocessedSB = unprocessedSB.append(bufferStr);
            System.arraycopy(buffer, 0, unprocessedArr, totalReadCount, readCount);
            totalReadCount += readCount;

            // 判断是否已取到结束帧
            if (unprocessedSB.indexOf("--zhanweifu--") != -1) {
                notTerminated = false;
            }

            // 解析数据
            // 在对应的 actionType 下, 查找对应的目标字符串
            // 去到一帧完整的json或audio type的数据之后就做下一步处理
            switch (actionType){
                case TO_FIND_BOUNDARY_START: {
                    int findCount = unprocessedSB.indexOf("--zhanweifu", unprocessedCount);
                    if (findCount != -1) {
                        actionType = ActionType.TO_FIND_BOUNDARY_DESC_END;
                        unprocessedCount = findCount + "--zhanweifu\r\n".length();
                    }
                    break;
                }
                case TO_FIND_BOUNDARY_DESC_END: {
                    int findCount = unprocessedSB.indexOf("\r\n\r\n", unprocessedCount);
                    if (findCount != -1) {
                        // 取出一帧数据的描述头
                        String descHeadStr = unprocessedSB.substring(unprocessedCount, findCount);
                        // 更新 dataType
                        dataType = getDataType(descHeadStr);
                        // dataLength = getDataLength(descHeadStr);
                        actionType = ActionType.TO_FIND_CONTENT_END;
                        unprocessedCount = findCount + "\r\n\r\n".length();
                    }
                    break;
                }
                case TO_FIND_CONTENT_END: {
                    int findCount = unprocessedSB.indexOf("\r\n", unprocessedCount);
                    if (findCount != -1) {
                        switch (dataType){
                            case JSON_TYPE: {
                                // 至此,取到了一帧完整的json字符串
                                String jsonContentStr = unprocessedSB.substring(unprocessedCount, findCount);
                                actionType = ActionType.TO_FIND_BOUNDARY_START;
                                unprocessedCount = findCount + "\r\n".length();
                                break;
                            }
                            case AUDIO_TYPE:{
                                if (unprocessedSB.indexOf("--zhanweifu", findCount) != -1) {
                                    // 至此,取到了一帧完整的二进制音频数据
                                    byte[] audioArr = new byte[findCount - unprocessedCount];
                                    System.arraycopy(unprocessedArr, unprocessedCount, audioArr, 0, findCount-unprocessedCount);
                                    actionType = ActionType.TO_FIND_BOUNDARY_START;
                                }
                                unprocessedCount = findCount + "\r\n".length();
                                break;
                            }
                        }

                    }
                    break;
                }
                default:
                    break;
            }
        }
        inputStream.close();
    }

    private DataType getDataType(String descHeadStr){
        return DataType.JSON_TYPE; // 先假设本段是json,实际Type 从 descHead里做字符串解析
    }
}

处理这种多类型返回的响应数据,关键的是下面两点:

1\ 现在java并没有针对byte数组提供如String类型的index方法,无法直接从 {1, 2,3,4,5,55,55,55,55, 99} 中直接找出{4, 5, 55}这样一个子数组的索引,所以还是需要一个String类型的存储结构,来帮助找占位符之类的字符串。由于响应中包含字符串的数据帧,音频流或是其它二进制数据帧,json字符串中可能包含中文,所以要显式的用 ascii 编码来转化 buffer,然后append 入StringBuilder。这么做的目的是让StringBuilder对象中的字符串和unprocessedArr 的字节一一对应。这样用StringBuilder 所引出来的数据的索引位置就可以直接用来从unprocessedArr中取数据。

2\ 每段内容结束之后会有一个\r\n,然后再开始下一段数据。所以就可以用\r\n 来作为一段数据结束的标志。但如果一帧数据是音频字节数据,就不能单纯的用\r\n来判断。这是因为,一段音频数据中,出现\r\n的概率还是挺大的,会造成误判。所以在找到\r\n 之后要判断紧跟着的数据是不是占位符来确定内容是否真的结束。当然json数据或其他文本类数据也可能会有\r\n的出现,为了规避这种情况,则可以根据描述符中得到的长度来判断数据是否取完整。

上面一版是优化后的代码,优化之前我是用String unprocessedStr = new String(unprocessedArr, "ascii") 初始化用来索引的字符串。代码用来做并发测试的话,并发量会有3-5倍的影响。

做完记录,在此立个flag,以后勤奋的两周更新一次博客。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
multipart/form-data是一种常用的用于在HTTP协议中传输二进制数据的编码格式。它通常用于文件上传功能。 在HTTP中,请求和响应的数据被划分为多个部分,每个部分由特定的分隔符进行划分。每个数据部分中包含一个头部和一个内容体,头部用来描述这个数据部分的一些信息,例如数据类型、数据长度等。内容体就是实际的数据内容。 在multipart/form-data中,数据部分的内容体可以是任意类型的数据,例如文本、图片、音频等。当上传文件时,可以将文件数据直接放到内容体中进行传输。每个数据部分的头部中会指定Content-Disposition字段,用来告诉服务器接收到的数据应该如何处理,例如存储文件的路径和文件名。 使用multipart/form-data编码格式可以确保文件和其他数据正确地传输到服务器端。而不会出现数据被截断或改变的问题。同时multipart/form-data也支持多个文件上传的功能,可以一次性上传多个文件。 对于服务器端来说,接收multipart/form-data格式的数据相对比较复杂,需要进行数据解析。常见的处理multipart/form-data的方式是通过使用特定的库或框架,例如PHP中的$_FILES和Python中的Flask框架中的request.files来处理文件上传。 总结来说,multipart/form-data是一种专门用于传输二进制数据的编码格式,特别适用于文件上传功能。它能够确保数据正确地传输到服务器端,同时支持多个文件上传。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值