前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于Http原理实现Android的图片上传和表单提交

基于Http原理实现Android的图片上传和表单提交

原创
作者头像
张坤
修改2017-06-19 19:26:55
5.6K0
修改2017-06-19 19:26:55
举报
文章被收录于专栏:张坤的专栏

现在服务器主要是Web居多,客户端一般通过http上传文件到web服务器,最开始的设想很简单,直接将图片转化为字节流,写入到http的outstream,随后发送出去即可。

但当这种方法出现问题,服务器根据文件名这个表单中的字段来判定是否接收到文件,我上面那种简单的方法从而使得每次服务器反馈说没有接收到图片文件,从而发送失败。由此推断是表单传输出了问题,Android由于历史原因,有很多表单传输的方法。当前官方推荐的是HttpURLConnection,但是利用HttpURLConnection构建表单的方式,没有成型的form封装方法。比如对于C#的表单提交,简简单单几句话搞定:

代码语言:javascript
复制
        WWWForm form = new WWWForm();
        form.AddField("frameCount", Time.frameCount.ToString());
        form.AddBinaryData("fileUpload", bytes);

        // Upload to a cgi script
        WWW w = new WWW("http://localhost/cgi-bin/env.cgi?post", form);

Java的HttpURLConnection没有这么简单的封装形式,需要完整的请求体模拟,用起来相对不方便,不过这样能够对单提交的本质原理有更加清晰的理解。

web端demo

在Android端上传图片总是失败的情况下,后台开发哥们帮忙实现了web端的请求demo,是可以正常处理请求的,页面如下:

选择文件之后,按浏览器的F12,便可出现开发者工具界面,在Network一栏可以看到具体的请求和响应, 分析其请求头和请求体,来构造Android中相同的参数,就可以实现文件的正常上传。 下面就根据web端的请求demo来模拟实现Android的post提交方法。

Http请求头分析

首先来看请求的消息头:

