Bootstrap

HttpClient报错cn.hutool.http.HttpException: Read timed out

这个错误明面上说是请求等待超时,但是其实真正的问题并不一定是等待超时的问题

错误出现

这个错误出现是一个项目更新以后的出现的这个问题,错误的代码如下:

cn.hutool.http.HttpException: Read timed out
	at cn.hutool.http.HttpResponse.init(HttpResponse.java:511)
	at cn.hutool.http.HttpResponse.initWithDisconnect(HttpResponse.java:484)
	at cn.hutool.http.HttpResponse.<init>(HttpResponse.java:81)
	at cn.hutool.http.HttpRequest.doExecute(HttpRequest.java:1130)
	at cn.hutool.http.HttpRequest.execute(HttpRequest.java:1012)
	at cn.hutool.http.HttpRequest.execute(HttpRequest.java:988)
	at cn.zc.sport.medical.http.HuHttpUtil.httpPost(HuHttpUtil.java:41)
	at cn.zc.sport.medical.task.RequestSending.core(RequestSending.java:138)
	at cn.zc.sport.medical.task.RequestSending.sending(RequestSending.java:77)
	at sun.reflect.GeneratedMethodAccessor35.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
	at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
	at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
	at java.util.concurrent.FutureTask.runAndReset(Unknown Source)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(Unknown Source)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(Unknown Source)
	at java.net.SocketInputStream.read(Unknown Source)
	at java.net.SocketInputStream.read(Unknown Source)
	at sun.security.ssl.SSLSocketInputRecord.read(Unknown Source)
	at sun.security.ssl.SSLSocketInputRecord.readHeader(Unknown Source)
	at sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(Unknown Source)
	at sun.security.ssl.SSLSocketImpl.readApplicationRecord(Unknown Source)
	at sun.security.ssl.SSLSocketImpl.access$300(Unknown Source)
	at sun.security.ssl.SSLSocketImpl$AppInputStream.read(Unknown Source)
	at java.io.BufferedInputStream.fill(Unknown Source)
	at java.io.BufferedInputStream.read1(Unknown Source)
	at java.io.BufferedInputStream.read(Unknown Source)
	at sun.net.www.http.HttpClient.parseHTTPHeader(Unknown Source)
	at sun.net.www.http.HttpClient.parseHTTP(Unknown Source)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(Unknown Source)
	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(Unknown Source)
	at java.net.HttpURLConnection.getResponseCode(Unknown Source)
	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getResponseCode(Unknown Source)
	at cn.hutool.http.HttpConnection.responseCode(HttpConnection.java:470)
	at cn.hutool.http.HttpResponse.init(HttpResponse.java:508)
	... 20 common frames omitted

这里解释一下,因为项目使用了hutool这个工具包,并且http请求也是用的hutool包中的HttpRequest,但是其实hutool中的HttpRequest是对httpClient做了封装本质上也是HttpClient报的错

接下来说一下原因:需求是这样的,我们需要做一个服务将这个电脑上的数据和文件发送给服务器,因此发送采用了hotool工具包HttpRequest,一开始项目是没有问题的,但是后面服务器项目更新了之后就出现了这个问题

分析的过程

最早的分析是:

String result = HttpRequest.post(url)
                .header("contentType", "multipart/form-data")
                .form(paramFile)
                .timeout(20000) // 请求等待时间
                .execute().body();

我们认为这个请求超时的时间设置的太短了,因此引发了这个故障,于是我们把原来的20000 设置成了 60000 也就是 一分钟,部署之后服务又开始正常运行了,可是没有几天他又出现了这个问题,因为出现了一个数据量很大的文件,这个文件的处理超过了一分钟,他又报错了,紧接着我设置了2分钟,但是这次的代码审核没有通过,原因是,可能会存在更大的文件。并且在审核的时候拿了一个很大的文件,确实Read Time out又出现了。

紧接着研究了很长时间,最后项目经理给我了思路,他说为什么会这么长的时间,这些时间到底做了什么?

然后,我通过日志分析了整个这个请求的所有的流程,终于发现了问题出在哪里,因为这个请求是发送给另一个项目的接口,一两句话说不清楚,下面用一个简单的图描述一下这个流程:

请求的流程图
其实当时分析完这个之后,我想到了使用异步MQ的方式,但是这个提议被否决了,原因是需要搭建MQ服务,成本太大。

最终解决方案

最后分析的解决的方式是这样的:

其实在数据服务器接收到文件的时候,上一个数据推送服务的工作已经结束了,他就没必要等了,所以这里采用了异步调用的方式,来执行,后面的文件的处理

这里写一下异步的实现方式:

首先写一个接口类:

/**
 * 异步处理DCM文件
 *
 * @author An
 * @date 2022/11/3 17:59
 */
public interface AsyncDcm {
    @Async
    void DCMDetailWith(MultipartFile multipartFiles, String decrypt, String study_uid);
}

写class实现这个接口:

/**
 * 异步处理DCM文件实现类
 *
 * @author An
 * @date 2022/11/3 18:01
 */
@Log
@Service
public class AsyncDcmImpl implements AsyncDcm {
    @Autowired
    private Zip4jUtil zip4jUtil;

    @Autowired
    private FileExists fileExists;

