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();
}

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

java.lang.OutOfMemoryError: Java heap space

客户端

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

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接收上传文件的写法(多个文件)

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

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

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

如果是文件类型,那么就要通过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;

根据分隔符找出上传的多个文件进行读取字节流,同时创建本地文件,写入到本地文件,这里循环通过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);
            }
        }
    }

从这里我们也能看出来,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<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的问题
对于大文件可以这么进行指定。

 HttpResponse response = HttpUtil.createPost("http://localhost:8062/fallback/uploadModel?type=1")
                .setChunkedStreamingMode(0)
                .form("file", new File("D:\\heap\\heapDump.hprof"))
                .execute();

参考文章:http://blog.ncmem.com/wordpress/2023/12/13/java%e5%ae%9e%e7%8e%b0http%e7%9a%84%e4%b8%8a%e4%bc%a0%e4%b8%8e%e4%b8%8b%e8%bd%bd/
欢迎入群一起讨论

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值