• Java实现HTTP的上传与下载


    相信很多人对于java文件下载的过程都存在一些疑惑,比如下载上传文件会不会占用vm内存,上传/下载大文件会不会导致oom。下面从字节流的角度看下载/上传的实现,可以更加深入理解文件的上传和下载功能。

    文件下载

    首先明确,文件下载不仅仅只有下载方,还有服务端也就是返回文件的服务器
    那么看一个简易文件服务器返回下载的文件。

    服务端

    这里是使用springMvc实现

        @GetMapping("download")
        public void downFile(HttpServletResponse response) throws IOException {
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment; filename=" + "test.jhprof");
            File file = new File("D:\\heap\\heapDump.hprof");
            InputStream in = new FileInputStream(file);
            OutputStream out = response.getOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = in.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }
            in.close();
            out.close();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里每次从文件流中读取1024个字节输出,是因为如果读取太多字节会给内存造成压力,我们这里使用的是java的堆内存。如果直接完整读取整个文件,那么可以导致oom。

    java.lang.OutOfMemoryError: Java heap space
    
    • 1

    客户端

            URL url = new URL("http://localhost:8062/fallback/download");
            URLConnection conn = url.openConnection();
            InputStream in = conn.getInputStream();
            FileOutputStream fileOutputStream = new FileOutputStream("D:\\tmp\\a.hrpof");
    
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = in.read(buffer)) > 0) {
                fileOutputStream.write(buffer, 0, len);
            }
    
            in.close();
            fileOutputStream.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    同样,在下载文件时也需要注意,不要一次读取特别多的字节数
    测试过程发现由于TCP发送缓冲区和接受缓冲区有限,当缓冲区满之后就会阻塞,例如下载方的速度满,服务端的文件不断写到缓冲区,缓冲区满了,就无法继续写入,那么就会导致在执行write方法时暂时阻塞。等到接收端接受到数据了,才能继续写入。

    文件上传

    服务端

    http是支持多个文件进行上传的,文件数据都在请求体中,多个文件之间可以通过分隔符区分
    例如上传两个文本文件
    请求大概长这样

    1.HTTP上传method=post,enctype=multipart/form-data;
    2.计算出所有上传文件的总的字节数作为Content-Length的值
    3.设置
    Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryJ9RUA0QCk13RaoAp
    4.多个文件数据:请求体
    ------WebKitFormBoundaryJ9RUA0QCk13RaoAp
    Content-Disposition: form-data; name="pic"; filename="thunder.gif"
    Content-Type: image/gif
    这中间是文件的二进制数据
     
    ------WebKitFormBoundaryJ9RUA0QCk13RaoAp
    Content-Disposition: form-data; name="pic"; filename="uuu.gif"
    Content-Type: image/gif
    这中间是文件的二进制数据
    ------WebKitFormBoundaryJ9RUA0QCk13RaoAp--
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    服务端使用springMvc接收上传文件的写法(多个文件)

     @RequestMapping(
                method = RequestMethod.POST,
                value = "/uploadModel"
        )
        public void uploadModel(@RequestPart(value = "file", required = true) List<MultipartFile> file, @RequestParam Integer type) {
           ··········
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样就可以直接获取到上传的文件。
    具体接受过程还要看springMvc的实现
    在doDispatch方法中会是否按照文件进行处理
    判断方式也很简单检查请求头的multipart

    @Override
    	public boolean isMultipart(HttpServletRequest request) {
    		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
    	}
    
    • 1
    • 2
    • 3
    • 4

    如果是文件类型,那么就要通过IO流将文件下载到本地
    springMvc的大致实现如下
    原代码位置org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

               FileItemIterator iter = getItemIterator(ctx);
                FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(), "No FileItemFactory has been set.");
                final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];//8KB的字节数组用于读取字节流
                while (iter.hasNext()) {
                    final FileItemStream item = iter.next();
                    // Don't use getName() here to prevent an InvalidFileNameException.
                    final String fileName = ((FileItemStreamImpl) item).getName();
                    FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
                                                       item.isFormField(), fileName);
                    items.add(fileItem);
                    try {
                        Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
                    } catch (FileUploadIOException e) {
                        throw (FileUploadException) e.getCause();
                    } catch (IOException e) {
                        throw new IOFileUploadException(String.format("Processing of %s request failed. %s",
                                                               MULTIPART_FORM_DATA, e.getMessage()), e);
                    }
                    final FileItemHeaders fih = item.getHeaders();
                    fileItem.setHeaders(fih);
                }
                successful = true;
                return items;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    根据分隔符找出上传的多个文件进行读取字节流,同时创建本地文件,写入到本地文件,这里循环通过8kb数组读取到内存,再写到文件,是为了防止文件过大造成占用内存大。

    public static long copy(InputStream inputStream,
                OutputStream outputStream, boolean closeOutputStream,
                byte[] buffer)
        throws IOException {
            OutputStream out = outputStream;
            InputStream in = inputStream;
            try {
                long total = 0;
                for (;;) {
                    int res = in.read(buffer);
                    if (res == -1) {
                        break;
                    }
                    if (res > 0) {
                        total += res;
                        if (out != null) {
                            out.write(buffer, 0, res);
                        }
                    }
                }
                if (out != null) {
                    if (closeOutputStream) {
                        out.close();
                    } else {
                        out.flush();
                    }
                    out = null;
                }
                in.close();
                in = null;
                return total;
            } finally {
                IOUtils.closeQuietly(in);
                if (closeOutputStream) {
                    IOUtils.closeQuietly(out);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    从这里我们也能看出来,http请求不并不是tomcat服务器接收进完全接收的,而是先接收请求头进行就开始进行处理了,至于后面要不要读取请求提,如何读取,就要看程序员的代码了,这也是程序员可以控制的。

    客户端

    文件上传的客户端逻辑比较复杂

    package javaio;
    
    import java.io.BufferedReader;
    import java.io.DataOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @author liuxishan 2023/9/3
     */
    
    public class FileUpload  {
    
        public static void main(String[] args) {
            String reslut = null;
            Map<String, File> files = new HashMap() {
                {
                    put("0.png", new File("C:\\Users\\lxs\\Desktop\\0.png"));
                    put("1.jpg", new File("C:\\Users\\lxs\\Desktop\\1.png"));
                    put("big.herof", new File("D:\\heap\\heapDump.hprof"));
                }
            };
            try {
                String BOUNDARY = java.util.UUID.randomUUID().toString();
                String PREFIX = "--", LINEND = "\r\n";
                String MULTIPART_FROM_DATA = "multipart/form-data";
                String CHARSET = "UTF-8";
                URL uri = new URL("http://localhost:8062/fallback/uploadModel?type=1");
                HttpURLConnection conn = (HttpURLConnection) uri.openConnection();
             //   conn.setChunkedStreamingMode(0);
                conn.setReadTimeout(100 * 1000);
                conn.setDoInput(true);// 允许输入
                conn.setDoOutput(true);// 允许输出
                conn.setUseCaches(false);
                conn.setRequestMethod("POST"); // Post方式
                conn.setRequestProperty("connection", "keep-alive");
                conn.setRequestProperty("Charsert", "UTF-8");
                conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA  + ";boundary=" + BOUNDARY);
                conn.connect();
                // 首先组拼文本类型的参数
                StringBuilder sb = new StringBuilder();
                OutputStream outStream = conn.getOutputStream();
                outStream.flush();
                // 发送文件数据
                if (files != null)
    //		         for (Map.Entry file : files.entrySet()) {
                    for (String key : files.keySet()) {
                        StringBuilder sb1 = new StringBuilder();
                        sb1.append(PREFIX);
                        sb1.append(BOUNDARY);
                        sb1.append(LINEND);
                        sb1.append("Content-Disposition: form-data; name=\"file\"; filename=\""   + key + "\"" + LINEND);
                        sb1.append("Content-Type: multipart/form-data; charset="  + CHARSET + LINEND);
                        sb1.append(LINEND);
                        outStream.write(sb1.toString().getBytes());
                        File valuefile = files.get(key);
                        InputStream is = new FileInputStream(valuefile);
                        byte[] buffer = new byte[1024];
                        int len = 0;
                        while ((len = is.read(buffer)) != -1) {
                            outStream.write(buffer, 0, len);
                        }
                        is.close();
                        outStream.write(LINEND.getBytes());
                    }
                // 请求结束标志
                byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes();
                outStream.write(end_data);
                outStream.flush();
                // 得到响应码
    //		     success = conn.getResponseCode()==200;
                InputStream in = conn.getInputStream();
                InputStreamReader isReader = new InputStreamReader(in);
                BufferedReader bufReader = new BufferedReader(isReader);
                String line = null;
                reslut = "";
                while ((line = bufReader.readLine()) != null)
                    reslut += line;
                outStream.close();
                conn.disconnect();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93

    这里需要注意的是在使用HttpURLConnection 上传大文件时,出现内存溢出的错误,这让我产生了错觉,输入和输出流咋会暂用内存,不就是一个数据传送的管道么,都没有把数据读取到内存中,为撒会报错。。。然后就纠结了。。。
    不过实在与原来的经验相违背,然后写了一个示例直接从file中读出然后写入到输出流中,发现并没有问题
    查HttpURLConnection api发现其有缓存机制,数据并没有实时发送到网络,而是先缓存再发送,导致内存溢出。
    解决办法:
    httpConnection.setChunkedStreamingMode(0);
    //不使用HttpURLConnection的缓存机制,直接将流提交到服务器上。
    需要注意的是我们经常使用的hutool的http也存在这个问题
    如果不指定chunkedStreamingMode也会出现oom的问题
    对于大文件可以这么进行指定。

      HttpResponse response = HttpUtil.createPost("http://localhost:8062/fallback/uploadModel?type=1")
                    .setChunkedStreamingMode(0)
                    .form("file", new File("D:\\heap\\heapDump.hprof"))
                    .execute();
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    2022牛客蔚来杯加赛
    导师问我打开句柄fd和没有打开的差异在哪里?
    Keep-Alive中通过component多次加载同样的动态组件无法保持状态的解决办法
    12v24v60v高校同步降压转换芯片推荐
    SpringBoot 还在用 if 校验参数?那你真的太low了,老司机都是这么玩的
    docker部署前端项目(二)遇到的问题
    有关多线程环境下的Volatile、lock、Interlocked和Synchronized们
    docker camunda 8.5 部署步骤
    结构化日志记录增强网络安全性
    (vue的入门
  • 原文地址:https://blog.csdn.net/qq_37436172/article/details/132652010