    @Value("${file.zipPath}")
    private String zipPath;

    @Value("${file.filePath}")
    private String filePath;

    @Value("${file.jpgPath}")
    private String jpgPath;

    @Autowired
    private OrthancPacsRestTemplate pacsRestTemplate;


    @Override
    public void DCMDetailWith(MultipartFile multipartFiles, String decrypt, String study_uid) {
        // 图片影像dcm文件处理
        if(multipartFiles != null) {
            // 创建缓存目录
            fileExists.directory(zipPath);
            fileExists.directory(filePath);
            fileExists.directory(jpgPath);
            // 清理缓存
            //zip4jUtil.emptyFile(zipPath);
            zip4jUtil.emptyFile(filePath);
            //zip4jUtil.emptyFile(jpgPath);
            // 接收压缩包 文件缓存到本地

            log.info("影像数据文件 ->" + decrypt + " " + multipartFiles.getOriginalFilename());

            File zipFile = new File(zipPath + multipartFiles.getOriginalFilename());
            try {
                //读取zipFile文件
                multipartFiles.transferTo(zipFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 解压
            zip4jUtil.unzip(zipPath + zipFile.getName(), filePath + study_uid);
            // 解压后的dcm文件
            List<File> fileList = fileExists.search(filePath + study_uid + "/", new ArrayList<>());
            //上传图片到PACS
            for (File JpgFile : fileList) {
                pacsRestTemplate.sendInstances(JpgFile);
                log.info("发送DICOM图像至Orthanc ->" + JpgFile.getName());
            }
        }
    }
}

然后在controller层中调用异步方式处理文件:
注意:在controller上添加 @EnableAsync 注解

package cn.stylefeng.guns.modular.api.controller;

import cn.stylefeng.guns.entity.Study;
import cn.stylefeng.guns.modular.api.async.AsyncDcm;
import cn.stylefeng.guns.modular.utils.AESUtil;
import cn.stylefeng.guns.modular.utils.FileExists;
import cn.stylefeng.guns.modular.utils.Zip4jUtil;
import cn.stylefeng.guns.sys.modular.system.entity.User;
import cn.stylefeng.guns.sys.modular.system.service.UserService;
import cn.stylefeng.guns.util.OrthancPacsRestTemplate;
import cn.stylefeng.roses.core.util.ToolUtil;
import cn.stylefeng.roses.kernel.model.response.ResponseData;
import lombok.extern.java.Log;

import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zc.workflow.form.handle.service.HandleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.*;

@Controller
@Log
@EnableAsync
@RequestMapping("/api")
public class HandleEquipController {

    @Autowired
    private HandleService handleService;

    @Autowired
    private UserService userService;

    @Autowired
    private AESUtil aesUtil;

    @Autowired
    private Zip4jUtil zip4jUtil;

    @Autowired
    private FileExists fileExists;

    @Value("${file.zipPath}")
    private String zipPath;

    @Value("${file.filePath}")
    private String filePath;

    @Value("${file.jpgPath}")
    private String jpgPath;

    @Autowired
    private AsyncDcm asyncDcm;


    /**
     * 接收影像设备检查结果:修改表单中图片影像、影像结果
     * @param enData 密文
     * @param multipartFiles  dcm文件压缩包
     * @return
     */
    @RequestMapping(method = RequestMethod.POST, path="/acceptEquipmentResult")
    @ResponseBody
    public ResponseData acceptEquipmentResult(@RequestParam("enData") String enData,
                                              @RequestParam(value = "files", required = false) MultipartFile multipartFiles) {
    	
    	log.info("影像接收到密文 -> " + enData);
    	
        String allOrTaskForm = "allForm";
        String msg = "yes";
        // 解密
        String decrypt = aesUtil.decrypt(enData);
        
        log.info("影像数据解密 ->" + decrypt);
        
        // 解析为json数组
        JSONArray jsonArray = JSONArray.parseArray(decrypt);
        // 装入Study List集合
        List<Study> addList = jsonArray.toJavaList(Study.class);
        // 流程任务单据编号
        String processTaskCode = "";
        // 临床诊断
        String clinicalDiagnosis = "";
        // 用户uid (用于文件存储操作)
        String study_uid = "";
        // 操作用户
        String operateName = null;
        // 遍历集合 拿出数据
        for (Study study : addList) {
            processTaskCode = study.getPatient_id();
            clinicalDiagnosis = study.getFinding();
            if (study.getReporter() != null) {
                operateName = study.getReporter();
            }
            study_uid = study.getStudy_uid();
        }
        // 异步处理接收DCM文件
        asyncDcm.DCMDetailWith(multipartFiles, decrypt, study_uid);
        return ResponseData.success(result);
    }
}

最后在启动类上添加@EnableAsync 注解

测试

2022-07-22 15:29:52.121  INFO 4388 --- [scheduling-1] cn.zc.sport.medical.http.HuHttpUtil      : >>>>>>>>>> 发送完成,耗时:2822 ms <<<<<<<<<<

完美解决

总结

出现这个问题很多时候需要分析发出去请求之后等待的时间服务到底做了什么,如果确实都没发过去,那再调整超时时间,不然可以使用异步的方式缩短处理时间,还不行,推荐使用MQ异步消息队列。

;