Android表单及图片上传--HTTP协议构造分析

        最近一段时间在做Android客户端与服务器之间的通信,包括表单提交和图片上传两部分,这块内容Android没有提供具体的API,后来到网上找了一下,发现大神们都是自己构造相关的http协议。不过,在这里说一句,大神们提供的思路和代码都比较好,但是那些代码,我自己是没法正常运行的,所以,我又想了一套如何进行查错,调试的东东。这个绝对很关键哦,至少可以保证你的工程工作的很顺利。

        还是先借用一段话为大家描述一下http协议怎么进行分析吧,这个内容,我是借鉴以前的大神的,我会在文章最后为大家留下参考链接,为了保证后面的叙述部分合理分布,我就在这里重新说一下吧。

        前面也说到了,Android上面没有上传表单和图片的API,我们要仿照表单提交的数据流,自己构造一个http数据包,然后发送到actionUrl上,这个actionUrl就是处理表单数据的链接。

       为了方便大家对系统进行深入的研究分析,我会将系统中使用到的工程文件打包上传,欢迎传阅。

       在此再说明一下,使用Windows7+Apache2.2.25+PHP/5.4.11搭建的本地服务器,因为表单处理的过程都是用PHP语言完成的。下面给出具体的步骤及相关解释:

1、本地新建fileUpload.html文件,源码如下

<!doctype html>
<html lang="en">
 <head>
  <meta charset="UTF-8">
  <meta name="Generator" content="EditPlus®">
  <meta name="Author" content="">
  <meta name="Keywords" content="">
  <meta name="Description" content="">
  <title>Document</title>
 </head>
 <body>
 
  <form method="post" action="file.php" enctype="multipart/form-data">
  Name: <input type="text" name="fname">
  <br>
  File: <input type="file" name="upfile">
  <br>
  <input type="submit" value="上传">
  </form>
  
 </body>
</html>
        表单上传界面如图所示:

        

注意:这次实验中,我上传的是jpg图片文件,所以在后面的数据捕获中大家会看到一大堆乱码,其实这堆乱码是对图片的二进制数据的显示。

2、新建后台处理文件file.php,代码如下

<?php
$destination_folder = "../upload/";
$imgpreviewsize = 1 / 8; //缩略图比例

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $fname = $_POST["fname"];
    echo $fname . "<br>"; 
    
    if (!is_uploaded_file($_FILES['upfile']["tmp_name"])) {
        echo "failed";
        exit;
    }

    $file = $_FILES["upfile"];
    $name = $file["name"];
    //echo $name . "<br>";
    if (!file_exists($destination_folder)) {
        mkdir($destination_folder);
    }

    $filename = $file["tmp_name"];

    $image_size = getimagesize($filename);
    $pinfo = pathinfo($name);
    $destination = $destination_folder . $name;
    if (file_exists($destination)) {
        echo "success";  //如果有重名文件,直接返回success上传
        exit;
    }
    if (!move_uploaded_file($filename, $destination)) {
        echo "failed";
        exit;
    }
    echo "success";
}
?>

3、对于抓包的工具的选择,如果用的是Windows系统的话,建议使用HttpWatch抓包工具,配合IE浏览器使用,个人原因,不大会使用HttpFox等工具,在抓包的时候,只能看到headers,无法完整查看具体传输的内容。



这是HttpWatch运行的界面,在安装HttpWatch后,打开IE浏览器,右键打开HttpWatch Professional,在提交表单前点击Record,然后提交表单,看到浏览器返回数据后再点击Stop,便能捕获传输的数据了。当然,我们也可以把捕获的数据保存起来,点击菜单栏中的Save按钮,一般保存为后缀名为hwl的文件。

下面是捕获数据后的截图



好咯,言归正传,我们看下捕获的数据的具体格式吧。

4、捕获的数据分析


这些是表单上传的文件头,注意这一行:

Content-Type: multipart/form-data; boundary=---------------------------7dfd33020b4a 根据 rfc1867, multipart/form-data是必须的. 

---------------------------7dfd33020b4a是分隔符,分隔多个文件、表单项。其中d33020b4a 是即时生成的一个数字,用以确保整个分隔符不会在文件或表单项的内容中出现。Form每个部分用分隔符分割,分隔符之前必须加上"--"着两个字符(即--{boundary})才能被http协议认为是Form的分隔符,表示结束的话用在正确的分隔符后面添加"--"表示结束。

前面的-------------------------7d 是 IE 特有的标志,Mozila 为-------------------------71

