Transfer plików pomiędzy serwerem a klientem z wykorzystaniem Spring boot REST

Odpowiedz Nowy wątek
2019-06-28 17:21
0

Cześć, potrzebuję napisać usługę API REST na serwerze, która będzie wywoływana przez aplikacje kliencką w celu pobrania pliku. Pliki jakie będą przesyłane to ok 1-5GB. Dodatkowo usługa powinna być odporna na słabe łącze i zerwanie połączenia(powinno wznawiać przesyłanie) . Napisałem taki serwis, który "bierze" plik i przesyła go dalej do klienta i w przypadku małych plików nie ma żadnego problemu, został przesłany. Problem pojawił się już przy pliku ok 100mb, bo nie dostałem odpowiedzi, a żądanie mieliło i mieliło...

Poniżej kod wysyłania pliku:

FileInputStream inputStream = new FileInputStream(file2download);
        response.setContentType(URLConnection.guessContentTypeFromName(file2download.getName()));
        response.setContentLength((int) file2download.length());
        String headerValue = String.format("attachment; filename=\"%s\"", file2download.getName());
        response.setHeader("Content-Disposition", headerValue);
        OutputStream outStream = response.getOutputStream();
        byte[] buffer = new byte[BUFFER_SIZE];
        int bytesRead = -1; // czytamy w pętli po fragmencie, który następnie przepisujemy do strumienia wyjściowego
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, bytesRead);
        }
        inputStream.close();
        outStream.close();

Nie udało mi się znaleźć żadnych ciekawych bibliotek, które pomogły by mi rozwiązać mój problem, czy koś ma jakiś pomysł i może mi pomóc? :)

Pozostało 580 znaków

2019-06-28 18:26
0

Może aplikacja kliencka wywala się, bo widzi Content-Length o wartości <dużo> i naiwnie próbuje zaalokować sobie tablicę bajtów o takim rozmiarze, co raczej nie może się dobrze skończyć. Na pewno po stronie klienta nie leci żadne exception? Spróbuj wysyłać odopowiedź HTTP chunked. Jakieś asynchroniczne IO byłoby też wskazane, może być nawet WriteListener z Servlet 3.1. Polecam też zero-copy.

Pozostało 580 znaków

2019-07-08 11:52
0

Potrzebuję to zrobić po http, więc sockety itp odpadają.
Próbowałem odczytywać plik fragmentami i tylko odczytany fragment przesyłać, tak żeby w aplikacji klienckiej później posklejać w całość, ale pojawia się problem nadmiernego zużycia pamięci. Używając nio i FIleChannel można z jednego kanału do drugiego "przekopiować" plik bez ładowania do pamięci. Nie znalazłem, jednak takiego rozwiązania, żeby taki plik(ewentualnie część) zwracać w responsie http bez ładowania do pamięci aplikacji. Ktoś ma pomysł jak rozwiązać mój problem, lub inne pomocne rozwiązanie? :)

Pozostało 580 znaków

2019-07-08 16:01
0

Ktoś ma pomysł jak rozwiązać mój problem, lub inne pomocne rozwiązanie? :)

Czemu tego pliku nie streamujesz tylko ładujesz cały do pamięci albo sklejasz kawałki po stronie klienta? Użytkownik może sobie spokojnie pobierać kawałki pliku ze streama i przeglądarka ogarnie mu poskładanie go do kupy.

Pozostało 580 znaków

2019-07-08 16:09
0

W tym problem, że po żadnej ze stron nie ma przeglądarki. Jest aplikacja serwerowa i kliencka. Aplikacja kliencka jak się dowie, że w serwerowej został dodany nowy plik, to ma sobie sama go ściągnąć do siebie bez ingerencji użytkownika. Aplikacji klienckich będzie n :)

Pozostało 580 znaków

2019-07-08 16:29
0

Więc nadal możesz streamować, tylko zapisem pliku ze streama musi zająć się aplikacja kliencka.
Co do zerwanych połączeń to nie mam pojęcia. Prawdopodobnie klient musiałby poprosić o ponowne przesłanie pliku wskazując miejsce, na którym skończył pobieranie (tu gdybam bo w takie coś się nie bawiłem)

edytowany 4x, ostatnio: OtoKamil, 2019-07-08 16:33

Pozostało 580 znaków

2019-07-08 16:42
0

A czy za pomocą streamowania mogę pobrać od miejsca x?
Mógłbym prosić o jakiś przykład takiego streamowania?

Pozostało 580 znaków

2019-07-08 19:10
0