代码语言:javascript
复制
Accept:*/*
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8,en;q=0.6
Content-Length:38275
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryCjkbdjmUGD4QJISL
Host:118.69.25.90
Origin:http://118.69.25.90
Proxy-Connection:keep-alive
Referer:http://118.69.25.90/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36
Request Payload

根据请求头,去构建Android的HttpURLConnection相关参数:

代码语言:javascript
复制
URL url = new URL("http://118.69.25.90/uploadpicture.php");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setUseCaches(true);
// 启动post方法
connection.setRequestMethod("POST");
// 设置请求头内容
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=----WebKitFormBoundaryCjkbdjmUGD4QJISL");
Http请求体分析

下面来分析消息体(Request Payload),内容如下:

代码语言:javascript
复制
------boundary=----WebKitFormBoundaryCjkbdjmUGD4QJISL
Content-Disposition: form-data; name="file"; filename="ouba.jpg"
Content-Type: image/jpeg

ÿØÿà
2!!22222222222222222222222222222222222222222222222222ÿÂ
yÛ"{qX½?WóþÔ¾ÅæÑÏRiÙ'˺hÍ’Ÿ>¡&@˜AÈ"L"æҲđ`z•«©Ä[x¯eu”k1ÑÏ–³¶¬‚næZYT4H¶‹ ´tp؃•Rô)ÁÕ1åêå2«ee—fŒ]¬¢kd«’ú~ï’õøÔ™$Ò-°¢$IË“—>ÊNw1S
…………………………………………
Ü‚-Èw1…ª(,RÅ·IȪ•‘Z~Yôë7U<»ÄçV‹+V3.¬ÛR‹cBËF=…™n²Zò[*ÇqEÇCgÌë«Ž™µaCMj¼ÉÛçNÙ®—´ù¿²šôí´C
­¦"CÃm>Òj5…§Ñ*ÙWvĪÙúÜÉ?K),GŽ½)Ì,Xj‰@gªˆAMêrªÙe ’Ô7—Ý_´3à^ƒÔÿÙ
------WebKitFormBoundaryCjkbdjmUGD4QJISL--

从消息体内容可以看出,请求消息体本质上就是字节数组或字符串。内容主要分为三部分:

1. 开始和结束字段 开始和结束都有明确的字段 boudary字段的具体内容是由消息头中Content-Type字段进行定义的:

代码语言:javascript
复制
Content-Type:multipart/form-data; 
boundary=----WebKitFormBoundaryCjkbdjmUGD4QJISL

这里面设置的boundary和消息体中的boundary必须保持完全一致,才可以确保消息能够得到服务端的正常解析。

2. 表单信息 包含Content-Disposition、name、filename和Content-Type等四个表单变量,必须要填写正确的字段,web服务器才可以对相关变量进行正确解析

3. 图片 payload中的乱码数据,就是文件的二进制表示了

4. 换行回车\r\n 所以Java构造payload的原理,就是按照这种顺序和特定的字段,进行模拟即可,java代码如下:

代码语言:javascript
复制
DataOutputStreamdos = new DataOutputStream(connection.getOutputStream());
FileInputStream fin = new FileInputStream(filePath);
File file = new File(filePath);
dos.writeBytes("------boundary=----WebKitFormBoundaryCjkbdjmUGD4QJISL");
dos.writeBytes("\r\n");
dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename="+file.getName());
dos.writeBytes("\r\n");
dos.writeBytes("Content-Type: image/jpeg");
dos.writeBytes("\r\n");
dos.writeBytes("\r\n");     

// 取得本地图片的字节流,向url流中写入图片字节流
bytesAvailable = fin.available();
bufferSize = Math.min(bytesAvailable, maxBufferSize);
buffer = new byte[bufferSize];

bytesRead = fin.read(buffer, 0, bufferSize);
while (bytesRead > 0) {
    dos.write(buffer, 0, bufferSize);
    bytesAvailable = fin.available();
    bufferSize = Math.min(bytesAvailable, maxBufferSize);
    bytesRead = fin.read(buffer, 0, bufferSize);
}
dos.writeBytes("\r\n"); 
dos.writeBytes("------WebKitFormBoundaryCjkbdjmUGD4QJISL--");
dos.writeBytes("\r\n"); 
dos.writeBytes("\r\n");
完整上传图片的java代码
代码语言:javascript
复制
package com.youreye.tts.qq;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class UploadPicture {

    String serverUrl = "http://118.89.25.65/upload-and-detect.php";

    HttpURLConnection connection = null;
    DataOutputStream dos = null;

    int bytesAvailable, bufferSize, bytesRead;
    int maxBufferSize = 1 * 1024 * 512;
    byte[] buffer = null;

    String boundary = "-----------------------------1954231646874";
    Map<String, String> formParams = new HashMap<String, String>();

    FileInputStream fin = null;

    // 对包含中文的字符串进行转码,此为UTF-8。服务器那边要进行一次解码
    private String encode(String value) throws Exception {
        return URLEncoder.encode(value, "UTF-8");
    }

    public String uploadPicToWebServer(String filePath) {

        try {
            URL url = new URL(serverUrl);
            connection = (HttpURLConnection) url.openConnection();

            // 允许向url流中读写数据
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setUseCaches(true);

            // 启动post方法
            connection.setRequestMethod("POST");

            // 设置请求头内容
            connection.setRequestProperty("connection", "Keep-Alive");
            connection
                    .setRequestProperty("Content-Type", "multipart/form-data; boundary=---------------------------1954231646874");

            dos = new DataOutputStream(connection.getOutputStream());
            fin = new FileInputStream(filePath);

            File file = new File(filePath);
            dos.writeBytes(boundary);
            dos.writeBytes("\r\n");
            dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename="+file.getName());
            dos.writeBytes("\r\n");
            dos.writeBytes("Content-Type: image/jpeg");
            dos.writeBytes("\r\n");
            dos.writeBytes("\r\n");     

            // 取得本地图片的字节流,向url流中写入图片字节流
            bytesAvailable = fin.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            buffer = new byte[bufferSize];

            bytesRead = fin.read(buffer, 0, bufferSize);
            while (bytesRead > 0) {
                dos.write(buffer, 0, bufferSize);
                bytesAvailable = fin.available();
                bufferSize = Math.min(bytesAvailable, maxBufferSize);
                bytesRead = fin.read(buffer, 0, bufferSize);
            }           
            dos.writeBytes("\r\n"); 
            dos.writeBytes("-----------------------------1954231646874--");
            dos.writeBytes("\r\n"); 
            dos.writeBytes("\r\n"); 

            // Server端返回的信息
            int code = connection.getResponseCode();
            if (code == 200) {
                InputStream inStream = connection.getInputStream();
                ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int len = -1;
                while ((len = inStream.read(buffer)) != -1) {
                    outSteam.write(buffer, 0, len);
                }

                outSteam.close();
                inStream.close();
                return new String(outSteam.toByteArray());
            }

            if (dos != null) {
                dos.flush();
                dos.close();
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}
遇到的主要的坑:

这个问题花了五个小时时间,花费时间长主要原因如下:

  1. Android的多种表单提交方案 有HttpClient、httpmine.jar和HttpURLConnection,前两种方案,官方已不在推荐,而且很容易出现版本兼容性问题。所以需要采用HttpURLConnection,但是这种方案没有成型的表单提交接口,所以在上传图片时,服务器对表单解析很容易出问题。
  2. chrome的F12工具,requestload中的图片内容看不到,影响了对图片http上传的理解。 最后采用Firefox浏览器来分析请求协议:

图片中requestload的内容一目了然,所以就知道如何去构造图片+表单提交的request内容了,所以这次非常感谢FireFox这种强大的工具,帮忙定位核心问题。

最终总结:

Android由于凝聚的开发者众多,很多问题都在网上有着成熟的解决方案,很快的利用网上方案就可以实现快速验证和功能的快速编写,但从另一个角度,这种编程习惯也会降低开发者的编程能力。 从这次文件传输的调试过程中,自己也越发发现从根本原理来分析问题,才是追溯问题本质,提升对知识原理的理解深度的最佳途径。

附注:本文是参加腾讯云马拉松创意大赛,我们组作品的一个技术解决方案,源码链接

相关推荐

Android动态库压缩壳的实现
HTTP/2探索第一篇——概念
深度了解Android 7.0 ,你准备好了吗?

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • web端demo
    • Http请求头分析
      • Http请求体分析
        • 完整上传图片的java代码
          • 遇到的主要的坑:
            • 最终总结:
              • Android动态库压缩壳的实现
                • HTTP/2探索第一篇——概念
                  • 深度了解Android 7.0 ,你准备好了吗?
                  相关产品与服务
                  云服务器
                  云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档