(上面深色部分的内容多数为引用其他地方的文章,引用的链接会在文章最后给出)



。。。。。。。。。。。。众多二进制乱码。。。。内容省略。。。



图片内容过多,我把主要的内容提取出来为大家解释一下

-----------------------------7dfd33020b4a
Content-Disposition: form-data; name="fname"

image
-----------------------------7dfd33020b4a
Content-Disposition: form-data; name="upfile"; filename="tt.jpg"
Content-Type: image/jpeg

/**图片的二进制内容乱码。不在此展示**/
-----------------------------7dfd33020b4a--


最后一行是一个空格,这个空格是必须的。通过观察上面的数据,很多我们都可以理解其中的含义,但是“--------------------------7dfd33020b4a”这个是什么东东呢?细心的读者可能会发现,在上面提到的headers中好像出现过这个字符串,是哪里呢?对,就是刚才给大家透露的boundary。但是这个东东是“--boundary”,boundary的介绍在上文中已经提到了。

现在我们根据捕获的内容来总结一下这个传递表单数据的格式。首先分成两个部分来说明,

第一,文本部分

1、第一行:--boundary

2、第二行:Content-Disposition: form-data; name="你为编辑框定义的name"

3、第三行:\r\n  回车换行符

4、第四行:name对应的value

第二,图片部分

1、第一行:--boundary

2、第二行:Content-Disposition: form-data; name="上传的文件选择框的name"

3、第三行:Content-Type:image/jpeg (如果你上传的不是图片,是其它格式的文件的话,这个地方可能不同)

4、第四行:\r\n 回车换行符

5、第五行开始是上传的二进制数据

6、最后的结束符是--boundary--,最后还有一个\r\n换行符


-------------------------------------------------------------------------------华丽的分割线------------------------------------------------------------------------------------


上面的内容都属于原理性介绍部分,接下来我们讲述实际工程应用中如何实现数据包的构造和调试。其实之前的大神已经贴出相关代码,但是在实际操作中发现不是那么顺利,代码中偶尔会出现一些小错误,在这里我都进行的改正,绝对可以通过工程测试。


第一部分,由于我们主要上传的是图片文件,所以在此先对图片文件的数据格式进行构造。

package com.example.uploadform.upload;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * Created by Sun on 2015/4/4 0004.
 */
public class FormImage {

    /*表单字段名称*/
    private String formname;
    /*上传文件的数据*/
    private byte[] data;
    /*内容类型*/
    private String contentType = "application/jpeg";
    /*文件名称*/
    private String filename;

    public FormImage(String filename, String filePath, String formname, String contentType) {
        this.filename = filename;
        this.formname = formname;
        this.contentType = contentType;

        try {
            this.data = getContent(filePath);
        } catch (IOException e) {
            System.out.println("Could not completely read file");
        }
    }

    public String getFormName() {
        return formname;
    }

    public void SetFormName(String formname) {
        this.formname = formname;
    }

    public String getFileName() {
        return filename;
    }

    public void setFileName(String filename) {
        this.filename = filename;
    }

    public byte[] getData() {
        return data;
    }

    public void SetData(byte[] data) {
        this.data = data;
    }

    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public void freeBuffer() {
        data = null;
        System.gc();
    }

    /*
    * 读取文件的byte字符流
    * */
    public byte[] getContent(String filePath) throws IOException {
        File file = new File(filePath);

        long fileSize = file.length();
        if(fileSize > Integer.MAX_VALUE) {
            System.out.println("file too big to read");
            return null;
        }

        FileInputStream fi = new FileInputStream(file);
        byte[] buffer = new byte[(int) fileSize];
        int offset = 0;
        int numRead = 0;
        while(offset < buffer.length && (numRead = fi.read(buffer, offset, buffer.length - offset)) >= 0) {
            offset += numRead;
        }

        //确保所有数据均被读取
        if (offset != buffer.length) {
            throw new IOException("Could not completely read file" + file.getName());
        }

        fi.close();
        return buffer;
    }
}

第二部分:实现上传的代码
package com.example.uploadform.upload;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import java.util.UUID;

/**
 * Created by Sun on 2015/4/4 0004.
 */
public class UploadImage {

