在开发当中,我们常常需要实现文件上传,比较常见的就是图片上传,比如修改个头像什么的。但是这个功能在Android和iOS中都没有默认的实现类,对于Android我们可以使用Apache提供的HttpClient.jar来实现这个功能,其中依赖的类就是Apache的httpmime.jar中的MultipartEntity这个类。我就是要实现一个文件上传功能,但是我还得下载一个jar包,而这个jar包几十KB,这尼玛仿佛并非人间!今天我们就来自己实现文件上传功能,并且弄懂它们的原理。
在上一篇文章HTTP POST请求报文格式分析与Java实现文件上传中我们介绍了HTTP POST报文格式,如果有对POST报文格式不了解的同学可以先阅读这篇文章。
我们知道,使用网络协议传输数据无非就是要遵循某个协议,我们在开发移动应用时基本上都是使用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());
}
}
writeTo(final OutputStream outstream)
方法将所有参数的字节流数据写入到与服务器建立的TCP连接的输出流中,这样就将我们的参数传递给服务器了。当然在此之前,我们需要按照格式来向ByteArrayOutputStream对象中写数据。
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) ;
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---
/**
* @author mrsimple
*/
public class MultipartRequest extends Request {
MultipartEntity mMultiPartEntity = new MultipartEntity();
Map mHeaders = new HashMap();
private final Listener mListener;
/**
* Creates a new request with the given url.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
*/
public MultipartRequest(String url, Listener listener) {
this(url, listener, null);
}
/**
* Creates a new POST request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public MultipartRequest(String url, Listener listener, ErrorListener errorListener) {
super(Method.POST, url, errorListener);
mListener = listener;
}
/**
* @return
*/
public MultipartEntity getMultiPartEntity() {
return mMultiPartEntity;
}
@Override
public String getBodyContentType() {
return mMultiPartEntity.getContentType().getValue();
}
public void addHeader(String key, String value) {
mHeaders.put(key, value);
}
@Override
public Map getHeaders() throws AuthFailureError {
return mHeaders;
}
@Override
public byte[] getBody() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
// multipart body
mMultiPartEntity.writeTo(bos);
} catch (IOException e) {
Log.e("", "IOException writing to ByteArrayOutputStream");
}
return bos.toByteArray();
}
@Override
protected Response 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));
}
@Override
protected void deliverResponse(String response) {
if (mListener != null) {
mListener.onResponse(response);
}
}
}
RequestQueue queue = Volley.newRequestQueue(this);
MultipartRequest multipartRequest = new MultipartRequest(
"http://yourhost.com", new Listener() {
@Override
public void onResponse(String response) {
Log.e("", "### response : " + response);
}
});
// 添加header
multipartRequest.addHeader("header-name", "value");
// 通过MultipartEntity来设置参数
MultipartEntity multi = multipartRequest.getMultiPartEntity();
// 文本参数
multi.addStringPart("location", "模拟的地理位置");
multi.addStringPart("type", "0");
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
// 直接从上传Bitmap
multi.addBinaryPart("images", bitmapToBytes(bitmap));
// 上传文件
multi.addFilePart("imgfile", new File("storage/emulated/0/test.jpg"));
// 将请求添加到队列中
queue.add(multipartRequest);