版权声明:本文由张坤  原创文章,转载请注明出处: 
文章原文链接: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. 单列模式 (singleton pattern)

    单列就是说一个类只能被实例化一次,重点是确保某个对象只有一个,不会有第2个. c# 的实现是这样的 代码来源 : http://www.cnblogs.com/zhili/p/3185302.html ...

  2. PL/SQL 0.几秒出结果,SQL效率一定高吗?

    今天开发问我一个问题,PL/SQL很快出结果了,为什么应用还是很慢 create index F_AGT_SAVB_ACCTINFO_H_idx4 on F_AGT_SAVB_ACCTINFO_H ( ...

  3. RMI入门教程

    一.什么是RMI Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口.它使客户机上运 ...

  4. Java LinkedList 源码分析

    简介 LinkedList 是一个常用的集合类,用于顺序存储元素. LinkedList 经常和 ArrayList 一起被提及.大部分人应该都知道 ArrayList 内部采用数组保存元素,适合用于 ...

  5. android 初学: 提示No Launcher activity found!

    提示No Launcher activity found! 三步检查: 1 必须有 <category android:name="android.intent.category.LA ...

  6. css浮动+应用(瀑布流效果的实现)

    首先是index.html文件: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"& ...

  7. yii 使用 phpmailer发送邮件

    原文链接 :  http://www.yiiframework.com/extension/mailer/ 下载插件放在 :XXX/protected/extensions/ 作为一个普通的组建使用 ...

  8. Uncaught SyntaxError : Unexpected token ILLEGAL js传递带空格的参数

    通常在页面中要让某些内容点击后产生点击事件(非页面跳转)都会使用onclick,但是这样不适于需要传递参数的情况,于是写成直接调用JavaScript函数的方式:<a href=javascri ...

  9. WP独立文件资源字典

    有时候,开发者想把资源字典定义在单独的XAML文件中,在网上找了找,没找到比较详细的,自己弄了许久. 1. 新建xaml资源字典文件,如在根目录下“ResourceDictionary1.xaml”, ...

  10. rpm包安装

    RPM全称是“RedHatPackageManager”是由RedHat公司发发展起来的,本质是将软件源码包经过编译并且打包成rpm的格式,rpm文件包含的有二进制文件,配置文件,库文件等,同时RPM ...