从 OKHTTP 下载二进制文件

我在我的机器人应用程序中使用 OKHTTP 客户端进行网络连接。

这个 示例显示了如何上传二进制文件。我想知道如何获得与 OKHTTP 客户端二进制文件下载的输入流。

下面是这个例子的清单:

public class InputStreamRequestBody extends RequestBody {


private InputStream inputStream;
private MediaType mediaType;


public static RequestBody create(final MediaType mediaType,
final InputStream inputStream) {
return new InputStreamRequestBody(inputStream, mediaType);
}


private InputStreamRequestBody(InputStream inputStream, MediaType mediaType) {
this.inputStream = inputStream;
this.mediaType = mediaType;
}


@Override
public MediaType contentType() {
return mediaType;
}


@Override
public long contentLength() {
try {
return inputStream.available();
} catch (IOException e) {
return 0;
}
}


@Override
public void writeTo(BufferedSink sink) throws IOException {
Source source = null;
try {
source = Okio.source(inputStream);
sink.writeAll(source);
} finally {
Util.closeQuietly(source);
}
}
}

简单获取请求的当前代码是:

OkHttpClient client = new OkHttpClient();
request = new Request.Builder().url("URL string here")
.addHeader("X-CSRFToken", csrftoken)
.addHeader("Content-Type", "application/json")
.build();
response = getClient().newCall(request).execute();

现在我如何将响应转换为 InputStream。类似于 Apache HTTP ClientOkHttp响应的响应:

InputStream is = response.getEntity().getContent();

剪辑

接受下面的回答。 我修改过的代码:

request = new Request.Builder().url(urlString).build();
response = getClient().newCall(request).execute();


InputStream is = response.body().byteStream();


BufferedInputStream input = new BufferedInputStream(is);
OutputStream output = new FileOutputStream(file);


byte[] data = new byte[1024];


long total = 0;


while ((count = input.read(data)) != -1) {
total += count;
output.write(data, 0, count);
}


output.flush();
output.close();
input.close();
93646 次浏览

Getting ByteStream from OKHTTP

I've been digging around in the Documentation of OkHttp you need to go this way

use this method :

response.body().byteStream() wich will return an InputStream

so you can simply use a BufferedReader or any other alternative

OkHttpClient client = new OkHttpClient();
request = new Request.Builder().url("URL string here")
.addHeader("X-CSRFToken", csrftoken)
.addHeader("Content-Type", "application/json")
.build();
response = getClient().newCall(request).execute();


InputStream in = response.body().byteStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String result, line = reader.readLine();
result = line;
while((line = reader.readLine()) != null) {
result += line;
}
System.out.println(result);
response.body().close();

For what it's worth, I would recommend response.body().source() from okio (since OkHttp is already supporting it natively) in order to enjoy an easier way to manipulate a large quantity of data that can come when downloading a file.

@Override
public void onResponse(Call call, Response response) throws IOException {
File downloadedFile = new File(context.getCacheDir(), filename);
BufferedSink sink = Okio.buffer(Okio.sink(downloadedFile));
sink.writeAll(response.body().source());
sink.close();
}

A couple of advantages taken from the documentation in comparison with InputStream:

This interface is functionally equivalent to InputStream. InputStream requires multiple layers when consumed data is heterogeneous: a DataInputStream for primitive values, a BufferedInputStream for buffering, and InputStreamReader for strings. This class uses BufferedSource for all of the above. Source avoids the impossible-to-implement available() method. Instead callers specify how many bytes they require.

Source omits the unsafe-to-compose mark and reset state that's tracked by InputStream; callers instead just buffer what they need.

When implementing a source, you need not worry about the single-byte read method that is awkward to implement efficiently and that returns one of 257 possible values.

And source has a stronger skip method: BufferedSource.skip(long) won't return prematurely.

This is how I use Okhttp + Okio libraries while publishing download progress after every chunk download:

public static final int DOWNLOAD_CHUNK_SIZE = 2048; //Same as Okio Segment.SIZE


