最近在参加CSDN博客之星,希望大家给投一票,谢谢啦~ 点这里投我一票吧~
前言
在开发当中,我们常常需要实现文件上传,比较常见的就是图片上传,比如修改个头像什么的。但是这个功能在Android和iOS中都没有默认的实现类,对于Android我们可以使用Apache提供的HttpClient.jar来实现这个功能,其中依赖的类就是Apache的httpmime.jar中的MultipartEntity这个类。我就是要实现一个文件上传功能,但是我还得下载一个jar包,而这个jar包几十KB,这尼玛仿佛并非人间!今天我们就来自己实现文件上传功能,并且弄懂它们的原理。
在上一篇文章HTTP POST请求报文格式分析与Java实现文件上传中我们介绍了HTTP POST报文格式,如果有对POST报文格式不了解的同学可以先阅读这篇文章。
自定义实现MultipartEntity
我们知道,使用网络协议传输数据无非就是要遵循某个协议,我们在开发移动应用时基本上都是使用HTTP协议。HTTP协议说白了就是基于TCP的一套网络请求协议,你根据该协议规定的格式传输数据,然后服务器返回给你数据。你的协议参数要是传递错了,那么服务器只能给你返回错误。
这跟间谍之间对暗号有点相似,他们有一个规定的暗号,双方见面,A说: 天王盖地虎,B对: 宝塔镇河妖。对上了,说事;对不上,弄死这B。HTTP也是这样的,在HTTP请求时添加header和参数,服务器根据参数进行解析。形如 :
POST /api/feed/ HTTP/1.1 这里是header数据 --分隔符 参数1 --分隔符 参数2
只要根据格式来向服务器发送请求就万事大吉了!下面我们就来看MultipartEntity的实现:
public class MultipartEntity implements HttpEntity { private final static char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" .toCharArray(); /** * 换行符 */ private final String NEW_LINE_STR = "\r\n"; private final String CONTENT_TYPE = "Content-Type: "; private final String CONTENT_DISPOSITION = "Content-Disposition: "; /** * 文本参数和字符集 */ private final String TYPE_TEXT_CHARSET = "text/plain; charset=UTF-8"; /** * 字节流参数 */ private final String TYPE_OCTET_STREAM = "application/octet-stream"; /** * 二进制参数 */ private final byte[] BINARY_ENCODING = "Content-Transfer-Encoding: binary\r\n\r\n".getBytes(); /** * 文本参数 */ private final byte[] BIT_ENCODING = "Content-Transfer-Encoding: 8bit\r\n\r\n".getBytes(); /** * 分隔符 */ private String mBoundary = null; /** * 输出流 */ ByteArrayOutputStream mOutputStream = new ByteArrayOutputStream(); public MultipartEntity() { this.mBoundary = generateBoundary(); } /** * 生成分隔符 * * @return */ private final String generateBoundary() { final StringBuffer buf = new StringBuffer(); final Random rand = new Random(); for (int i = 0; i < 30; i++) { buf.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]); } return buf.toString(); } /** * 参数开头的分隔符 * * @throws IOException */ private void writeFirstBoundary() throws IOException { mOutputStream.write(("--" + mBoundary + "\r\n").getBytes()); } /** * 添加文本参数 * * @param key * @param value */ public void addStringPart(final String paramName, final String value) { writeToOutputStream(paramName, value.getBytes(), TYPE_TEXT_CHARSET, BIT_ENCODING, ""); } /** * 将数据写入到输出流中 * * @param key * @param rawData * @param type * @param encodingBytes * @param fileName */ private void writeToOutputStream(String paramName, byte[] rawData, String type, byte[] encodingBytes, String fileName) { try { writeFirstBoundary(); mOutputStream.write((CONTENT_TYPE + type + NEW_LINE_STR).getBytes()); mOutputStream .write(getContentDispositionBytes(paramName, fileName)); mOutputStream.write(encodingBytes); mOutputStream.write(rawData); mOutputStream.write(NEW_LINE_STR.getBytes()); } catch (final IOException e) { e.printStackTrace(); } } /** * 添加二进制参数, 例如Bitmap的字节流参数 * * @param key * @param rawData */ public void addBinaryPart(String paramName, final byte[] rawData) { writeToOutputStream(paramName, rawData, TYPE_OCTET_STREAM, BINARY_ENCODING, "no-file"); } /** * 添加文件参数,可以实现文件上传功能 * * @param key * @param file */ public void addFilePart(final String key, final File file) { InputStream fin = null; try { fin = new FileInputStream(file); writeFirstBoundary(); final String type = CONTENT_TYPE + TYPE_OCTET_STREAM + NEW_LINE_STR; mOutputStream.write(getContentDispositionBytes(key, file.getName())); mOutputStream.write(type.getBytes()); mOutputStream.write(BINARY_ENCODING); final byte[] tmp = new byte[4096]; int len = 0; while ((len = fin.read(tmp)) != -1) { mOutputStream.write(tmp, 0, len); } mOutputStream.flush(); } catch (final IOException e) { e.printStackTrace(); } finally { closeSilently(fin); } } private void closeSilently(Closeable closeable) { try { if (closeable != null) { closeable.close(); } } catch (final IOException e) { e.printStackTrace(); } } private byte[] getContentDispositionBytes(String paramName, String fileName) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(CONTENT_DISPOSITION + "form-data; name=\"" + paramName + "\""); // 文本参数没有filename参数,设置为空即可 if (!TextUtils.isEmpty(fileName)) { stringBuilder.append("; filename=\"" + fileName + "\""); } return stringBuilder.append(NEW_LINE_STR).toString().getBytes(); } @Override public long getContentLength() { return mOutputStream.toByteArray().length; } @Override public Header getContentType() { return new BasicHeader("Content-Type", "multipart/form-data; boundary=" + mBoundary); } @Override public boolean isChunked() { return false; } @Override public boolean isRepeatable() { return false; } @Override public boolean isStreaming() { return false; } @Override public void writeTo(final OutputStream outstream) throws IOException { // 参数最末尾的结束符 final String endString = "--" + mBoundary + "--\r\n"; // 写入结束符 mOutputStream.write(endString.getBytes()); // outstream.write(mOutputStream.toByteArray()); } @Override public Header getContentEncoding() { return null; } @Override public void consumeContent() throws IOException, UnsupportedOperationException { if (isStreaming()) { throw new UnsupportedOperationException( "Streaming entity does not implement #consumeContent()"); } } @Override public InputStream getContent() { return new ByteArrayInputStream(mOutputStream.toByteArray()); } }
用户可以通过addStringPart、addBinaryPart、addFilePart来添加参数,分别表示添加字符串参数、添加二进制参数、添加文件参数。在MultipartEntity中有一个ByteArrayOutputStream对象,先将这些参数写到这个输出流中,当执行网络请求时,会执行
writeTo(final OutputStream outstream)
方法将所有参数的字节流数据写入到与服务器建立的TCP连接的输出流中,这样就将我们的参数传递给服务器了。当然在此之前,我们需要按照格式来向ByteArrayOutputStream对象中写数据。
例如我要向服务器发送一个文本、一张bitmap图片、一个文件,即这个请求有三个参数。代码如下 :
MultipartEntity multipartEntity = new MultipartEntity(); // 文本参数 multipartEntity.addStringPart("type", "我的文本参数"); Bitmap bmp = BitmapFactory.decodeResource(getResources(), R.drawable.thumb); // 二进制参数 multipartEntity.addBinaryPart("images", bitmapToBytes(bmp)); // 文件参数 multipartEntity.addFilePart("images", new File("storage/emulated/0/test.jpg")); // POST请求 HttpPost post = new HttpPost("url") ; // 将multipartEntity设置给post post.setEntity(multipartEntity); // 使用http client来执行请求 HttpClient httpClient = new DefaultHttpClient() ; httpClient.execute(post) ;
MultipartEntity的输出格式会成为如下的格式 :
POST /api/feed/ HTTP/1.1 Content-Type: multipart/form-data; boundary=o3Fhj53z-oKToduAElfBaNU4pZhp4- User-Agent: Dalvik/1.6.0 (Linux; U; Android 4.4.4; M040 Build/KTU84P) Host: www.myhost.com Connection: Keep-Alive Accept-Encoding: gzip Content-Length: 168518 --o3Fhj53z-oKToduAElfBaNU4pZhp4- Content-Type: text/plain; charset=UTF-8 Content-Disposition: form-data; name="type" Content-Transfer-Encoding: 8bit This my type --o3Fhj53z-oKToduAElfBaNU4pZhp4- Content-Type: application/octet-stream Content-Disposition: form-data; name="images"; filename="no-file" Content-Transfer-Encoding: binary 这里是bitmap的二进制数据 --o3Fhj53z-oKToduAElfBaNU4pZhp4- Content-Type: application/octet-stream Content-Disposition: form-data; name="file"; filename="storage/emulated/0/test.jpg" Content-Transfer-Encoding: binary 这里是图片文件的二进制数据 --o3Fhj53z-oKToduAElfBaNU4pZhp4---
看到很熟悉吧,这就是我们在文章开头时提到的POST报文格式。没错!HttpEntity就是负责将参数构造成HTTP的报文格式,文本参数该是什么格式、文件该是什么格式,什么类型,这些格式都是固定的。构造完之后,在执行请求时会将http请求的输出流通过writeTo(OutputStream) 函数传递进来,然后将这些参数数据全部输出到http输出流中即可。
明白了这些道理,看看代码也就应该明白了吧。
Volley中实现文件上传
Volley是Google官方推出的网络请求库,这个库很精简、优秀,但是他们也没有默认添加文件上传功能的支持。我们今天就来自定义一个Request实现文件上传功能,还是需要借助上面的MultipartEntity类,下面看代码:
/** * MultipartRequest,返回的结果是String格式的 * @author mrsimple */ public class MultipartRequest extends Request<String> { MultipartEntity mMultiPartEntity = new MultipartEntity(); public MultipartRequest(HttpMethod method, String url, Map<String, String> params, RequestListener<String> listener) { super(method, url, params, listener); } /** * @return */ public MultipartEntity getMultiPartEntity() { return mMultiPartEntity; } @Override public String getBodyContentType() { return mMultiPartEntity.getContentType().getValue(); } @Override public byte[] getBody() { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { // 将mMultiPartEntity中的参数写入到bos中 mMultiPartEntity.writeTo(bos); } catch (IOException e) { Log.e("", "IOException writing to ByteArrayOutputStream"); } return bos.toByteArray(); } @Override protected void deliverResponse(String response) { mListener.onResponse(response); } @Override protected Response<String> parseNetworkResponse(NetworkResponse response) { String parsed; try { parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); } catch (UnsupportedEncodingException e) { parsed = new String(response.data); } return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); } }
使用示例代码:
MultipartRequest multipartRequest = new MultipartRequest(HttpMethod.POST, "http://服务器地址", null, new RequestListener<String>() { @Override public void onStart() { // TODO Auto-generated method stub } @Override public void onComplete(int stCode, String response, String errMsg) { } }); // 获取MultipartEntity对象 MultipartEntity multipartEntity = multipartRequest.getMultiPartEntity(); multipartEntity.addStringPart("content", "hello"); // Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb); // bitmap参数 multipartEntity.addBinaryPart("images", bitmapToBytes(bitmap)); // 文件参数 multipartEntity.addFilePart("images", new File("storage/emulated/0/test.jpg")); // 构建请求队列 RequestQueue queue = RequestQueue.newRequestQueue(Context); // 将请求添加到队列中 queue.addRequest(multipartRequest);
效果图
这是我post到我的应用的截图 :