简述
这周遇到两个新问题,就是大文件的上传和下载。
场景 |
之前 |
问题 |
大文件上传云存储 |
文件上传到服务器,在从服务器上传到云存储 |
1、通过nginx、nacos转发耗时太长 2、太消耗服务器资源 |
用户下载视频 |
从其他端下载视频,再将视频转发给客户端 |
同上 |
这两个问题的解决,还得感谢chatgpt,这玩意还是好用的。
大文件上传
之前都是直接将文件上传到服务器,通过服务器将文件上传到云端,拿到链接后保存链接。这样刚开始使用的时候还行,上传的文件比较小,后来要上传大文件,问题就暴露出来了,耗时太长。文件要经过nginx、nacos的转发,一个200M的文件,到服务器后端代码开始处理已经过去十多分钟了。除了耗时,还太消耗资源,nginx、nacos、spring服务这每一项都要转发一下大文件,都要消耗内存流量。
解决方案:1、改长相应时间,2、修改设计方案。第一个解决方案明显不行,没解决根本问题,耗时长,资源消耗大的问题还是存在。那就只能考虑第二个解决方案了。
其实刚开始,我也没啥好的解决方案(见识短浅)。后来问了一下chatgpt,解决方案直接就出来了,生成预链接,让前端通过预链接上传文件。说是业界都用的这种方案,我们都不知道,还是落后呢脱节了啊。我们公司用的是AWS的S3存储,它是支持生成预链接的,除了AWS,其他的云厂商都是支持的(阿里云,腾讯云等)。为了保障链接的有效性,我在后加了一个地址校验,校验一下预链接的地址前端有没有上传文件,防止前端没有上传。代码编写随便百度一下大把。

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
|
public URL generatePresignedPutUrl(String bucketName, String objectKey, String contentType, int validMinutes) { try (S3Presigner preSigner = S3Presigner.builder() .region(REGION) .credentialsProvider(StaticCredentialsProvider.create( AwsBasicCredentials.create(ACCESS_KEY, SECRET_KEY) )) .build()) {
PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) .key(objectKey) .acl(ObjectCannedACL.PUBLIC_READ) .contentType(contentType) .build();
PresignedPutObjectRequest preSignedRequest = preSigner.presignPutObject(builder -> builder .putObjectRequest(putObjectRequest) .signatureDuration(Duration.ofMinutes(validMinutes)) ); return preSignedRequest.url(); } }
|
视频下载
背景:本来可以提供三方的视频链接直接给前端下载的,可以视频链接有反扒校验,预ip绑定,其他ip都打不开链接,所以只能后端服务器下载了传给前端。
初次解决方案:服务器直接下载完,再转发给前端下载,但是这样要与前端保持链接的时间太长了,服务端要下载一遍,前端也要接着下载一遍。同时也消耗服务器资源,带宽流量和服务器内存。
优化方案:通过服务器直接透传给前端,这样服务器不用保存,可以减少内存的压力,同时也减少相应的时间。不过这也有问题,前端没那么稳定,会断,导致整个视频下载不了。
最终方案:分片返回给前端,将一个视频流切成很多片,每次请求一片,这样将一个长连接变成了许多短连接。不会因前端的一次断开,导致整个下载失败。
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
| @PostMapping("/pronhub/url/download") public ResponseEntity<StreamingResponseBody> downloadVideoWithRange( HttpServletRequest request, HttpServletResponse response, @RequestBody PronhubUrlDownLoadRequest body) throws IOException {
String videoUrl = body.getUrl(); URL url = new URL(videoUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestProperty("User-Agent", "Mozilla/5.0");
int responseCode = conn.getResponseCode(); if (responseCode != 200) { return ResponseEntity.status(responseCode) .contentType(MediaType.TEXT_PLAIN) .body(out -> out.write(("视频请求失败: " + responseCode).getBytes(StandardCharsets.UTF_8))); }
String contentType = conn.getContentType(); int fullLength = conn.getContentLength();
String rangeHeader = request.getHeader("Range"); int start = 0; int end;
if (rangeHeader != null && rangeHeader.startsWith("bytes=")) { String[] ranges = rangeHeader.replace("bytes=", "").split("-"); start = Integer.parseInt(ranges[0].trim()); if (ranges.length > 1 && !ranges[1].isEmpty()) { end = Integer.parseInt(ranges[1].trim()); } else { end = Math.min(start + CHUNK_SIZE - 1, fullLength - 1); } } else { end = Math.min(start + CHUNK_SIZE - 1, fullLength - 1); }
int rangeLength = end - start + 1; conn.setRequestProperty("Range", "bytes=" + start + "-" + end); InputStream inputStream = conn.getInputStream();
final int finalStart = start; final int finalEnd = end;
StreamingResponseBody stream = outputStream -> { try (InputStream in = inputStream) { byte[] buffer = new byte[8192]; int bytesRead; long totalRead = 0; long lastFlush = System.currentTimeMillis();
while ((bytesRead = in.read(buffer)) != -1 && totalRead < rangeLength) { int toWrite = (int) Math.min(bytesRead, rangeLength - totalRead); outputStream.write(buffer, 0, toWrite); totalRead += toWrite;
if (System.currentTimeMillis() - lastFlush > 2000) { outputStream.flush(); lastFlush = System.currentTimeMillis(); } }
outputStream.flush(); } catch (IOException ex) { if (ex.getMessage().contains("Broken pipe")) { log.warn("客户端连接中断: {}", ex.getMessage()); } else { log.error("视频传输异常: ", ex); } } };
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT) .header("Accept-Ranges", "bytes") .header("Content-Type", contentType != null ? contentType : "video/mp4") .header("Content-Length", String.valueOf(rangeLength)) .header("Content-Range", String.format("bytes %d-%d/%d", finalStart, finalEnd, fullLength)) .header("Full-Length", String.valueOf(fullLength)) .header("Access-Control-Expose-Headers", "Content-Range,Full-Length") .header("Content-Disposition", "attachment; filename=video.mp4") .body(stream); }
|