写在前面
上传文件时一般还需要附加一些额外的信息,比如userid等与文件密切相关而服务端无法直接获取的数据,所以multipart/form-data
是比较灵活的选择,可以通过表单同时提交文件及其相关用户数据
一.Android客户端
1.包装http通信过程
首先我们不希望上传文件的过程影响UI更新,这里以AsyncTask
的方式来实现:
public class HttpUpload extends AsyncTask<Void, Integer, Void> {
public HttpUpload(Context context, String filePath) {
super();
this.context = context; // 用于更新ui(显示进度)
this.filePath = filePath;
}
@Override
protected void onPreExecute() {
// 设置client参数
// 显示进度条/对话框
}
@Override
protected Void doInBackground(Void... params) {
// 创建post请求
// 填充表单字段及值
// 监听进度
// 发送post请求
// 处理响应结果
}
@Override
protected void onProgressUpdate(Integer... progress) {
// 更新进度
}
@Override
protected void onPostExecute(Void result) {
// 隐藏进度
}
}
2.实现http通信
http通信可以通过Apache HttpClient来实现,如下:
//--- onPreExecute
// 设置client参数
int timeout = 10000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeout);
HttpConnectionParams.setSoTimeout(httpParameters, timeout);
// 创建client
client = new DefaultHttpClient(httpParameters);
// 创建并显示进度
System.out.println("upload start");
//---doInBackground
try {
File file = new File(filePath);
// 创建post请求
HttpPost post = new HttpPost(url);
// 创建multipart报文体
// 并监听进度
MultipartEntity entity = new MyMultipartEntity(new ProgressListener() {
@Override
public void transferred(long num) {
// Call the onProgressUpdate method with the percent
// completed
// publishProgress((int) ((num / (float) totalSize) * 100));
System.out.println(num + " - " + totalSize);
}
});
// 填充文件(可以有多个文件)
ContentBody cbFile = new FileBody(file, "image/png");
entity.addPart("source", cbFile); // 相当于<input type="file" name="source">
// 填充字段
entity.addPart("userid", new StringBody("u30018512", Charset.forName("UTF-8")));
entity.addPart("username", new StringBody("中文不乱码", Charset.forName("UTF-8")));
// 初始化报文体总长度(用来描述进度)
int totalSize = entity.getContentLength();
// 设置post请求的报文体
post.setEntity(entity);
// 发送post请求
HttpResponse response = client.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
// 返回200
String fullRes = EntityUtils.toString(response.getEntity());
System.out.println("OK: " + fullRes);
} else {
// 其它错误状态码
System.out.println("Error: " + statusCode);
}
} catch (ClientProtocolException e) {
// Any error related to the Http Protocol (e.g. malformed url)
e.printStackTrace();
} catch (IOException e) {
// Any IO error (e.g. File not found)
e.printStackTrace();
}
//--- onProgressUpdate
// 更新进度
//--- onPostExecute
// 隐藏进度
3.记录进度
Apache HttpClient
本身不提供进度数据,此处需要自行实现,重写MultipartEntity:
public class MyMultipartEntity extends MultipartEntity {
private final ProgressListener listener;
public MyMultipartEntity(final ProgressListener listener) {
super();
this.listener = listener;
}
public MyMultipartEntity(final HttpMultipartMode mode, final ProgressListener listener) {
super(mode);
this.listener = listener;
}
public MyMultipartEntity(HttpMultipartMode mode, final String boundary, final Charset charset,
final ProgressListener listener) {
super(mode, boundary, charset);
this.listener = listener;
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
super.writeTo(new CountingOutputStream(outstream, this.listener));
}
public static interface ProgressListener {
void transferred(long num);
}
public static class CountingOutputStream extends FilterOutputStream {
private final ProgressListener listener;
private long transferred;
public CountingOutputStream(final OutputStream out, final ProgressListener listener) {
super(out);
this.listener = listener;
this.transferred = 0;
}
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
this.transferred += len;
this.listener.transferred(this.transferred);
}
public void write(int b) throws IOException {
out.write(b);
this.transferred++;
this.listener.transferred(this.transferred);
}
}
}
内部通过重写FilterOutputStream
支持计数,得到进度数据
4.调用HttpUpload
在需要的地方调用new HttpUpload(this, filePath).execute();
即可,和一般的AsyncTask
没什么区别
二.Node服务端
1.搭建服务器
express
快速搭建http服务器,如下:
var app = express();
// ...路由控制
app.listen(3000);
最简单的方式当然是经典helloworld中的http.createServer().listen(3000)
,此处使用express
主要是为了简化路由控制和中间件管理,express
提供了成熟的路由控制和中间件管理机制,自己写的话。。还是有点麻烦
2.multipart请求预处理
请求预处理是中间件的本职工作,这里采用Connect中间件:connect-multiparty
// 中间件预处理
app.use('/upload', require('connect-multiparty')());
// 处理multipart post请求
app.post('/upload', function(req, res){
console.log(req.body.userid);
console.log(req.body.username);
console.log('Received file:\n' + JSON.stringify(req.files));
var imageDir = path.join(__dirname, 'images');
var imagePath = path.join(imageDir, req.files.source.name);
// if exists
fs.stat(imagePath, function(err, stat) {
if (err && err.code !== 'ENOENT') {
res.writeHead(500);
res.end('fs.stat() error');
}
else {
// already exists, gen a new name
if (stat && stat.isFile()) {
imagePath = path.join(imageDir, new Date().getTime() + req.files.source.name);
}
// rename
fs.rename(
req.files.source.path,
imagePath,
function(err){
if(err !== null){
console.log(err);
res.send({error: 'Server Writting Failed'});
} else {
res.send('ok');
}
}
);
}
});
});
此处只是简单的rename
把图片放到目标路径,更复杂的操作,比如创建缩略图,裁剪,合成(水印)等等,可以通过相关开源模块完成,比如imagemagick
connect-multiparty
拿到req
后先流式接收所有文件到系统临时文件夹(当然,接收路径可以通过uploadDir属性设置,还可以设置限制参数maxFields、maxFieldsSize等等,详细用法请查看andrewrk/node-multiparty),同时解析表单字段,最后把处理结果挂在req
对象上(req.body
是表单字段及值组成的对象, req.files
是临时文件对象组成的对象),例如:
u30018512
中文不乱码
Received file:
{"source":{"fieldName":"source","originalFilename":"activity.png","path":"C:\\Us
ers\\ay\\AppData\\Local\\Temp\\KjgxW_Rmz8XL1er1yIVhEqU9.png","headers":{"content
-disposition":"form-data; name=\"source\"; filename=\"activity.png\"","content-t
ype":"image/png","content-transfer-encoding":"binary"},"size":66148,"name":"acti
vity.png","type":"image/png"}}
拿到临时文件路径后再rename
就把文件放到服务器目标位置了
注意:Windows下临时文件夹为C:/Users/[username]/AppData/Local/Temp/
,如果目标路径不在C盘会报错(跨盘符会导致rename错误),如下:
{ [Error: EXDEV, rename 'C:\Users\ay\AppData\Local\Temp\KjgxW_Rmz8XL1er1yIVhEqU9
.png']
errno: -4037,
code: 'EXDEV',
path: 'C:\\Users\\ay\\AppData\\Local\\Temp\\KjgxW_Rmz8XL1er1yIVhEqU9.png' }
至于解决方案,不建议用Windows鼓捣node,会遇到各种奇奇怪怪的问题,徒增烦扰,况且vbox装个虚拟机也不麻烦
3.响应静态文件
文件上传之后,如果是可供访问的(比如图片),还需要响应静态文件,express
直接配置静态目录就好了,省好多事,如下:
// static files
app.use('/images', express.static(path.join(__dirname, 'images')));
三.项目地址
GitHub: ayqy/MultipartUpload
P.S.话说github主页空空如也,有点说不过去了,接下来的这段时间尽量放点东西上去吧。工程师事业指南之一就是积累作品集(portfolio),到现在也没有理由偷懒了,cheer up, ready to work
参考资料
danysantiago/NodeJS-Android-Photo-Upload
本文fork自该项目,但原项目年久失修(Latest commit 518c106 on 19 Nov 2012),笔者做了修缮
-
更成熟更受欢迎的android上传组件,提供node和php的服务端实现,当然,也相对庞大