版权声明:本文由张坤  原创文章,转载请注明出处: 
文章原文链接: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. c#调用c++开发的dll const char* 返回值接收问题

    原文:c#调用c++开发的dll const char* 返回值接收问题 用c#调用视频接口相关的dll,dll使用c++开发. c++接口定义如下: PLATFORM const char* Pla ...

  2. 子句判断、启动强度和去模糊化--AForge.NET框架的使用(三)

    原文:子句判断.启动强度和去模糊化--AForge.NET框架的使用(三) 使用AForge.NET进行模糊运算 上一篇说来一些模糊运算的数学问题,用AForge.NET做相关运算就很简单了. 1.联 ...

  3. 转自http://blog.sina.com.cn/daylive——C++ STL set&multiset

    C++ STL set和multiset的使用 1,set的含义是集合,它是一个有序的容器,里面的元素都是排序好的,支持插入,删除,查找等操作,就  像一个集合一样.所有的操作的都是严格在logn时间 ...

  4. Java设计模式之简单工厂、工厂方法和抽象工厂

    在前面的学习中(参见前面的博客),我们学到了很多OO原则: 封装变化 多用组合,少用继承 针对接口/超类编程,不针对实现编程 松耦合 开闭原则 让我们从一个简单的类开始,看看如何将之改造成符合OO原则 ...

  5. mysql数据库优化[千万级查询]

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索 ...

  6. Server Job: error: String or binary data would be truncated. The statement has been terminated.

    "String or binary data would be truncated. The statement has been terminated" most probabl ...

  7. android 解析json数据格式

    json数据格式解析我自己分为两种: 一种是普通的,一种是带有数组形式的: 普通形式的:服务器端返回的json数据格式如下: {"userbean":{"Uid" ...

  8. iOS中block实现的探究

    [0. Brief introduction of block] Block是iOS4.0+ 和Mac OS X 10.6+ 引进的对C语言的扩展,用来实现匿名函数的特性. 用维基百科的话来说,Blo ...

  9. [Angular 2] Filter items with a custom search Pipe in Angular 2

    This lessons implements the Search Pipe with a new SearchBox component so you can search through eac ...

  10. Zend Framework学习日记(2)--HelloWorld篇(转)

    Zend Framework学习日记(2)--HelloWorld篇 这一篇主要演示如何用zf命令行工具建立一个基于Zend Framework框架的工程,也是我初学Zend Framework的小练 ...