Zrobiłem prosty przykład serwera używając Netty w Kotlinie (wspiera zero-copy przez wrapowanie pliku w typ FileRegion) - w requeście podajesz nazwę pliku i offset od którego ma być wysyłany, np.: ?file=test.txt&from=10
Obsługę większości błędów i bezpieczeństwa ominąłem, być może trzeba też manualnie zwalniać jakieś resource. Na podstawie materiałów w sieci można sobie dopisać klienta do tego, który w razie potrzeby robi retry od pewnego offsetu:

package server

import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.Unpooled
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelInitializer
import io.netty.channel.DefaultFileRegion
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.codec.http.*
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.nio.file.Files

fun main() {
    ServerBootstrap()
        .group(NioEventLoopGroup(1), NioEventLoopGroup())
        .channel(NioServerSocketChannel::class.java)
        .childHandler(object : ChannelInitializer<SocketChannel>() {
            override fun initChannel(connection: SocketChannel) {
                connection.pipeline()
                    .addLast(HttpServerCodec())
                    .addLast(HttpObjectAggregator(Short.MAX_VALUE.toInt()))
                    .addLast(
                        object : ChannelInboundHandlerAdapter() {
                            override fun channelRead(context: ChannelHandlerContext, request: Any) {
                                request as FullHttpRequest
                                try {
                                    handleFileRequest(request, context)
                                } catch (ex: Exception) {
                                    handleError(request, ex, context)
                                }
                            }
                        }
                    )
            }
        })
        .bind(8080).sync().channel().closeFuture().sync()
}

fun handleFileRequest(request: HttpRequest, context: ChannelHandlerContext) {
    val params = QueryStringDecoder(request.uri()).parameters()
    val fileName = params["file"]?.firstOrNull() ?: error("File name not provided")
    val fromOffset = params["from"]?.run { firstOrNull()?.toLong() } ?: 0L
    val file = File(fileName)
    if (file.exists().not()) error("File does not exist: $file")
    val fileSize = file.length()
    val fileRegion = DefaultFileRegion(file, fromOffset, fileSize - fromOffset)
    val headers = DefaultHttpHeaders()
        .add(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"${file.name}\"")
        .add(HttpHeaderNames.CONTENT_TYPE, Files.probeContentType(file.toPath()))
        .add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED)
    val response = DefaultHttpResponse(
        request.protocolVersion(),
        HttpResponseStatus.OK,
        headers
    )
    with(context) {
        writeAndFlush(response)
        writeAndFlush(fileRegion)
        writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
    }
    println(fileRegion)
}

fun handleError(request: HttpRequest, error: Exception, context: ChannelHandlerContext) {
    val body =
        Unpooled.wrappedBuffer(
            StringWriter()
                .also { error.printStackTrace(PrintWriter(it)) }
                .toString()
                .toByteArray()
        )
    val headers = DefaultHttpHeaders()
        .add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
        .add(HttpHeaderNames.CONTENT_LENGTH, body.capacity())
    val response = DefaultFullHttpResponse(
        request.protocolVersion(),
        HttpResponseStatus.BAD_REQUEST,
        body,
        headers,
        EmptyHttpHeaders.INSTANCE
    )
    context.channel()
        .writeAndFlush(response)
}

Pozostało 580 znaków

2019-07-08 22:08
0

SpringBoot/MVC domylsnie wspiera content-range header, więc można ściągać "od danego miejsca" ale to musisz klientem sobie ogarnąć.


Masz problem? Pisz na forum, nie do mnie. Nie masz problemów? Kup komputer...

Pozostało 580 znaków

2019-07-09 09:45
0
Shalom napisał(a):

SpringBoot/MVC domylsnie wspiera content-range header, więc można ściągać "od danego miejsca" ale to musisz klientem sobie ogarnąć.

Ale w tedy aplikacja serwerowa musi obsługiwać odczytywanie danego zakresu i ładuje mi wszystko do pamięci, a przy założeniu, że pliki będą duże i wiele klientów, to prowadzi do wysypania się aplikacji serwerowej i OutofMemory.

Pozostało 580 znaków

2019-07-09 12:58
0

ładuje mi wszystko do pamięci

Dlatego plik streamujesz z dysku (czy gdziekolwiek to trzymasz) i nie pakujesz wszystkiego do pamięci

a przy założeniu, że pliki będą duże i wiele klientów, to prowadzi do wysypania się aplikacji serwerowej i OutofMemory.

Wtedy skalujesz aplikację i nie ma OOM (zakładając, że nie wyrabia ci nawet przy streamowaniu)

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

3 użytkowników online, w tym zalogowanych: 0, gości: 0, botów: 3

Robot: Googlebot, CCBot (2x)