最近在做项目时碰到一个用到 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,以后勤奋的两周更新一次博客。