天天看點

node for android,Android+Node實作檔案上傳

寫在前面

上傳檔案時一般還需要附加一些額外的資訊,比如userid等與檔案密切相關而服務端無法直接擷取的資料,是以multipart/form-data是比較靈活的選擇,可以通過表單同時送出檔案及其相關使用者資料

一.Android用戶端

1.包裝http通信過程

首先我們不希望上傳檔案的過程影響UI更新,這裡以AsyncTask的方式來實作:

public class HttpUpload extends AsyncTask {

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); // 相當于

// 填充字段

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')));

三.項目位址

P.S.話說github首頁空空如也,有點說不過去了,接下來的這段時間盡量放點東西上去吧。工程師事業指南之一就是積累作品集(portfolio),到現在也沒有理由偷懶了,cheer up, ready to work

參考資料

本文fork自該項目,但原項目年久失修(Latest commit 518c106 on 19 Nov 2012),筆者做了修繕

更成熟更受歡迎的android上傳元件,提供node和php的服務端實作,當然,也相對龐大