使用 Volley 和没有 HttpEntity 的工作 POST 多部分请求

Posted

技术标签:

【中文标题】使用 Volley 和没有 HttpEntity 的工作 POST 多部分请求【英文标题】:Working POST Multipart Request with Volley and without HttpEntity 【发布时间】:2015-11-21 07:01:51 【问题描述】:

这不是一个真正的问题,但是,我想在这里分享一些我的工作代码,以供您在需要时参考。

我们知道 HttpEntity 从 API22 中被弃用,并从 API23 中完全删除。目前,我们无法再访问HttpEntity Reference on android Developer (404)。因此,以下是我的 POST Multipart Request with Volley 和不带 HttpEntity 的工作示例代码。它工作正常,用Asp.Net Web API 测试。当然,代码可能只是一个基本示例,发布了两个现有的可绘制文件,也不是所有情况的最佳解决方案,也不是很好的调优。

MultipartActivity.java:

package com.example.multipartvolley;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import com.android.volley.VolleyError;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;


public class MultipartActivity extends Activity 

    private final Context context = this;
    private final String twoHyphens = "--";
    private final String lineEnd = "\r\n";
    private final String boundary = "apiclient-" + System.currentTimeMillis();
    private final String mimeType = "multipart/form-data;boundary=" + boundary;
    private byte[] multipartBody;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multipart);

        byte[] fileData1 = getFileDataFromDrawable(context, R.drawable.ic_action_android);
        byte[] fileData2 = getFileDataFromDrawable(context, R.drawable.ic_action_book);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try 
            // the first file
            buildPart(dos, fileData1, "ic_action_android.png");
            // the second file
            buildPart(dos, fileData2, "ic_action_book.png");
            // send multipart form data necesssary after file data
            dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
            // pass to multipart body
            multipartBody = bos.toByteArray();
         catch (IOException e) 
            e.printStackTrace();
        

        String url = "http://192.168.1.100/api/postfile";
        MultipartRequest multipartRequest = new MultipartRequest(url, null, mimeType, multipartBody, new Response.Listener<NetworkResponse>() 
            @Override
            public void onResponse(NetworkResponse response) 
                Toast.makeText(context, "Upload successfully!", Toast.LENGTH_SHORT).show();
            
        , new Response.ErrorListener() 
            @Override
            public void onErrorResponse(VolleyError error) 
                Toast.makeText(context, "Upload failed!\r\n" + error.toString(), Toast.LENGTH_SHORT).show();
            
        );

        VolleySingleton.getInstance(context).addToRequestQueue(multipartRequest);
    

    @Override
    public boolean onCreateOptionsMenu(Menu menu) 
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_multipart, menu);
        return true;
    

    @Override
    public boolean onOptionsItemSelected(MenuItem item) 
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) 
            return true;
        

        return super.onOptionsItemSelected(item);
    

    private void buildPart(DataOutputStream dataOutputStream, byte[] fileData, String fileName) throws IOException 
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\"; filename=\""
                + fileName + "\"" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);

        ByteArrayInputStream fileInputStream = new ByteArrayInputStream(fileData);
        int bytesAvailable = fileInputStream.available();

        int maxBufferSize = 1024 * 1024;
        int bufferSize = Math.min(bytesAvailable, maxBufferSize);
        byte[] buffer = new byte[bufferSize];

        // read file and write it into form...
        int bytesRead = fileInputStream.read(buffer, 0, bufferSize);

        while (bytesRead > 0) 
            dataOutputStream.write(buffer, 0, bufferSize);
            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            bytesRead = fileInputStream.read(buffer, 0, bufferSize);
        

        dataOutputStream.writeBytes(lineEnd);
    

    private byte[] getFileDataFromDrawable(Context context, int id) 
        Drawable drawable = ContextCompat.getDrawable(context, id);
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    

MultipartRequest.java:

package com.example.multipartvolley;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;

import java.util.Map;

