fly1tkg blog

Volleyでmultipart/form-dataを送信する

はじめに書きますが、Volleyは画像ファイル等の大きいファイルを送ることには適していません。が、ちょっとファイルを送りたいときに、そこだけapacheのhttpclient等を使うのも微妙な気がするので、このエントリーではVolleyを使ったmultipart/form-dataの送り方を紹介します。

はじめに

multipart/form-dataを利用するために最新のhttpcoreとhttpmineを利用するのがよいですhttp://hc.apache.org/downloads.cgi から最新のHttpclientをダウンロードしてlibsのhttpcore.jarとhttpmine.jarを利用したり、maven、gradle経由でアプリに組み込んでください。

シンプルな実装

http://stackoverflow.com/questions/16797468/how-to-send-a-multipart-form-data-post-in-android-with-volley

上記のリンクのようにmultipart/form-data用のリクエストを作成すると割りと簡単に実装できます。ポイントはgetBodyのメソッドでmultipart/form-data形式のバイナリをVolleyのRequestに渡す設計になっている所だと思います。

もう少し実用的な設計にすると以下のようになります。第4引数と第5引数にはそれぞれkey-valueで送信するリクエストを渡します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class MultipartRequest extends Request<String> {

    private MultipartEntity entity = new MultipartEntity();

    private final Response.Listener<String> mListener;
    private final Map<String, String> mStringParts;
    private final Map<String, File> mFileParts;

    public MultipartRequest(String url, Response.Listener<String> listener,
            Response.ErrorListener errorListener,
            Map<String, String> stringParts, Map<String, File> fileParts) {
        super(Method.POST, url, errorListener);

        mListener = listener;
        mStringParts = stringParts;
        mFileParts = fileParts;
        buildMultipartEntity();
    }

    private void buildMultipartEntity() {
        for (Map.Entry<String, String> entry : mStringParts.entrySet()) {
            try {
                entity.addPart(entry.getKey(), new StringBody(entry.getValue()));
            } catch (UnsupportedEncodingException e) {
                VolleyLog.e("UnsupportedEncodingException");
            }
        }

        for (Map.Entry<String, File> entry : mFileParts.entrySet()) {
            entity.addPart(entry.getKey(), new FileBody(entry.getValue()));
        }
    }

    @Override
    public String getBodyContentType() {
        return entity.getContentType().getValue();
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            entity.writeTo(bos);
        } catch (IOException e) {
            VolleyLog.e("IOException writing to ByteArrayOutputStream");
        }
        return bos.toByteArray();
    }

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        return Response.success("Uploaded", getCacheEntry());
    }

    @Override
    protected void deliverResponse(String response) {
        mListener.onResponse(response);
    }
}

独自のHurlStackと共に利用する

先のシンプルな実装ではgetBodyでOOM(Out of memory)が発生する場合があります。というのも送信するファイルサイズが大きい場合Multipart entityをByteArrayInputStreamに書き込む処理が問題になるからです。簡単に言うとここで送信したいデータ分のメモリが確保できなければ、メモリ不足で強制終了するというわけです。

Volleyの通信の処理はHurlStackというクラスにかかれています。VolleyのRequest queueを作成する際に、独自のHurlStackを渡すことができるようになっているのを利用して、Multipart/form-dataの時はファイルをストリームで渡せるようにカスタマイズしたHurlStackを渡して、OOMを防ぎます。

MultipartRequestクラス、ここでMultipart entityを外部へ渡せるようにしておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class MultipartRequest extends Request<String> {

    private MultipartEntity entity = new MultipartEntity();

    private final Response.Listener<String> mListener;
    private final Map<String, String> mStringParts;
    private final Map<String, File> mFileParts;

    public MultipartRequest(String url, Response.Listener<String> listener,
            Response.ErrorListener errorListener,
            Map<String, String> stringParts, Map<String, File> fileParts) {
        super(Method.POST, url, errorListener);

        mListener = listener;
        mStringParts = stringParts;
        mFileParts = fileParts;
        buildMultipartEntity();
    }

    private void buildMultipartEntity() {
        for (Map.Entry<String, String> entry : mStringParts.entrySet()) {
            try {
                entity.addPart(entry.getKey(), new StringBody(entry.getValue()));
            } catch (UnsupportedEncodingException e) {
                VolleyLog.e("UnsupportedEncodingException");
            }
        }

        for (Map.Entry<String, File> entry : mFileParts.entrySet()) {
            entity.addPart(entry.getKey(), new FileBody(entry.getValue()));
        }
    }

    @Override
    public String getBodyContentType() {
        return entity.getContentType().getValue();
    }

    public MultipartEntity getEntity() {
        return entity;
    }

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        return Response.success("Uploaded", getCacheEntry());
    }

    @Override
    protected void deliverResponse(String response) {
        mListener.onResponse(response);
    }
}

独自のHurlStack、MultiPartRequestの時だけ独自の通信処理を利用するようにします

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MultiPartStack extends HurlStack {
    private final static String HEADER_CONTENT_TYPE = "Content-Type";

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws AuthFailureError, IOException {
        if (!(request instanceof MultipartRequest)) {
            return super.performRequest(request, additionalHeaders);
        }
        HttpPost httpRequest = new HttpPost(request.getUrl());
        httpRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
        setMultiPartBody(httpRequest, request);
        addHeaders(httpRequest, additionalHeaders);
        addHeaders(httpRequest, request.getHeaders());
        HttpParams httpParams = httpRequest.getParams();
        int timeoutMs = request.getTimeoutMs();
        HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
        HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);

        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", new PlainSocketFactory(), 80));
        registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

        ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(httpParams, registry);
        HttpClient httpClient = new DefaultHttpClient(manager, httpParams);

        return httpClient.execute(httpRequest);
    }

    private void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
        for (String key : headers.keySet()) {
            httpRequest.setHeader(key, headers.get(key));
        }
    }

    private static void setMultiPartBody(HttpEntityEnclosingRequestBase httpRequest,
            Request<?> request) throws AuthFailureError {
        if (request instanceof MultipartRequest) {
            httpRequest.setEntity(((MultipartRequest) request).getEntity());
        }
    }
}

このMultiPartStackを利用したRequestQueueは以下のように利用できます

1
RequestQueue queue = Volley.newRequestQueue(context, new MultiPartStack());

以上のようなコードで大きいファイルの送信もOOMになることなくできます。

補足: multipart/form-dataとは?

http://d.hatena.ne.jp/satox/20110726/1311665904

上記のリンク等が参考になるのですが、基本的にはHeaderでboundaryと呼ばれる仕切り文字列を指定して、Bodyで送信したいデータをboundaryで区切ってまとめて送信してしまう通信方法です。もっといい仕様はなかったのか、、、