OKHTTP 3 跟踪分段上传进度
OKHTTP 3 Tracking Multipart upload progress
如何在 OkHttp 3 中跟踪上传进度
我可以找到 v2 但不是 v3 的答案,比如 this
来自 OkHttp 食谱的示例多部分请求
private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
你可以装饰你的OkHttp请求体来计算写入的字节数;为了完成这个任务,用一个 Listener 实例将你的 MultiPart RequestBody 包装在这个 RequestBody 中,瞧!
public class ProgressRequestBody extends RequestBody {
protected RequestBody mDelegate;
protected Listener mListener;
protected CountingSink mCountingSink;
public ProgressRequestBody(RequestBody delegate, Listener listener) {
mDelegate = delegate;
mListener = listener;
}
@Override
public MediaType contentType() {
return mDelegate.contentType();
}
@Override
public long contentLength() {
try {
return mDelegate.contentLength();
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
mCountingSink = new CountingSink(sink);
BufferedSink bufferedSink = Okio.buffer(mCountingSink);
mDelegate.writeTo(bufferedSink);
bufferedSink.flush();
}
protected final class CountingSink extends ForwardingSink {
private long bytesWritten = 0;
public CountingSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
bytesWritten += byteCount;
mListener.onProgress((int) (100F * bytesWritten / contentLength()));
}
}
public interface Listener {
void onProgress(int progress);
}
}
查看 this link 了解更多。
根据Sourabh的回答,我想告诉CountingSink的那个字段
private long bytesWritten = 0;
必须移入 ProgressRequestBody class
我无法得到任何适合我的答案。问题是在上传图像之前进度会 运行 100%,这表明在通过网络发送数据之前某些缓冲区已被填满。研究了一下发现确实是这样,那个buffer就是Socket发送buffer。向 OkHttpClient 提供 SocketFactory 终于奏效了。我的 Kotlin 代码如下...
首先,与其他人一样,我有一个用于包装 MultipartBody 的 CountingRequestBody。
class CountingRequestBody(var delegate: RequestBody, private var listener: (max: Long, value: Long) -> Unit): RequestBody() {
override fun contentType(): MediaType? {
return delegate.contentType()
}
override fun contentLength(): Long {
try {
return delegate.contentLength()
} catch (e: IOException) {
e.printStackTrace()
}
return -1
}
override fun writeTo(sink: BufferedSink) {
val countingSink = CountingSink(sink)
val bufferedSink = Okio.buffer(countingSink)
delegate.writeTo(bufferedSink)
bufferedSink.flush()
}
inner class CountingSink(delegate: Sink): ForwardingSink(delegate) {
private var bytesWritten: Long = 0
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
listener(contentLength(), bytesWritten)
}
}
}
我在 Retrofit2 中使用它。一般用法是这样的:
val builder = MultipartBody.Builder()
// Add stuff to the MultipartBody via the Builder
val body = CountingRequestBody(builder.build()) { max, value ->
// Progress your progress, or send it somewhere else.
}
此时,我正在取得进展,但我会看到 100%,然后在数据上传时等待很长时间。关键是套接字在我的设置中默认配置为缓冲 3145728 字节的发送数据。好吧,我的图像就在那个下方,并且进度显示了填充该套接字发送缓冲区的进度。为了缓解这种情况,为 OkHttpClient 创建一个 SocketFactory。
class ProgressFriendlySocketFactory(private val sendBufferSize: Int = DEFAULT_BUFFER_SIZE) : SocketFactory() {
override fun createSocket(): Socket {
return setSendBufferSize(Socket())
}
override fun createSocket(host: String, port: Int): Socket {
return setSendBufferSize(Socket(host, port))
}
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
return setSendBufferSize(Socket(host, port, localHost, localPort))
}
override fun createSocket(host: InetAddress, port: Int): Socket {
return setSendBufferSize(Socket(host, port))
}
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
return setSendBufferSize(Socket(address, port, localAddress, localPort))
}
private fun setSendBufferSize(socket: Socket): Socket {
socket.sendBufferSize = sendBufferSize
return socket
}
companion object {
const val DEFAULT_BUFFER_SIZE = 2048
}
}
并在配置期间进行设置。
val clientBuilder = OkHttpClient.Builder()
.socketFactory(ProgressFriendlySocketFactory())
正如其他人所提到的,记录请求正文可能会影响这一点并导致数据被多次读取。要么不记录正文,要么我所做的是为 CountingRequestBody 关闭它。为此,我编写了自己的 HttpLoggingInterceptor,它解决了这个问题和其他问题(比如记录 MultipartBody)。但这超出了这个问题的范围。
if(requestBody is CountingRequestBody) {
// don't log the body in production
}
其他问题与 MockWebServer 有关。我有一种使用 MockWebServer 和 json 文件的风格,因此我的应用程序可以 运行 没有网络,因此我可以在没有这种负担的情况下进行测试。要使此代码正常工作,Dispatcher 需要读取正文数据。我创建这个 Dispatcher 就是为了做到这一点。然后它将调度转发给另一个 Dispatcher,例如默认的 QueueDispatcher。
class BodyReadingDispatcher(val child: Dispatcher): Dispatcher() {
override fun dispatch(request: RecordedRequest?): MockResponse {
val body = request?.body
if(body != null) {
val sink = ByteArray(1024)
while(body.read(sink) >= 0) {
Thread.sleep(50) // change this time to work for you
}
}
val response = child.dispatch(request)
return response
}
}
您可以在 MockWebServer 中将其用作:
var server = MockWebServer()
server.setDispatcher(BodyReadingDispatcher(QueueDispatcher()))
这是我项目中的所有工作代码。我确实出于说明目的将其拉出。如果开箱即用对您不起作用,我深表歉意。
如何在 OkHttp 3 中跟踪上传进度 我可以找到 v2 但不是 v3 的答案,比如 this
来自 OkHttp 食谱的示例多部分请求
private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
// Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "Square Logo")
.addFormDataPart("image", "logo-square.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
你可以装饰你的OkHttp请求体来计算写入的字节数;为了完成这个任务,用一个 Listener 实例将你的 MultiPart RequestBody 包装在这个 RequestBody 中,瞧!
public class ProgressRequestBody extends RequestBody {
protected RequestBody mDelegate;
protected Listener mListener;
protected CountingSink mCountingSink;
public ProgressRequestBody(RequestBody delegate, Listener listener) {
mDelegate = delegate;
mListener = listener;
}
@Override
public MediaType contentType() {
return mDelegate.contentType();
}
@Override
public long contentLength() {
try {
return mDelegate.contentLength();
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
@Override
public void writeTo(BufferedSink sink) throws IOException {
mCountingSink = new CountingSink(sink);
BufferedSink bufferedSink = Okio.buffer(mCountingSink);
mDelegate.writeTo(bufferedSink);
bufferedSink.flush();
}
protected final class CountingSink extends ForwardingSink {
private long bytesWritten = 0;
public CountingSink(Sink delegate) {
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException {
super.write(source, byteCount);
bytesWritten += byteCount;
mListener.onProgress((int) (100F * bytesWritten / contentLength()));
}
}
public interface Listener {
void onProgress(int progress);
}
}
查看 this link 了解更多。
根据Sourabh的回答,我想告诉CountingSink的那个字段
private long bytesWritten = 0;
必须移入 ProgressRequestBody class
我无法得到任何适合我的答案。问题是在上传图像之前进度会 运行 100%,这表明在通过网络发送数据之前某些缓冲区已被填满。研究了一下发现确实是这样,那个buffer就是Socket发送buffer。向 OkHttpClient 提供 SocketFactory 终于奏效了。我的 Kotlin 代码如下...
首先,与其他人一样,我有一个用于包装 MultipartBody 的 CountingRequestBody。
class CountingRequestBody(var delegate: RequestBody, private var listener: (max: Long, value: Long) -> Unit): RequestBody() {
override fun contentType(): MediaType? {
return delegate.contentType()
}
override fun contentLength(): Long {
try {
return delegate.contentLength()
} catch (e: IOException) {
e.printStackTrace()
}
return -1
}
override fun writeTo(sink: BufferedSink) {
val countingSink = CountingSink(sink)
val bufferedSink = Okio.buffer(countingSink)
delegate.writeTo(bufferedSink)
bufferedSink.flush()
}
inner class CountingSink(delegate: Sink): ForwardingSink(delegate) {
private var bytesWritten: Long = 0
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
listener(contentLength(), bytesWritten)
}
}
}
我在 Retrofit2 中使用它。一般用法是这样的:
val builder = MultipartBody.Builder()
// Add stuff to the MultipartBody via the Builder
val body = CountingRequestBody(builder.build()) { max, value ->
// Progress your progress, or send it somewhere else.
}
此时,我正在取得进展,但我会看到 100%,然后在数据上传时等待很长时间。关键是套接字在我的设置中默认配置为缓冲 3145728 字节的发送数据。好吧,我的图像就在那个下方,并且进度显示了填充该套接字发送缓冲区的进度。为了缓解这种情况,为 OkHttpClient 创建一个 SocketFactory。
class ProgressFriendlySocketFactory(private val sendBufferSize: Int = DEFAULT_BUFFER_SIZE) : SocketFactory() {
override fun createSocket(): Socket {
return setSendBufferSize(Socket())
}
override fun createSocket(host: String, port: Int): Socket {
return setSendBufferSize(Socket(host, port))
}
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
return setSendBufferSize(Socket(host, port, localHost, localPort))
}
override fun createSocket(host: InetAddress, port: Int): Socket {
return setSendBufferSize(Socket(host, port))
}
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
return setSendBufferSize(Socket(address, port, localAddress, localPort))
}
private fun setSendBufferSize(socket: Socket): Socket {
socket.sendBufferSize = sendBufferSize
return socket
}
companion object {
const val DEFAULT_BUFFER_SIZE = 2048
}
}
并在配置期间进行设置。
val clientBuilder = OkHttpClient.Builder()
.socketFactory(ProgressFriendlySocketFactory())
正如其他人所提到的,记录请求正文可能会影响这一点并导致数据被多次读取。要么不记录正文,要么我所做的是为 CountingRequestBody 关闭它。为此,我编写了自己的 HttpLoggingInterceptor,它解决了这个问题和其他问题(比如记录 MultipartBody)。但这超出了这个问题的范围。
if(requestBody is CountingRequestBody) {
// don't log the body in production
}
其他问题与 MockWebServer 有关。我有一种使用 MockWebServer 和 json 文件的风格,因此我的应用程序可以 运行 没有网络,因此我可以在没有这种负担的情况下进行测试。要使此代码正常工作,Dispatcher 需要读取正文数据。我创建这个 Dispatcher 就是为了做到这一点。然后它将调度转发给另一个 Dispatcher,例如默认的 QueueDispatcher。
class BodyReadingDispatcher(val child: Dispatcher): Dispatcher() {
override fun dispatch(request: RecordedRequest?): MockResponse {
val body = request?.body
if(body != null) {
val sink = ByteArray(1024)
while(body.read(sink) >= 0) {
Thread.sleep(50) // change this time to work for you
}
}
val response = child.dispatch(request)
return response
}
}
您可以在 MockWebServer 中将其用作:
var server = MockWebServer()
server.setDispatcher(BodyReadingDispatcher(QueueDispatcher()))
这是我项目中的所有工作代码。我确实出于说明目的将其拉出。如果开箱即用对您不起作用,我深表歉意。