Okhttp上传图片失败,居然是服务端的锅?(一)

OKhttp作为一款优秀的android网络框架,在我们的日常开发中经常使用到,我们除了用来发起get、post等请求,还可以用来上传文件,比如上传图片,正常来说其实是没什么问题的,但是可能会遇到上传图片失败的问题。

我在近期的项目开发中也遇到了使用OKhttp上传图片失败的问题。

项目工程由于历史悠久,迭代周期长达几年,起初网络库使用android内置的httpClient和HttpURLConnection,后来改成了Volley,再后来又换成了retrofit(原理上是通过Okhttp实现各种网络操作的)。

但是。
在这里插入图片描述

由于各种原因(具体原因各位可以自行脑补)项目工程中的上传图片还使用HttpURLConnection来实现,严格来讲应该统一通过retrofit来实现(整个项目工程应该采用统一的网络框架),刚好有闲暇时间,便想着把上传图片的网络层实现改为retrofit。

配置上传接口请求服务:

public interface UploadService {
    @Multipart
    @POST
    Observable<ResponseBody> upLoadImage(@QueryMap Map<String, Object> map, @Part MultipartBody.Part file);
}

上传实现如下:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                        .readTimeout(20, TimeUnit.SECONDS)
                        .connectTimeout(20, TimeUnit.SECONDS)
                        .writeTimeout(20, TimeUnit.SECONDS)
                        .retryOnConnectionFailure(true)
                        .build();
RequestBody requestBody = RequestBody.create(MediaType.parse("image/jpeg"), file);
MultipartBody.Part body = MultipartBody.Part.createFormData("img", "image.jpg", requestBody);
Retrofit retrofit = new Retrofit.Builder()
                        .addConverterFactory(GsonConverterFactory.create())
                        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                        .baseUrl("xxxx")//这里替换为请求主机头地址
                        .client(okHttpClient)
                        .build();
UploadService apiService = retrofit.create(UploadService.class);
apiService.upLoadImage(map, body).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ResponseBody>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        
                    }

                    @Override
                    public void onNext(ResponseBody responseBody) {

                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });

这样就可以了,运行一下试试,结果,真的上传不成功。
在这里插入图片描述

通过charles抓包观察接口返回的Response如下:

{
	"state": 0,
	"msg": "unknown request header",
	"data": {}
}

unknown request header?

因为我们与接口有个约定,当上传成功后返回的response的state字段值应该为1,且会把图片的远程url也一并给回来,所以这个时候其实是上传失败的。

请求参数其实是跟原来用HttpURLConnection的时候一样的,怎么换成retrofit就不行了呢,我们先看下原先用HttpURLConnection上传图片后接口返回的response是怎样的:

{
	"state": 1,
	"msg": "",
	"data": [{
		"original": "xxx.jpg",
		"thumb_b": "xxx.jpg?size=1000x1000",
		"thumb_s": "xxx.jpg?size=100*100",
		"id": 8651
	}]
}

百思不得其解,经过漫长的对比排查后,终于通过retrofit成功上传了图片,做法就是通过charles抓包打断点后改了retrofit发起的上传图片的request,把对应的Form表单格式改成跟用HttpURLConnection上传图片的一直,我们先看下一开始两种方式上传图片对应的form表单对比图:

1、通过HttpURLConnection上传图片的form表单格式如下:
在这里插入图片描述

2、通过retrofit上传图片的form表单格式如下:
在这里插入图片描述

仔细比对我们发现不同的地方是通过retrofit上传图片的form表单内容多了个Content-length内容,奈何我们服务端api不支持包含“Content-length”的上传图片请求,如果没这个字段就可以请求成功了,可是我们并没有显式地往form表单内容添加过“Content-length”,所以应该是网络库帮忙自动添加进去的,在浏览了对应的源码后,发现在MultipartBody.class中发现有对“Content-length”的写入,源码截图如下
在这里插入图片描述

我们看到以下这句

sink.writeUtf8("Content-Length: ")

那么我们只需要在使得这句的if条件不成立,就可以避免往form表单内容写入“Content-length”
在这里插入图片描述

这里的body是我们创建的RequestBody,我们最终调用的是RequestBody的create方法创建的,源码如下:
在这里插入图片描述

我们看到了contentLength()方法返回的是文件的大小,所以上边提到的if条件是永远成立的,因此最终上传的form表单内容也会带“Content-length”,找到原因就好办了,我们只需要在创建RequestBody的时候重写contentLength()方法,使得返回值为-1就可以让form表单内容不写入“Content-length”了,通过如下代码创建新的RequestBody:

 /**
     * 创建用于上传图片的RequestBody
     */
    private RequestBody createRequestBody(final @Nullable MediaType contentType, final File file) {
        return new RequestBody() {
            @Override
            public @Nullable
            MediaType contentType() {
                return contentType;
            }

            @Override
            public long contentLength() {
                /*上传图片最终会用到MultipartBody,表单数据最终会写入Content-Length:xxx,xxx的值为contentLength(),
                 * 而我们的服务端api不支持Content-Length,需要禁止Content-Length的写入,写入逻辑见MultipartBody的
                 * writeOrCountBytes方法*/
                return /*file.length()*/-1;
            }

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

最终我们的上传图片实现如下:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                        .readTimeout(20, TimeUnit.SECONDS)
                        .connectTimeout(20, TimeUnit.SECONDS)
                        .writeTimeout(20, TimeUnit.SECONDS)
                        .retryOnConnectionFailure(true)
                        .build();
RequestBody requestBody = createRequestBody(MediaType.parse("image/jpeg"), file);
MultipartBody.Part body = MultipartBody.Part.createFormData("img", "image.jpg", requestBody);
Retrofit retrofit = new Retrofit.Builder()
                        .addConverterFactory(GsonConverterFactory.create())
                        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                        .baseUrl("xxxx")//这里替换为请求主机头地址
                        .client(okHttpClient)
                        .build();
UploadService apiService = retrofit.create(UploadService.class);
apiService.upLoadImage(map, body).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ResponseBody>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        
                    }

                    @Override
                    public void onNext(ResponseBody responseBody) {

                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });

在这里插入图片描述

搞定,收工。

希望本文可以帮助到您,也希望各位不吝赐教,提出您在使用中的宝贵意见,谢谢。

如果可以的话,也可以扫一扫下方的二维码请作者喝一杯奶茶哈
在这里插入图片描述
谢谢您的观看。
有问题可发送至:1966353889@qq.com
欢迎交流,共同进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值