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();
相关推荐
涔溪1 小时前
HTTP TCP三次握手深入解析
网络·tcp/ip·http
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
三菱-Liu2 小时前
三菱MR-J4-B伺服连接器和信号排列
网络·驱动开发·硬件工程·制造·mr
WeeJot嵌入式2 小时前
网络安全:挑战、策略与未来趋势
网络
霖雨2 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构