try {
Request request = new Request.Builder().url(uri.toString()).build();


Response response = client.newCall(request).execute();
ResponseBody body = response.body();
long contentLength = body.contentLength();
BufferedSource source = body.source();


File file = new File(getDownloadPathFrom(uri));
BufferedSink sink = Okio.buffer(Okio.sink(file));


long totalRead = 0;
long read = 0;
while ((read = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) {
totalRead += read;
int progress = (int) ((totalRead * 100) / contentLength);
publishProgress(progress);
}
sink.writeAll(source);
sink.flush();
sink.close();
publishProgress(FileInfo.FULL);
} catch (IOException e) {
publishProgress(FileInfo.CODE_DOWNLOAD_ERROR);
Logger.reportException(e);
}

Better solution is to use OkHttpClient as:

OkHttpClient client = new OkHttpClient();


Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();






client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}


@Override
public void onResponse(Call call, Response response) throws IOException {


if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);


//                    Headers responseHeaders = response.headers();
//                    for (int i = 0; i < responseHeaders.size(); i++) {
//                        System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
//                    }
//                    System.out.println(response.body().string());


InputStream in = response.body().byteStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String result, line = reader.readLine();
result = line;
while((line = reader.readLine()) != null) {
result += line;
}
System.out.println(result);




}
});

The best option to download (based on source code "okio")

private void download(@NonNull String url, @NonNull File destFile) throws IOException {
Request request = new Request.Builder().url(url).build();
Response response = okHttpClient.newCall(request).execute();
ResponseBody body = response.body();
long contentLength = body.contentLength();
BufferedSource source = body.source();


BufferedSink sink = Okio.buffer(Okio.sink(destFile));
Buffer sinkBuffer = sink.buffer();


long totalBytesRead = 0;
int bufferSize = 8 * 1024;
for (long bytesRead; (bytesRead = source.read(sinkBuffer, bufferSize)) != -1; ) {
sink.emit();
totalBytesRead += bytesRead;
int progress = (int) ((totalBytesRead * 100) / contentLength);
publishProgress(progress);
}
sink.flush();
sink.close();
source.close();
}

Kotlin version based on kiddouk answer

 val request = Request.Builder().url(url).build()
val response = OkHttpClient().newCall(request).execute()
val downloadedFile = File(cacheDir, filename)
val sink: BufferedSink = downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source())
sink.close()

If you are trying to write downloaded bytes into Shared Storage on latest Android, you should have Uri on your hand instead of File instance. Here is how to convert Uri to OutputStream:

fun Uri?.toOutputStream(context: Context)
: OutputStream? {
if (this == null) {
return null
}


fun createAssetFileDescriptor() = try {
context.contentResolver.openAssetFileDescriptor(this, "w")
} catch (e: FileNotFoundException) {
null
}


fun createParcelFileDescriptor() = try {
context.contentResolver.openFileDescriptor(this, "w")
} catch (e: FileNotFoundException) {
null
}


/** scheme://<authority>/<path>/<id> */
if (scheme.equals(ContentResolver.SCHEME_FILE)) {
/** - If AssetFileDescriptor is used, it always writes 0B.
* - (FileOutputStream | ParcelFileDescriptor.AutoCloseOutputStream) works both for app-specific + shared storage
* - If throws "FileNotFoundException: open failed: EACCES (Permission denied)" on Android 10+, use android:requestLegacyExternalStorage="true" on manifest and turnOff/turnOn "write_external_storage" permission on phone settings. Better use Content Uri on Android 10+ */
return try {
FileOutputStream(toFile())
} catch (e: Throwable) {
null
}


} else if (scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE)) {
// i think you can't write to asset inside apk
return null


} else {
// content URI (MediaStore)
if (authority == android.provider.MediaStore.AUTHORITY) {
return try {
context.contentResolver.openOutputStream(this, "w")
} catch (e: Throwable) {
null
}
} else {
// content URI (provider), not tested
return try {
val assetFileDescriptor = createAssetFileDescriptor()
if (assetFileDescriptor != null) {
AssetFileDescriptor.AutoCloseOutputStream(assetFileDescriptor)
} else {
val parcelFileDescriptor = createParcelFileDescriptor()
if (parcelFileDescriptor != null) {
ParcelFileDescriptor.AutoCloseOutputStream(parcelFileDescriptor)
} else {
null
}
}
} catch (e: Throwable) {
null
}
}
}
}

Once you have OutputStream, rest is similar to other answers. More about sink/source and emit/flush:

// create Request
val request = Request.Builder()
.method("GET", null)
.url(url)
.build()


