记录一次由于流的read(byte[3 * 1024], 0, len); 读取不足定义的长度,引发的线上bug

场景

需求流程:body中携带参数将参数、签名、文件流(base64)格式,通过Http请求第三方接口进行转义音频
请求参数如下:

{
    "sign":"BSKJFKGJKSLQK",
    "body":{
        "bt":"Base64字符"
    }
}

1.第一版代码(OOM)

1.1.流程概要

 - 从网络中获取流,将流转成base64字符
 - 将base64字符串与其它参数进行toString(),这时由于base64占用很大的堆内存,极易引发OOM
 - 通过http发起请求

1.2.概要代码

1.2.1.获取网络中流文件并转Base64字符串(内存占用极高)

/**
	 * @description: 文件转Base64字符串
	 * @param fileUrl
	 * @author: cc
	 * @date: 2021/5/13 15:52
	 */
	public String fileToString(String fileUrl) throws IOException {
		byte[] fileBytes = FileUtils.getNetWorkFileBytes(fileUrl);
		return LybUtil.byteToBase64(fileBytes);
	}


/**
     * @description: 获取网络中文件流
     * @param urlPath	文件路径
     * @author: cc
     * @date: 2021/1/12 17:12
     */
    public static byte[] getNetWorkFileBytes(String urlPath) throws IOException{
        URL url = new URL(urlPath);
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();
        //设置超时间为3秒
        conn.setConnectTimeout(3*1000);
        //防止屏蔽程序抓取而返回403错误
        conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
        //获取输入流
        InputStream inputStream = conn.getInputStream();
        //获取文件字节数组
        byte[] bytes = readInputStream(inputStream);
        return bytes;
    }

