文档章节

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

偶素浅小浅
 偶素浅小浅
发布于 2017/01/09 19:54
字数 1786
阅读 42
收藏 1

版权声明:本文由张坤   原创文章,转载请注明出处: 
文章原文链接:https://www.qcloud.com/community/article/794875001483009140

来源:腾云阁 https://www.qcloud.com/community

 

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

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

        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请求头分析

首先来看请求的消息头:

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相关参数:

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),内容如下:

------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字段进行定义的:

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代码如下:

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代码

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

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

本文转载自:

偶素浅小浅
粉丝 8
博文 202
码字总数 0
作品 0
信阳
私信 提问
Android 网络框架 Retrofit

概述 Retrofit是一个OkHttp网络请求框架的封装库,Retrofit通过注解配置网络参数,可以按照我们的规则去构造实际的HTTP请求,能够灵活设置URL、头部、请求体、返回值等,是目前最优雅的一个网...

xi阳
2018/09/11
0
0
Android HttpClient上传文件与Httpconnection知识小结

Android上传文件到服务端可以使用HttpConnection 上传文件,也可以使用Android封装好的HttpClient类。当仅仅上传文件可以直接使用httpconnection 上传比较方便快捷。 1、使用HttpConection上...

安克诚
2012/07/11
550
0
《JavaScript实用效果整理》系列分享专栏

整理一些使用的JavaScript效果,在Web开发中遇到的比较好的动态效果,都收藏在这里,对以后的网站开发增加不少的色彩 《JavaScript实用效果整理》已整理成PDF文档,点击可直接下载至本地查阅...

开元中国2015
2018/10/29
63
0
Rexsee 上线应用生成工具,无需编程创建 Android 应用

Rexsee开放其在线应用生成工具,提供6类应用模板,进一步降低Android应用实现难度。普通用户无需编写任何代码,也能轻松打造个性化移动应用。 访问www.apk.rexsee.com,即可免费使用: 图片集...

yejiang
2012/06/12
4.5K
6
uni-app系统目录文件上传(非只图片和视频)解决方案

背景 公司领导提出这样的产品需求:需要上传目录文件,不只是图片和视频,而且同时要支持Android和IOS两大移动端。另外公司App的架构采用的是uni-app。 思考 第一个想到的方案就是,看uni-a...

silianpan
09/22
0
0

没有更多内容

加载失败,请刷新页面

加载更多

3_数组

3_数组

行者终成事
32分钟前
3
0
经典系统设计面试题解析:如何设计TinyURL(二)

原文链接:https://www.educative.io/courses/grokking-the-system-design-interview/m2ygV4E81AR 编者注:本文以一道经典的系统设计面试题:《如何设计TinyURL》的参考答案和解析为例,帮助...

APEMESH
今天
7
0
使用logstash同步MySQL数据到ES

概述   在生成业务常有将MySQL数据同步到ES的需求,如果需要很高的定制化,往往需要开发同步程序用于处理数据。但没有特殊业务需求,官方提供的logstash就很有优势了。   在使用logstas...

zxiaofan666
今天
10
0
X-MSG-IM-分布式信令跟踪能力

经过一周多的鏖战, X-MSG-IM的分布式信令跟踪能力已基本具备, 特点是: 实时. 只有要RX/TX就会实时产生信令跟踪事件, 先入kafka, 再入influxdb待查. 同时提供实时sub/pub接口. 完备. 可以完整...

dev5
今天
7
0
OpenJDK之CyclicBarrier

OpenJDK8,本人看的是openJDK。以前就看过,只是经常忘记,所以记录下 图1 CyclicBarrier是Doug Lea在JDK1.5中引入的,作用就不详细描述了,主要有如下俩个方法使用: await()方法,如果当前线...

克虏伯
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部