// API call function
fun apiCall(): Response? {
return try {
client.newCall(request).execute()
} catch (error: Throwable) {
null
}
}


// execute API call
var response: Response? = apiCall()
// your retry logic if request failed (response==null)


// if failed, return
if (response == null || !response.isSuccessful) {
return
}


// response.body
val body: ResponseBody? = response!!.body
if (body == null) {
response.closeQuietly()
return
}


// outputStream
val outputStream = destinationUri.toOutputStream(appContext)
if (outputStream == null) {
response.closeQuietly() // calls body.close
return
}
val bufferedSink: BufferedSink = outputStream!!.sink().buffer()
val outputBuffer: Buffer = bufferedSink.buffer


// inputStream
val bufferedSource = body!!.source()
val contentLength = body.contentLength()


// write
var totalBytesRead: Long = 0
var toBeFlushedBytesRead: Long = 0
val BUFFER_SIZE = 8 * 1024L // KB
val FLUSH_THRESHOLD = 200 * 1024L // KB
var bytesRead: Long = bufferedSource.read(outputBuffer, BUFFER_SIZE)
var lastProgress: Int = -1
while (bytesRead != -1L) {
// emit/flush
totalBytesRead += bytesRead
toBeFlushedBytesRead += bytesRead
bufferedSink.emitCompleteSegments()
if (toBeFlushedBytesRead >= FLUSH_THRESHOLD) {
toBeFlushedBytesRead = 0L
bufferedSink.flush()
}


// write
bytesRead = bufferedSource.read(outputBuffer, BUFFER_SIZE)


// progress
if (contentLength != -1L) {
val progress = (totalBytesRead * 100 / contentLength).toInt()
if (progress != lastProgress) {
lastProgress = progress
// update UI (using Flow/Observable/Callback)
}
}
}


bufferedSink.flush()


// close resources
outputStream.closeQuietly()
bufferedSink.closeQuietly()
bufferedSource.closeQuietly()
body.closeQuietly()

Update in Kotlin to match KiddoUK's answer from 2015 (https://stackoverflow.com/a/29012988/413127):

val sourceBytes = response.body().source()
val sink: BufferedSink = File(downloadLocationFilePath).sink().buffer()
sink.writeAll(sourceBytes)
sink.close()

and to add progress monitoring:

val sourceBytes = response.body().source()
val sink: BufferedSink = File(downloadLocationFilePath).sink().buffer()


var totalRead: Long = 0
var lastRead: Long
while (sourceBytes
.read(sink.buffer, 8L * 1024)
.also { lastRead = it } != -1L
) {
totalRead += lastRead
sink.emitCompleteSegments()
// Call your listener/callback here with the totalRead value
}


sink.writeAll(sourceBytes)
sink.close()

I append my solution here:

fun downloadFile(url: String, file: File) : Completable {
val request = Request.Builder().url(url).build()
return Completable.fromPublisher<Boolean> { p ->
OkHttpClient.Builder().build().newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
p.onError(e)
}


override fun onResponse(call: Call, response: Response) {
val sink = Okio.buffer(Okio.sink(file))
sink.writeAll(response.body()!!.source())
sink.close()
p.onComplete()
}


})
}
}

Rebuild the client is important if you use various middlewares or you encoding the data in json, or find a solution that not encode the response in other format.

    val request = Request.Builder().url(URL).build()
val response = OkHttpClient().newCall(request).execute()


val `is`: InputStream = response.body!!.byteStream()


val PATH = (Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.toString() + "/Folder")
val file = File(PATH)


if (!file.exists()) file.mkdir()
  

val outputFile = File(file, getFileName(URL))


val input = BufferedInputStream(`is`)
val output: OutputStream = FileOutputStream(outputFile)


val data = ByteArray(1024)


var total: Long = 0


while (`is`.read(data).also { total = it.toLong() } != -1) {
output.write(data, 0, total.toInt())
}


output.flush()
output.close()
input.close()

Here is a quick method to do it.

  • client - OkHttpClient
  • url - the URL you wish to download
  • destination - where do you wish to save it
    fun download(url: String, destination: File) = destination.outputStream().use { output ->
client.newCall(Request.Builder().url(url).build())
.execute().body!!.byteStream().use { input ->
input.copyTo(output)
}
}