您的位置 首页 java

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() {

                    }
                });  

搞定,收工。

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

文章来源:智云一二三科技

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

文章地址:https://www.zhihuclub.com/174658.shtml

关于作者: 智云科技

热门文章

网站地图