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

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

基于Http原理实现Android的图片上传和表单提交的更多相关文章

  1. 如何用elementui去实现图片上传和表单提交,用axios的post方法

    下面是在vue搭建的脚手架项目中的组件component文件夹下面的upload.vue文件中的内容 <!--这个组件主要用来研究upload这个elementui的上传插件组件--> & ...

  2. thinkphp图片上传+validate表单验证+图片木马检测+缩略图生成

    目录 1.案例 1.1图片上传  1.2进行图片木马检测   1.3缩略图生成   1.4控制器中调用缩略图生成方法 1.案例 前言:在thinkphp框架的Thinkphp/Library/Thin ...

  3. swift文件上传及表单提交

    var carData:NSMutableDictionary = NSMutableDictionary(); var request:NSMutableURLRequest = NSMutable ...

  4. php 利用http上传协议(表单提交上传图片 )

    主要就是利用php 的 fsocketopen 消息传输. 这里先通过upload.html 文件提交,利用chrome抓包,可以看到几个关键的信息. 首先指定了表单类型为multipart/form ...

  5. 利用jquery.form.js实现将form提交转为ajax方式提交的方法(带上传的表单提交)

    提供一种方法就是利用jquery.form.js. (1)这个框架集合form提交.验证.上传的功能. 核心方法 -- ajaxForm() 和 ajaxSubmit() $('#myForm').a ...

  6. 利用WCF与Android实现图片上传并传参

    利用WCF与Android实现图片上传并传参 最近做一个项目后端使用WCF接收Android手机拍照并带其它参数保存到服务器里:刚好把最近学习的WCF利用上,本以为是个比较简单的功能应该很好实现,没想 ...

  7. ajax方式提交带文件上传的表单,上传后不跳转

    ajax方式提交带文件上传的表单 一般的表单都是通过ajax方式提交,所以碰到带文件上传的表单就比较麻烦.基本原理就是在页面增加一个隐藏iframe,然后通过ajax提交除文件之外的表单数据,在表单数 ...

  8. ANDROID使用MULTIPARTENTITYBUILDER实现类似FORM表单提交方式的文件上传

    最近在做 Android 端文件上传,要求采用 form 表单的方式提交,项目使用的 afinal 框架有文件上传功能,但是始终无法与php写的服务端对接上,无法上传成功.读源码发现:afinal 使 ...

  9. 项目回顾1-图片上传-form表单还是base64-前端图片压缩

    第一个项目终于上线了,是一个叫亲青筹的公益众筹平台,微信端,电脑端还有后台界面大部分都是我完成的,几个月过来,感觉收获了很多,觉得要总结一下. 首先想到的是图片上传的问题.在通常表单数据都是ajax上 ...

随机推荐

  1. Qt全局热键(windows篇)(使用RegisterHotKey和句柄进行注册)

    转载:http://www.cuteqt.com/blog/?p=2088 Qt对于系统底层,一直没有很好的支持,例如串口并口通信,还有我们经常都会用到的全局热键,等等.既然Qt可能出于某种原因,不对 ...

  2. Android ContentProvider完整案例

    ContentData类,提供数据常量: /** * 提供ContentProvider对外的各种常量,当外部数据需要访问的时候,就可以参考这些常量操作数据. * @author HB * */ pu ...

  3. Eclipse插件管理

    Eclipse 的特色之一,就是它的插件功能.可以说, Eclipse 是一个插件的大集合,所有的模块都以插件的形式存在.那么,究竟什么是插件呢? 插件( plug-in ),即 Eclipse 的功 ...

  4. 【转】在Ubuntu 12.04 上为Virtualbox 启用USB 设备支持--不错

    原文网址:http://www.cnblogs.com/ericsun/archive/2013/06/10/3130679.html 虚拟机我一直在用,不是说离不开Windows,而是有些时候一些应 ...

  5. Design Tic-Tac-Toe 解答

    Question Design a Tic-tac-toe game that is played between two players on a n x n grid. You may assum ...

  6. lua的几个时间相关处理函数

    随手写的,项目中没用,不一定对,只作参考. --游戏时间相关函数 local gt = {} local math = math local ONE_HOUR = ONE_MINUTE * ONE_M ...

  7. SQL Server 系统时间

    getdate()函数:取得系统当前的日期和时间.返回值为datetime类型的. 用法:getdate() 例子: select getdate() as dte,dateadd(day,-1,ge ...

  8. (转)iOS消息推送机制中pem文件的生成

    转自:http://2015.iteye.com/blog/1567777 以前写了一篇文章:iOS消息推送机制的实现,这篇文章中生成的是p12文件,但是php是用的pem文件,生成的方法和p12文件 ...

  9. CornerStone 破解 最简单的破解方法

    方法一:最近在用cornerstone这个svn的软件感觉非常不错,但是竟然忘了破解,以至于到了14天试用期的最后一天才开始破解, 其实方法很简单,就是修高试用期的天数,找到plist文件把14天改为 ...

  10. Python 获取Facebook用户的Friends的爱好中的Top10

    CODE; #!/usr/bin/python # -*- coding: utf-8 -*- ''' Created on 2014-8-12 @author: guaguastd @name: f ...