    String CONTENT_TYPE = "multipart/form-data";
    String BOUNDARY = UUID.randomUUID().toString(); //边界标识,随机生成
    String PREFIX = "--";
    String LINE_END = "\r\n";
    boolean Key = true;
    public UploadImage() {}
    /*
     * 上传图片内容,格式请参考HTTP 协议格式。
     * 人人网Photos.upload中的”程序调用“http://wiki.dev.renren.com/wiki/Photos.upload#.E7.A8.8B.E5.BA.8F.E8.B0.83.E7.94.A8
     * 对其格式解释的非常清晰。
     * 格式如下所示:
     * --****************fD4fH3hK7aI6
     * Content-Disposition: form-data; name="upload_file"; filename="apple.jpg"
     * Content-Type: image/jpeg
     *
     * 这儿是文件的内容,二进制流的形式
     */
    private void addImageContent(FormImage file, DataOutputStream output) {

            StringBuilder split = new StringBuilder();
            split.append(PREFIX);
            split.append(BOUNDARY);
            split.append(LINE_END);
            split.append("Content-Disposition: form-data; name=\"" + file.getFormName() + "\"; filename=\"" + file.getFileName() + "\"" + LINE_END);
            split.append("Content-Type: " + file.getContentType() + LINE_END);
            split.append(LINE_END);
            try {
                output.writeBytes(split.toString());
                output.write(file.getData(), 0, file.getData().length);
                output.writeBytes(LINE_END);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    }

    /*
     * 构建表单字段内容,格式请参考HTTP 协议格式(用httpfox可以抓取到相关数据)。(以便上传表单相对应的参数值)
     * 格式如下所示:
     * --****************fD4fH3hK7aI6
     * Content-Disposition: form-data; name="action"
     * // 一空行,必须有
     * upload
     */
    private void addFormField(Map<String, String> params, DataOutputStream output) {
        StringBuilder sb = new StringBuilder();
        for(Map.Entry<String, String> param : params.entrySet()) {
            sb.append(PREFIX);
            sb.append(BOUNDARY);
            sb.append(LINE_END);
            sb.append("Content-Disposition: form-data; name=\"" + param.getKey() + "\"" + LINE_END);
            sb.append(LINE_END);
            sb.append(param.getValue() + LINE_END);
        }
        try {
            output.writeBytes(sb.toString());
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 直接通过 HTTP 协议提交数据到服务器,实现表单提交功能。
     * @param actionUrl 上传路径
     * @param params 请求参数key为参数名,value为参数值
     * @param file 上传文件信息
     * @return 返回请求结果
     */
    public String Post(String actionUrl, Map<String, String> params, FormImage file) {
        HttpURLConnection conn = null;
        DataOutputStream output = null;
        InputStream input = null;
        try {
            URL url = new URL(actionUrl);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(120000);
            conn.setDoInput(true);    //允许输入
            conn.setDoOutput(true);   //允许输出
            conn.setUseCaches(false); //不使用cache
            conn.setRequestProperty("Accept", "text/html, application/xhtml+xml, */*");
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Connection", "keep-alive");
            conn.setRequestProperty("Charset", "UTF-8");
            conn.setRequestProperty("Content-Type", CONTENT_TYPE + "; boundary=" + BOUNDARY);
        //  conn.connect();
            output = new DataOutputStream(conn.getOutputStream());

            addImageContent(file, output); //添加图片内容
            addFormField(params, output);   //添加表单字段内容

            output.writeBytes(PREFIX + BOUNDARY + PREFIX + LINE_END);//数据结束
            output.flush();

            int code = conn.getResponseCode();
            if(code != 200) {
                throw new RuntimeException("请求‘" + actionUrl + "’失败");
            }

            input = conn.getInputStream();
            StringBuilder response = new StringBuilder();
            int ch;
            while((ch = input.read()) != -1) {
                response.append((char)ch);
            }
            output.close();
            input.close();
            conn.disconnect();
            return response.toString();
        } catch(IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (output != null) {
                    output.close();
                }
                if (input != null) {
                    input.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            if (conn != null) {
                conn.disconnect();
            }
        }
    }
}



第三部分:MainActivity.java 实现表单上传,上传的数据为 (”fname“,”image“)(”upfile“,"tt.jpg")

为了使app能够从内存卡中读取文件,并写入文件,而且还能访问互联网,需要在AndroidManifest.xml中加入下面几句话:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
接下来的是MainActivity的布局文件

<!-- 这是MainActivity的布局文件 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:orientation="vertical"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <Button
        android:id="@+id/send"
        android:text="发送测试"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/response"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    
    <ImageView
        android:id="@+id/testimage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>
下面的MainActivity.java主要实现的功能是将图片上传到服务器上并下载显示出来。

package com.example.uploadform.ui;

import android.os.Message;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.os.Handler;

import com.example.uploadform.R;
import com.example.uploadform.download.FileDown;
import com.example.uploadform.upload.FormImage;
import com.example.uploadform.upload.UploadImage;
import com.example.uploadform.utils.SDCardImageLoader;
import com.example.uploadform.utils.ScreenUtils;

import java.io.File;
import java.util.HashMap;
import java.util.Map;


public class MainActivity extends ActionBarActivity {
    /*定义发送按钮*/
    private Button mSend = null;
    /*定义上传图片实例*/
    private UploadImage instance = null;
    //定义表单数组
    private Map<String, String> params = new HashMap<String, String>();
    //定义返回值
    private String response = null;

    //读取内存卡的实例
    private SDCardImageLoader loader = null;
    //显示返回字符串的文本控件
    private TextView text = null;
    //显示返回图片的图像控件
    private ImageView image = null;
    //上传的图片的路径
    private String filePath = "/storage/emulated/0/DCIM/Camera/tt.jpg";//这个是我上传的图片的本地地址,大家根据需要可以修改

    //需要上传的图片文件的file接口
    private File file = null;
    //需要上传的图片的实例
    private FormImage formImage = null;

    //需要下载的图片的url地址
    private String urlDownload = "http://172.19.105.1/www/upload/tt.jpg";//这个是我上传到服务器上的图片的链接,之前设计好了,所以可以这么写,大家根据需要                                                                          //修改

    //下载的图像文件的本地存储文件夹
    private String localFile = "/fire/downloads/";

    //图像下载实例
    private FileDown fileDown = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //获取屏幕像素
        ScreenUtils.initScreen(this);

        loader = new SDCardImageLoader(ScreenUtils.getScreenW(), ScreenUtils.getScreenH());

        instance = new UploadImage();   //UploadImage实例

        mSend = (Button)this.findViewById(R.id.send);  //实例测试

        text = (TextView)this.findViewById(R.id.response);   //文本控件实例化
        image = (ImageView)this.findViewById(R.id.testimage);//图像控件实例化

        file = new File(filePath);
        formImage= new FormImage(file.getName(), filePath, "upfile", "application/jpeg"); //上传的图像数据实例化

        fileDown = new FileDown(urlDownload, localFile); //下载的图像数据实例化

        //处理UI消息的handler
        final Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg){
                super.handleMessage(msg);
                if (msg.what == 10001) {
                    text.setText(response);
                }
                if (msg.what == 10002) {
                    image.setTag(fileDown.getLocalFile());
                    loader.loadImage(1, fileDown.getLocalFile(), image);
                }
            }
        };

        params.put("fname", "image");   //添加表单字段
        mSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        response = instance.Post("http://172.19.105.1/www/test/file.php", params, formImage);
                        Message msg = Message.obtain();
                        msg.what = 10001;
                        handler.sendMessage(msg);

                        if (fileDown.downFile()) {
                            msg = Message.obtain();
                            msg.what = 10002;
                            handler.sendMessage(msg);
                        }

                    }
                }).start();
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}
最后来一张手机端成功的例子