public static byte[] readInputStream(InputStream inputStream) throws IOException {
        byte[] buffer = new byte[1024];
        int len = 0;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while ((len = inputStream.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }
        bos.close();
        return bos.toByteArray();
    }
    
 /**
     * @description: 字节数组转base64
     * @author: cc
     * @date: 2021/3/1 11:39
     */
    public static String byteToBase64(byte[] b) {
        return Base64.getEncoder().encodeToString(b);
    }
    

1.2.2.封装参数

JSONObject param = new JSONObject();
        param.put("bt", 流的base64字符串);
        param.toJSONString();

HttpUtil.sendRequest(sttFileUrl, "POST", params, HttpUtil.ContentTypeEnum.CONTENT_JSON.getValue());

1.2.3.发起请求代码


public static String sendRequest(String requestUrl, String requestMethod, String parameters, String contentType) {
        return baseBody(requestUrl, requestMethod, parameters, contentType);
    }
 
 /**
     * @description: http请求,body参数格式
     * @param requestUrl    请求地址
     * @param requestMethod 请求类型
     * @param parameters    请求参数
     * @param contentType   请求格式
     * @return: java.lang.String
     * @author: cc
     * @date: 2021/6/23 15:06
     */
    public static String baseBody(String requestUrl, String requestMethod, String parameters, String contentType) {
        try {
            URL url = new URL(requestUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setUseCaches(false);
            // 设置请求方式(GET/POST)
            conn.setRequestMethod(requestMethod);
            conn.setRequestProperty("content-type", contentType);
            // 当outputStr不为null时向输出流写数据
            if (null != parameters) {
                OutputStream outputStream = conn.getOutputStream();
                // 注意编码格式
                outputStream.write(parameters.getBytes("UTF-8"));
                outputStream.close();
            }
            // 从输入流读取返回内容
            InputStream inputStream = conn.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String str = null;
            StringBuffer buffer = new StringBuffer();
            while ((str = bufferedReader.readLine()) != null) {
                buffer.append(str);
            }
            // 释放资源
            bufferedReader.close();
            inputStreamReader.close();
            inputStream.close();
            conn.disconnect();
            return buffer.toString();
        } catch (ConnectException ce) {
            System.out.println("连接超时:{}" + ce);
        } catch (Exception e) {
            System.out.println("https请求异常:{}" + e);
        }
        return null;
    }

以上代码由于会将base64转字符串存到堆内存中,故内存占用极高,会引发OOM异常。
那有没有一种方法,我把base64字符串,通过流的方式一点一点的发过去呢,这样就解决了内存占用问题。答案是有的,接着往下看:


2.第二版代码(base64字符串问题)

基于上面的问题,和同事讨论之后,给出了一个方案,就是通过流一点点传输

2.1 概要代码

// 当outputStr不为null时向输出流写数据
            if (!EmptyTool.isNull(lybSttFileDto)) {
                LybSttFileDto.LybSttFileBody body = lybSttFileDto.getBody();
                OutputStream outputStream = conn.getOutputStream();
                outputStream.write("{ \"ver\":\"1.0\", \"sysVer\":\"1.0.0\", \"sys\":\"andriod\", \"body\":{  \"appKey\":\"com.fsk.sttfile\"".getBytes());
	            outputStream.write((",\"bt\":\"").getBytes());

               // 此方法由于实际读取的字节不足3字节,会导致base64时补充=符号,之后会引发录音文件偏大或偏小,无法播放情况
               byte[] buffer = new byte[3 * 1024];
                int len = 0;
                while ((len = lyInputStream.read(buffer)) != -1) {
                    outputStream.write(base64(Arrays.copyOfRange(buffer, 0, len)).getBytes());
                    outputStream.flush();
                }

                outputStream.write(("\"").getBytes());
   			    outputStream.write("}}".getBytes());
                outputStream.flush();
                outputStream.close();
            }

理想情况下,每次读取3kb的数据,之后进行base64,这样就是完整的base64字符串了。

但是由于 read(byte[], 0, len); 方法,实际读取的字节是不足3kb的,那么在进行base64时就会补位操作,字符串后面为追加 == 符号。进而导致第三方将base64转文件时,出现文件无法播放问题。

参考:
在这里插入图片描述
在这里插入图片描述

3.第三版代码(定位问题、每次只读1kb)

由于第二版代码read()字节时,不足3*1024字节,会导致base64编码问题。
思考之后得出结果:我每次读取一个字节,当读取的字节数等于3kb时,我就进行一次base64,这样就可以解决上述的问题了。

if (!EmptyTool.isNull(lybSttFileDto)) {
                LybSttFileDto.LybSttFileBody body = lybSttFileDto.getBody();
                OutputStream outputStream = conn.getOutputStream();
                outputStream.write("{ \"ver\":\"1.0\", \"sysVer\":\"1.0.0\", \"sys\":\"andriod\", \"body\":{  \"appKey\":\"com.fsk.sttfile\"".getBytes());
   	        outputStream.write((",\"bt\":\"").getBytes());

               /*
                * 一次读取一个字节,解决上述问题
                */
                int size = 3 * 1024;
                int b;
                byte[] buffer = new byte[size];
                int i = 0;
                while ((b = lyInputStream.read()) != -1) {
                    buffer[i] = (byte) b;
                    i++;
                    if (i % size == 0) {
                        outputStream.write(base64(Arrays.copyOfRange(buffer, 0, size)).getBytes());
                        outputStream.flush();
                        i = 0;
                    }
                } // while

                if (i != 0) {
                    String tempStr = Base64.getEncoder().encodeToString(Arrays.copyOfRange(buffer, 0, i));
                    outputStream.write(tempStr.getBytes());
                    outputStream.flush();
                }

                outputStream.write(("\"").getBytes());
                outputStream.write("}}".getBytes());
                outputStream.flush();
                outputStream.close();
            }

通过上述代码就解决了base64转码问题以及OOM问题。
这个问题个人觉得很有意思,故拿出来分享,欢迎大家讨论


最后附上一个接收base64字符串进行转文件的代码示例,其中有一个意思的是,字符串String类型可以用byte[]数组接收

    
@RequestMapping("/send")
    public BaseResponse<String> send(@RequestBody JSONObject jsonObject) throws IOException {
        JSONObject object = JSON.parseObject(jsonObject.toJSONString());
        JSONObject body = JSON.parseObject(object.get("bt").toString());
        LybSttFileBody lybSttFileBody = JSON.toJavaObject(body, LybSttFileBody.class);

        FileOutputStream fos = new FileOutputStream("D:\\file\\文件哈哈.txt");
        fos.write(lybSttFileBody.getBt());
        fos.flush();
        fos.close();
        return new BaseResponse<>();
    }

@Getter
@Setter
public class LybSttFileBody {
        @ApiModelProperty(value = "需识别的音频流 需要流做base64编码处理")
        private byte[] bt;
}

JSONObject param = new JSONObject();
 param.put("bt", "流的base64字符串");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值