我们在上一博文《Android网络编程(五) 之 Volley框架的使用》中简单介绍了利用Volley进行网络请求的GET和POST的基本使用。今天我们进一步探讨怎样使用Volley来进行文件的上传。
1 表单结构
文件上传其实就是进行表单的提交,只不过表单提交中有某个字段是该文件的二进制值。我们就拿腾讯云上传API接入来讲解Volley上传的使用步聚,开始前我们来看看表单提交的数据格式是怎么样的。下面是腾讯云上传文件的一个示例抓包数据:
POST /files/v2/<向腾讯云申请的appid>/<appid里头对应的bucket_name>/[文件夹]/<文件名> HTTP/1.1
Accept: */*
Authorization: XXX这里是腾讯云appid等一系列值生成的签名XXX
Content-Type: multipart/form-data;boundary=------------------------------分界字符
User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.0.2; MI2 MIUI/6.10.20)
Host: gz.file.myqcloud.com
Connection: Keep-Alive
Accept-Encoding: gzip
Content-Length: 778
--------------------------------分界字符
Content-Disposition: form-data;name="insertOnly"
0
--------------------------------分界字符
Content-Disposition: form-data; name="op"
upload
--------------------------------分界字符
Content-Disposition: form-data; name="sha"
XXX这里是文件的SHA-1值XXX
--------------------------------分界字符
Content-Disposition: form-data;name="biz_attr"
--------------------------------分界字符
Content-Disposition: form-data;name="filecontent"
Content-Type: text/plain
XXX这里是文件转二进制的byte[]值XXX
--------------------------------分界字符—
我们来分析下上面的数据包,它分两部分,第1行到第9行是headers,第10行到最后是body(提交的数据)。在Volley中headers一般大多都可自动生成,除了几个点要指定,如第1行POST到HTTP/1.1之间的是提交的URL;第4行Content-Type后面是指定提交参数的类型,而boundary后面的是自定义的分界字符,要跟下面的保持一致;腾讯云还要特别加上签名校验,那就是第3行的。接下来继续分析body部分,body部分的所有数据都是要手动拼接来生成的,欣慰的是它们都是有规律。
2 UploadRequest
我们在《Android网络编程(五) 之 Volley框架的使用》中介绍过StringRequest、JsonRequest和ImageRequest的使用,它们都是继承于Request类,所以我们完全可以模仿它们来写一个UploadRequest。
新建UploadRequest类,如下:
public class UploadRequest extends Request<JSONObject> {
private final static int TIME_OUT = 5 *60 * 1000; // 5分钟
private Map<String,Object> mParams;
private byte[] mFileByte;
private String mAuthorization;
private finalResponse.Listener<JSONObject> mListener;
public UploadRequest(String url, Map<String, Object> params, String authorization, byte[]fileByte, Response.Listener<JSONObject> listener, Response.ErrorListenererrorListener) {
super(Request.Method.POST,url, errorListener);
mParams =params;
mAuthorization= authorization;
mFileByte =fileByte;
this.mListener= listener;
setShouldCache(false);
setRetryPolicy(newDefaultRetryPolicy(TIME_OUT,DefaultRetryPolicy.DEFAULT_MAX_RETRIES,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
}
@Override
protected Response<JSONObject>parseNetworkResponse(NetworkResponse response) {
try {
String je = newString(response.data,HttpHeaderParser.parseCharset(response.headers,"utf-8"));
return Response.success(newJSONObject(je), HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException var3) {
return Response.error(newParseError(var3));
} catch (JSONException var4) {
return Response.error(newParseError(var4));
}
}
@Override
protected void deliverResponse(JSONObject jsonObject) {
if(mListener != null){
mListener.onResponse(jsonObject);
}
}
}
第一步,使UploadRequest类继承Request,因为腾讯云上传后返回是JSON类型的数据,所以指定为JSONObject类型;
第二步,定义构造函数,构造函数接收请求的url、参数、腾讯云签名、要上传文件的二进制值、上传成功回调和上传失败回调。函数实体中设置了Volley的缓存情况为false和请求的超时时间,这里定5分钟。当然这两个值也可以从构造函数中传入来指定;
第三步,重写必要的两个方法parseNetworkResponse和deliverResponse,parseNetworkResponse我们就把JsonRequest类内的parseNetworkResponse方法照搬即可,deliverResponse是指定上传成功的回调;
接下来,继续重写三个非常重要的方法,它们是getBodyContentType、getHeaders和getBody:
private final static String BOUNDARY = "------------------------------lyz_zyx"; // 数据分界线
@Override
public String getBodyContentType() {
return "multipart/form-data;boundary=" + BOUNDARY;
}
@Override
public Map<String, String> getHeaders() throwsAuthFailureError {
Map<String, String> headers = newHashMap<>();
headers.putAll(super.getHeaders());
if (mAuthorization!= null && mAuthorization.isEmpty()){
headers.put("Authorization",mAuthorization);
}
headers.put("Accept","*/*"); //这句一定要加上,否则上传的腾讯云上的文件会是0B,若加sha1校验,则上传失败
return headers;
}
@Override
public byte[] getBody() throwsAuthFailureError {
if (mParams== null) {
return null;
}
ByteArrayOutputStream bos = newByteArrayOutputStream() ;
String enterNewline = "\r\n";
String fix = "--";
StringBuffer sb= newStringBuffer() ;
try {
Iterator iter = mParams.entrySet().iterator();
while (iter.hasNext()){
Map.Entry entry = (Map.Entry)iter.next();
Object key = entry.getKey();
Object val =entry.getValue();
sb.append(fix + BOUNDARY+ enterNewline);
sb.append("Content-Disposition:form-data; name=\"" +key + "\"" +enterNewline);
sb.append(enterNewline);
sb.append(val +enterNewline);
}
sb.append(fix + BOUNDARY+ enterNewline);
sb.append("Content-Disposition:form-data; name=\"filecontent\""+ enterNewline);
sb.append("Content-Type:text/plain" + enterNewline);
sb.append(enterNewline);
bos.write(sb.toString().getBytes("utf-8"));
bos.write(mFileByte);
StringBuffer sbEnd= newStringBuffer() ;
sbEnd.append(enterNewline) ;
sbEnd.append(fix + BOUNDARY+ fix + enterNewline);
bos.write(sbEnd.toString().getBytes("utf-8"));
} catch (IOExceptione) {
e.printStackTrace();
}
return bos.toByteArray();
}
getBodyContentType方法返回的字符串,就是我们上面提到的headers中的第4行内容;
getHeaders方法添加了两项值,分别是Accept和Authorization。也就是我们上面提到在headers中的第2行和第3行的内容;
getBody方法就是我们上面提到的需要手动拼接生成的body部分,它的前面做了一个针对构造函数传入的mParams值的循环,循环完后再接定拼构造函数传入的mFileByte部分。
就这样整个UploadRequest类就大功告成。虽然我们的初衷是针对腾讯云上传功能做的设计,但其实UploadRequest类也是完全通用于其它普通表单提交上传文件的,只要不传入签名即可。
最后,就是如何调用了:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn1 =(Button)findViewById(R.id.btn1);
btn1.setOnClickListener(newView.OnClickListener() {
@Override
public void onClick(Viewv) {
uploadFile("/sdcard/upload_log.zip");
}
});
}
/**
* 执行上传文件
* @param filePath
*/
private void uploadFile(String filePath) {
File file = newFile(filePath);
if (!file.exists()){
return;
}
UploadFileData uploadFileData = newUploadFileData();
uploadFileData.setFileName(file.getName());
Common.getFileByteAndSha1(file,uploadFileData);
String url = String.format(REQUEST_UPLOAD_URL,uploadFileData.getFileName());
Map<String, Object> params = newHashMap<>();
params.put("op","upload");
params.put("sha",uploadFileData.getFileSha1().toLowerCase()); // 注意,sha1值必须小写,这点腾讯云API没提到
params.put("biz_attr","");
params.put("insertOnly",0);
String authorization = getSign();
RequestQueue requestQueue = Volley.newRequestQueue(getApplicationContext());
UploadRequest uploadRequest = new UploadRequest(url, params, authorization,uploadFileData.getFileByte(),
new Response.Listener<JSONObject>(){
@Override
publicvoid onResponse(JSONObject jsonObject) {
//TODO 上传成功
}
},
new Response.ErrorListener(){
@Override
publicvoid onErrorResponse(VolleyError volleyError) {
//TODO 上传失败
}
});
requestQueue.add(uploadRequest);
}
对应代码,调用过程是先指定一个手机文件目录,然后对应文件并算出它的sha1值和二进制值,接着通过腾讯云提供的资料算出本次上传的签名,最后一步就是构造一个我们准备好的UploadRequest类的对象然后交给RequestQueue就好了。还有一点,别忘记了声明相应的权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permissionandroid:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
3 总结
本博文主要是描述了表单的结构和利用Volley怎样生成表单来请求网络。关于腾讯云的使用,我们示例为了解析表单的提交,所以给出了最原始的使用方法。如果您项目不介意包的大小,完全可以使用腾讯云API中出提供了Android SDK包,只要简单地在项目中引用其jar后,对应API介绍方法可以非常简单的十来行代码便可完成整个上传功能,这里就不作过多的介绍了,详细可见腾讯去APIhttps://www.qcloud.com/document/api/436/6066,另外本项目完整源码可点击这里下载。