class MultipartRequest extends Request<NetworkResponse> 
    private final Response.Listener<NetworkResponse> mListener;
    private final Response.ErrorListener mErrorListener;
    private final Map<String, String> mHeaders;
    private final String mMimeType;
    private final byte[] mMultipartBody;

    public MultipartRequest(String url, Map<String, String> headers, String mimeType, byte[] multipartBody, Response.Listener<NetworkResponse> listener, Response.ErrorListener errorListener) 
        super(Method.POST, url, errorListener);
        this.mListener = listener;
        this.mErrorListener = errorListener;
        this.mHeaders = headers;
        this.mMimeType = mimeType;
        this.mMultipartBody = multipartBody;
    

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError 
        return (mHeaders != null) ? mHeaders : super.getHeaders();
    

    @Override
    public String getBodyContentType() 
        return mMimeType;
    

    @Override
    public byte[] getBody() throws AuthFailureError 
        return mMultipartBody;
    

    @Override
    protected Response<NetworkResponse> parseNetworkResponse(NetworkResponse response) 
        try 
            return Response.success(
                    response,
                    HttpHeaderParser.parseCacheHeaders(response));
         catch (Exception e) 
            return Response.error(new ParseError(e));
        
    

    @Override
    protected void deliverResponse(NetworkResponse response) 
        mListener.onResponse(response);
    

    @Override
    public void deliverError(VolleyError error) 
        mErrorListener.onErrorResponse(error);
    

更新:

文字部分请参考下方@Oscar 的回答。

【问题讨论】:

我刚刚在following question 复制了@Kevin 评论:有些服务器非常挑剔。如果您有问题,请在“;”之间添加一个空格和 "filename=" 构建 Content-Disposition 和 "multipart/form-data; boundary=" + boundary; :) 如果要添加mimtype:dataOutputStream.writeBytes("Content-Type: image/jpeg" + lineEnd); @MaorHadad:感谢您的评论 :) 感谢您提供这个出色的解决方案。更新到 appcompat 23 后,这个问题在 a** 中很痛苦 亲爱的 BNK 这对上传视频有用吗? 【参考方案1】:

只是想补充一下答案。我试图弄清楚如何将文本字段附加到正文并创建以下函数来做到这一点:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException 
    dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + parameterName + "\"" + lineEnd);
    dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
    dataOutputStream.writeBytes(lineEnd);
    dataOutputStream.writeBytes(parameterValue + lineEnd);

效果很好。

【讨论】:

我已将@BNK 解决方案嵌入到我的应用程序中。我正在我的手机库中选择一张照片并通过多部分表单数据发送它,但它需要几秒钟(~10-15)才能发送到服务器。有没有办法减少这种开销?或任何其他建议? 在弃用 HttpEntity 之后,使用 volley 进行多部分上传变得非常麻烦,加上实现 PATCH 请求是一个令人头疼的问题,所以我最终放弃了 volley,并在我所有的应用程序中都实现了 RetroFit (square.github.io/retrofit)应用。我建议您也这样做,因为 RetroFit 为您提供更好的向后兼容性和未来证明您的应用程序。 我同意@RacZo,但是我更喜欢 OkHttp :),我仍然使用 Volley 来处理其他网络请求。我已经定制了 Google 的 volley 删除 Apache lib,发布到 github.com/ngocchung/volleynoapache,但只测试了 Get、Post 和 Multipart。 我需要使用 Volley 库来使用 PATCH 请求。我怎样才能做到这一点。【参考方案2】:

我重写了你的代码@RacZo 和@BNK,使其更加模块化和易于使用

VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(Request.Method.POST, url, new Response.Listener<NetworkResponse>() 
    @Override
    public void onResponse(NetworkResponse response) 
        String resultResponse = new String(response.data);
        // parse success output
    
, new Response.ErrorListener() 
    @Override
    public void onErrorResponse(VolleyError error)                 
        error.printStackTrace();
    
) 
    @Override
    protected Map<String, String> getParams() 
        Map<String, String> params = new HashMap<>();
        params.put("api_token", "gh659gjhvdyudo973823tt9gvjf7i6ric75r76");
        params.put("name", "Angga");
        params.put("location", "Indonesia");
        params.put("about", "UI/UX Designer");
        params.put("contact", "angga@email.com");
        return params;
    

    @Override
    protected Map<String, DataPart> getByteData() 
        Map<String, DataPart> params = new HashMap<>();
        // file name could found file base or direct access from real path
        // for now just get bitmap data from ImageView
        params.put("avatar", new DataPart("file_avatar.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mAvatarImage.getDrawable()), "image/jpeg"));
        params.put("cover", new DataPart("file_cover.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mCoverImage.getDrawable()), "image/jpeg"));

        return params;
    
;

VolleySingleton.getInstance(getBaseContext()).addToRequestQueue(multipartRequest);

在我的gist 处查看完整的代码VolleyMultipartRequest

【讨论】:

我不想转换成字节或字符串。就我而言,服务器端需要一个文件和多个文本(键值对、多部分表单数据),而不是字节或字符串。这可能吗? 是的,您可以将服务器端的数据视为 Web 表单中的多部分表单数据,实际上代码是修改 http 标头请求以适应和类似的 Web 表单,因此这是您正在寻找的解决方案对于... @AhamadullahSaikat 你得到了任何东西,因为我项目中的同一件事发送多个同名请求 @AnggaAriWijaya 当我上传图像时,从服务器下载图像时显示无效图像。请帮助我卡住了 @Sagar 你检查你服务器上上传的文件吗?它是 0KB 还是丢失了?【参考方案3】:

对于那些正在努力发送 utf-8 参数但仍然没有运气的人,我遇到的问题是在 dataOutputStream 中,并将@RacZo 的代码更改为以下代码:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException 
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"");
        dataOutputStream.write(parameterName.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.write(parameterValue.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
     

【讨论】:

【参考方案4】:

我找到了一个原始 volley 库的包装器,它更容易集成以处理多部分请求。它还支持上传多部分数据以及其他请求参数。因此,我将我的代码分享给未来可能遇到我遇到的问题的开发人员(即使用 volley 和其他一些参数上传多部分数据)。

build.gradle 文件中添加以下库。

dependencies 
    compile 'dev.dworks.libs:volleyplus:+'

请注意,我从build.gradle 中删除了原始的 volley 库,并改用上述库,它可以处理具有类似集成技术的多部分和普通请求。

然后我只需要编写以下处理 POST 请求操作的类。

public class POSTMediasTask 
    public void uploadMedia(final Context context, String filePath) 

        String url = getUrlForPOSTMedia(); // This is a dummy function which returns the POST url for you
        SimpleMultiPartRequest multiPartRequestWithParams = new SimpleMultiPartRequest(Request.Method.POST, url,
                new Response.Listener<String>() 
                    @Override
                    public void onResponse(String response) 
                        Log.d("Response", response);
                        // TODO: Do something on success
                    
                , new Response.ErrorListener() 
            @Override
            public void onErrorResponse(VolleyError error) 
                // TODO: Handle your error here
            
        );

        // Add the file here
        multiPartRequestWithParams.addFile("file", filePath);

        // Add the params here
        multiPartRequestWithParams.addStringParam("param1", "SomeParamValue1");
        multiPartRequestWithParams.addStringParam("param2", "SomeParamValue2");

        RequestQueue queue = Volley.newRequestQueue(context);
        queue.add(multiPartRequestWithParams);
    

现在执行如下任务。

new POSTMediasTask().uploadMedia(context, mediaPath);

您可以使用此库一次上传一个文件。但是,我可以通过启动多个任务来上传多个文件。

希望对您有所帮助!

【讨论】:

【参考方案5】:

这是一个 Kotlin 版本的类,它允许使用 Volley 1.1.1 进行多部分请求。

它主要基于@BNK 的解决方案,但略微简化。我没有注意到任何特定的性能问题。我在大约 3 秒内上传了一张 5Mb 的图片。

class MultipartWebservice(context: Context) 

    private var queue: RequestQueue? = null

    private val boundary = "apiclient-" + System.currentTimeMillis()
    private val mimeType = "multipart/form-data;boundary=$boundary"

    init 
        queue = Volley.newRequestQueue(context)
    

    fun sendMultipartRequest(
        method: Int,
        url: String,
        fileData: ByteArray,
        fileName: String,
        listener: Response.Listener<NetworkResponse>,
        errorListener: Response.ErrorListener
    ) 

        // Create multi part byte array
        val bos = ByteArrayOutputStream()
        val dos = DataOutputStream(bos)
        buildMultipartContent(dos, fileData, fileName)
        val multipartBody = bos.toByteArray()

        // Request header, if needed
        val headers = HashMap<String, String>()
        headers["API-TOKEN"] = "458e126682d577c97d225bbd73a75b5989f65e977b6d8d4b2267537019ad9d20"

        val request = MultipartRequest(
            method,
            url,
            errorListener,
            listener,
            headers,
            mimeType,
            multipartBody
        )

        queue?.add(request)

    

    @Throws(IOException::class)
    private fun buildMultipartContent(dos: DataOutputStream, fileData: ByteArray, fileName: String) 

        val twoHyphens = "--"
        val lineEnd = "\r\n"

        dos.writeBytes(twoHyphens + boundary + lineEnd)
        dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"$fileName\"$lineEnd")
        dos.writeBytes(lineEnd)
        dos.write(fileData)
        dos.writeBytes(lineEnd)
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
    

    class MultipartRequest(
        method: Int,
        url: String,
        errorListener: Response.ErrorListener?,
        private var listener: Response.Listener<NetworkResponse>,
        private var headers: MutableMap<String, String>,
        private var mimeType: String,
        private var multipartBody: ByteArray
    ) : Request<NetworkResponse>(method, url, errorListener) 

        override fun getHeaders(): MutableMap<String, String> 
            return if (headers.isEmpty()) super.getHeaders() else headers
        

        override fun getBodyContentType(): String 
            return mimeType
        

        override fun getBody(): ByteArray 
            return multipartBody
        

        override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse> 
            return try 
                Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
             catch (e: Exception) 
                Response.error(ParseError(e))
            
        

        override fun deliverResponse(response: NetworkResponse?) 
            listener.onResponse(response)
        
    

【讨论】:

嗨。关于我们如何使用上述类的更多信息?例如,我们如何上传 .jpg 图片? 只要确保我了解它是如何工作的。这会创建一个名为fileName 的临时文件,还是上传名为fileName 的文件? 仍然无法让它工作。据我所知,这会将整个内容分成 1 块发送——而不是像我预期的那样发送多块。我的服务器不能接受大件,这就是我尝试使用多部分 POST 请求的原因。您能否提供更多信息或示例?【参考方案6】:

这里是用于 Multipart Volley POST 请求的非常简化的 Kotlin 版本

    private fun saveProfileAccount(concern: String, selectedDate: String, WDH: String, details: String, fileExtention: String) 

    val multipartRequest: VolleyMultipartRequest =
        object : VolleyMultipartRequest(
            Request.Method.POST, APIURLS.WhistleBlower,
            Response.Listener<JSONObject>  response ->

            ,
            Response.ErrorListener  error ->

                error.printStackTrace()
            ) 

            @Throws(AuthFailureError::class)
            override fun getHeaders(): MutableMap<String, String>
                val params: MutableMap<String, String> = HashMap()
                params[ConstantValues.XAppApiKey] = APIURLS.XAppApiValue
                return params
            

            override fun getParams(): Map<String, String>? 
                val params: MutableMap<String, String> = HashMap()
                val sharedPreferences: SharedPreferences = requireActivity().getSharedPreferences(Prefs.PREF_NAME, 0)
                val userId = Prefs.getStringPref(sharedPreferences, Prefs.USER_ID)
                val userName = Prefs.getStringPref(sharedPreferences, Prefs.FULL_NAME)

                params["PersonId"] = userId.toString()
                params["PersonName"] = userName.toString()
                params["Concern"] = concern
                params["DateOfHappen"] = selectedDate
                params["WhereHappened"] = WDH
                params["Ext"] = fileExtention
                

                return params
            


            override fun getByteData(): Map<String, DataPart>? 
                val params: MutableMap<String, DataPart> = HashMap()
                // file name could found file base or direct access from real path
                // for now just get bitmap data from ImageView

                params["cover"] = DataPart(
                    "sd.pdf",
                    byteArray,
                    "doc/pdf"
                )
                return params
            
        

    AppSingleton.getInstance(requireContext())?.addToRequestQueue(multipartRequest)

【讨论】:

以上是关于使用 Volley 和没有 HttpEntity 的工作 POST 多部分请求的主要内容,如果未能解决你的问题,请参考以下文章

实习HttpEntity工具类用法-源码阅读

volley2--volley的使用和架构

Volley的初步了解

使用Volley上传头像图片

Spring MVC:啥是 HttpEntity? [复制]

多次读取HttpEntity内容