好了,教程到此结束。谢谢阅览。


再扯几句闲话,童鞋们在自己测试这个通信的过程中,可能会问,如何捕获手机客户端请求的数据呢?这个问题问的好,我找了很多工具,发现了一款比较不错的,

”Visual Sniffer“,我会把这个工具和上面的源码部分一起打包上传的。

下面展示一下这个工具的工作界面。



和之前的HttpWatch使用方法差不多,就是点击”开始拦截“,'停止拦截'之类的,然后左边的列表解释一下,我的主机的IP是172.19.105.1,Android手机的IP是172.19.105.2

具体的就不多说了,大家自己摸索吧,这个软件自带的有帮助手册,绝对好用。(记得网卡选择正确哦)


资源下载链接:http://download.csdn.net/detail/s2392735818/8610077,新开的账号,没有积分,希望大家多多下载多多支持,如果没有积分的朋友,可以去百度网盘下载,谢谢支持。

百度网盘:链接: http://pan.baidu.com/s/1i3mtAPB 密码: t7g3

感谢的文章:

Android上传文件,图片。以及服务器端接收相关。 http://blog.csdn.net/qq247890212/article/details/16358581

Android下的应用编程--------用HTTP协议实现文件上传功能 http://blog.csdn.net/newjueqi/article/details/4777779

Android文件图片上传的详细讲解(一)HTTP multipart/form-data上传报文格式实现手机端上传 http://topmanopensource.iteye.com/blog/1605238

在Android上通过模拟HTTP multipart/form-data请求协议实信息实现图片上传




  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值