Java实现HTTP的上传与下载

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

文件下载

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

服务端

这里是使用springMvc实现

java 复制代码
    @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();
    }

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

java 复制代码
java.lang.OutOfMemoryError: Java heap space

客户端

java 复制代码
        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();

同样,在下载文件时也需要注意,不要一次读取特别多的字节数

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

文件上传

服务端

http是支持多个文件进行上传的,文件数据都在请求体中,多个文件之间可以通过分隔符区分

例如上传两个文本文件

请求大概长这样

java 复制代码
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--

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

java 复制代码
 @RequestMapping(
            method = RequestMethod.POST,
            value = "/uploadModel"
    )
    public void uploadModel(@RequestPart(value = "file", required = true) List<MultipartFile> file, @RequestParam Integer type) {
       ··········
    }

这样就可以直接获取到上传的文件。

具体接受过程还要看springMvc的实现

在doDispatch方法中会是否按照文件进行处理

判断方式也很简单检查请求头的multipart

java 复制代码
@Override
	public boolean isMultipart(HttpServletRequest request) {
		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
	}

如果是文件类型,那么就要通过IO流将文件下载到本地

springMvc的大致实现如下

原代码位置org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

java 复制代码
           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;

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

java 复制代码
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);
            }
        }
    }

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

客户端

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

java 复制代码
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<String, File> 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();
        }
    }

}

这里需要注意的是在使用HttpURLConnection 上传大文件时,出现内存溢出的错误,这让我产生了错觉,输入和输出流咋会暂用内存,不就是一个数据传送的管道么,都没有把数据读取到内存中,为撒会报错。。。然后就纠结了。。。

不过实在与原来的经验相违背,然后写了一个示例直接从file中读出然后写入到输出流中,发现并没有问题

查HttpURLConnection api发现其有缓存机制,数据并没有实时发送到网络,而是先缓存再发送,导致内存溢出。

解决办法:
httpConnection.setChunkedStreamingMode(0);

//不使用HttpURLConnection的缓存机制,直接将流提交到服务器上。

需要注意的是我们经常使用的hutool的http也存在这个问题

如果不指定chunkedStreamingMode也会出现oom的问题

对于大文件可以这么进行指定。

java 复制代码
  HttpResponse response = HttpUtil.createPost("http://localhost:8062/fallback/uploadModel?type=1")
                .setChunkedStreamingMode(0)
                .form("file", new File("D:\\heap\\heapDump.hprof"))
                .execute();
相关推荐
岁忧8 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao11 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
G等你下课26 分钟前
AJAX请求跨域问题
前端·javascript·http
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干1 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
qq_171538852 小时前
TCP/IP协议精解:IP协议——互联网世界的邮政编码系统
网络·网络协议·tcp/ip
珹洺2 小时前
计算机网络:(七)网络层(上)网络层中重要的概念与网际协议 IP
网络·tcp/ip·计算机网络
Bug退退退1233 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq