前言:关于本篇博客主要会讲解在Android端使用HttpUrlConnection模仿Web浏览器采用表单的形式提交参数和一个或多个文件给服务器,如果本身对这方面就特别熟悉和http协议这些很了解了,那么就不需要再看了,当然如果有不是很了解的或者底层了解不是很熟悉的还是值得一看的。
原因:首先有个很疑惑的问题,使用HttpUrlConnection上传文件给服务器那还不so easy,这有什么好说的,现在网上各种开源框架数不胜数,可能在你工作中你甚至都不用看里面的代码,直接放到你的项目中,利用预留的接口也许一句代码就解决掉了所有事,大大降低了开发者的开发难度,所有的一切对你来说都是透明的,而你要做的仅仅是传替一些参数和文件就game over了。是的,确实如此,而恰巧问题来了,这也就是我写这篇博客的原因,如果说我们仅仅停留在使用的层面上那就太out了。不论是什么框架,最终都会都到底层传输这块,而HttpUrlConnection在底层究竟是怎么完成这一过程的呢,其实也就是在模仿Web浏览器,接下来我们就来玩一玩。
最终效果目标:这次我们会建立一个Web端也就是一个Web工程命名day01,并且把关于文件上传提交到HttpServletDemo这个Servlet来处理,在这里完成文件的接收并保存到服务器上,服务器我们采用Tomcat,也就是说最终我们会实现在手机上面可以点击文件上传一次性可以提交一个或多个文件到我们的服务器。只不过这次这些都需要我们来写。
第一阶段:实现服务端的搭建,编写我们的HttpServletDemo,使其能够正常接收文件并保存到服务器操作。服务端不了解的也没事,最终我会在Android端实现,这里先完成项目的搭建,服务端这次用到关于文件上传的commons-fileupload-1.2.1.jar和commons-io-1.3.2.jar两个开源框架,最终HttpServletDemo完整代码如下,注释都写了
public class HttpServletDemo extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
/** 得到表单提交数据 */
String username = request.getParameter("username");
String password = request.getParameter("password");
/** 得到上传文件的保存目录,将上传的文件存放于WEB-INF,upload目录下 */
String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
File file = new File(savePath);
/** 判断上传文件的保存目录是否存在 */
if (!file.exists() && !file.isDirectory()) {
System.out.println(savePath + "目录不存在,需要创建");
/** 创建目录 */
file.mkdir();
}
/** 消息提示 */
String message = "";
try {
/** 使用Apache文件上传组件处理文件上传步骤:*/
/** 1、创建一个DiskFileItemFactory工厂 */
DiskFileItemFactory factory = new DiskFileItemFactory();
/** 2、创建一个文件上传解析器 */
ServletFileUpload upload = new ServletFileUpload(factory);
/** 解决上传文件名的中文乱码 */
upload.setHeaderEncoding("UTF-8");
/** 3、判断提交上来的数据是否是上传表单的数据 */
if (!ServletFileUpload.isMultipartContent(request)) {
/** 按照传统方式获取数据 */
return;
}
/** 4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,*/
/** 每一个FileItem对应一个Form表单的输入项 */
List<FileItem> list = upload.parseRequest(request);
for (FileItem item : list) {
/** 如果fileitem中封装的是普通输入项的数据 */
if (item.isFormField()) {
String name = item.getFieldName();
/** 解决普通输入项的数据的中文乱码问题 */
String value = item.getString("UTF-8");
/** value = new String(value.getBytes("iso8859-1"),"UTF-8"); */
System.out.println(name + "=" + value);
} else {
/** 如果fileitem中封装的是上传文件,得到上传的文件名称 */
String filename = item.getName();
System.out.println(filename);
if (filename == null || filename.trim().equals("")) {
continue;
}
/** 注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:*/
/** c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt */
/** 处理获取到的上传文件的文件名的路径部分,只保留文件名部分 */
filename = filename.substring(filename.lastIndexOf("\\") + 1);
/** 获取item中的上传文件的输入流 */
InputStream in = item.getInputStream();
/** 创建一个文件输出流 */
FileOutputStream out = new FileOutputStream(savePath + "\\" + filename);
/** 创建一个缓冲区 */
byte buffer[] = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
/** 关闭输入流 */
in.close();
/** 关闭输出流 */
out.close();
/** 删除处理文件上传时生成的临时文件 */
item.delete();
message = "恭喜,文件上传成功!";
}
}
} catch (Exception e) {
message = "文件上传失败!";
e.printStackTrace();
}
request.setAttribute("message", message);
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
}
关于文件上传这块步骤比较固定,上述注释写的很详细,最终文件上传成功以后会保存在服务器upload目录下,最后会将请求转发到message.jsp页面,并通过request域带了一个参数过去,在页面输出。最终将项目发布到我们的Tomcat服务器,运行先在Web浏览器测试如下:
1.项目目录截图
2.运行截图
3.文件上传成功
至此,第一阶段算是完成了,这时候测试我们的Servlet是没有问题的,能够完成文件的接收,并且保存在服务器对应的目录上。
第二阶段:分析http请求头和请求体信息,我这里直接可以通过火狐浏览器抓到请求头信息,一个完整的HTTP请求报文由3部分组成(请求行+请求头+请求体)就以刚刚我们上传的数据抓取如下:
Accept请求头代表我能接收的类型
Accept-Encoding代表支持的压缩类型
Accept-Language代表支持的语言
Connection keep-alive 代表服务器断开时候不立马断开连接
ContentLength 数据长度
Content-Type =
multipart/form-data; boundary=—————————41184676334
(重要)文件上传时必须设置,在表单里面 必须设置
enctype=”multipart/form-data” method=”post”
……剩余消息头不做解释了,可以自行上网查阅。
紧接着下面是请求体信息,可以看到我上传了两个文本数据和一个图片文件,请求体格式每行开头会有——————41184676334然后是一个换行,格式固定Content-Disposition: form-data; name=”username”,这里的name是你在表单里面定义的名子,我写表单的时候定义是username所以这里是username,紧接着是一个空行,空行下面是我在表单填写的数据叫briansxuan,至此第一个数据就完了,下同,注意文件的时候有个filename参数,这个代表你上传文件的文件名称,然后换行还需要带上文件的mimeType,Content-Type: image/jpeg,我这里上传的是一张jpg图片,所以是这个,如果你上传的是png那么就会是Content-Type: image/png。关于文件的mimeType类型可以上网查阅。
紧接着下面看到的乱码东西其实就是你上传的图片,这里是采用二进制流的形式上传,然后在最后看到—————41184676334–,这个是结尾,千万要注意啊,这里一定要小心,整个过程一定按照抓取的这个格式,不能出一点点错误,很坑的,水太深,老司机偶尔也会翻车。
至此第二阶段也完成了,请求头这里只是给大家做个简单介绍,主要是请求体内容这块一定要细心,出了一点点错误文件都会上传不成功的。
第三阶段:打开ADT(电脑太卡,AS和MyEclipse同时运行基本就挂掉了)建立一个Android项目,先看下最终的效果:
这是我的手机最终的效果,这里一次性选择了多个图片同时上传,如下所示,同时为了防止OOM这里也对图片进行了压缩,可用于上传所有图片。
图片上传成功以后保持在我服务器Tomcat对应的项目upload下,可以看到已经成功上传
关于上传核心代码如下:
public class UploadFileUtil {
public static final String BOUNDARY = "--------WEBRIANSXUANFROMDATA";
public static final String MYURL = "http://192.168.191.1:8080/day01/HttpServletDemo";
public static String fileName = "";
public static final String name = "file1";
public synchronized void uoloadFile(final File uploadFile ,final String imgPath){
new Thread(new Runnable() {
@Override
public void run() {
try {
int intFlag = (int)(Math.random() * 1000000);
fileName = "brians"+intFlag+".jpg";
StringBuilder sb = new StringBuilder();
/**
* 表单数据
*/
// 1.<input type="text" name="username">
sb.append("--" + BOUNDARY + "\r\n");
sb.append("Content-Disposition: form-data; name=" + "username" + "\r\n");
sb.append("\r\n" + "briansxuan" + "\r\n");
// 2.<input type="text" name="password">
sb.append("--" + BOUNDARY + "\r\n");
sb.append("Content-Disposition: form-data; name=" + "password" + "\r\n");
sb.append("\r\n" + "517518" + "\r\n");
// 3.<input type="file" name="file1">
sb.append("--" + BOUNDARY + "\r\n");
sb.append("Content-Disposition: form-data; name=" + name + "; filename=" + fileName + "\r\n");
sb.append("Content-Type: image/jpeg" + "\r\n");
sb.append("\r\n");
byte[] headerInfo = sb.toString().getBytes("UTF-8");
byte[] endInfo = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("UTF-8");
System.out.println(sb.toString());
URL url = new URL(MYURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
//文件上传必须为POST
conn.setRequestMethod("POST");
//注意这里的格式,模仿表单,出现一点点错误都会导致上传不成功
conn.setRequestProperty("Content-Type","multipart/form-data; boundary=" + BOUNDARY);
//数据长度
conn.setRequestProperty("Content-Length", String.valueOf(uploadFile
.length() + headerInfo.length + endInfo.length));
//通过conn拿到服务器的字节输出流
OutputStream out = conn.getOutputStream();
//需要上传的文件封装成字节输入流
InputStream in = new FileInputStream(uploadFile);
out.write(headerInfo);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1)
out.write(buf, 0, len);
out.write(endInfo);
in.close();
out.close();
if (conn.getResponseCode() == 200) {
uploadFileListener.success(imgPath);
}
} catch (Exception e) {
e.printStackTrace();
if(uploadFileListener != null){
uploadFileListener.fail(imgPath);
}
}
}
}).start();
}
private static UploadFileListener uploadFileListener;
public void setUploadFileListener(UploadFileListener uploadFileListener){
this.uploadFileListener = uploadFileListener;
}
public interface UploadFileListener{
public abstract void success(String path);
public abstract void fail(String path);
}
}
注释已经在代码中给出了,代码其实是很简单的,主要是熟悉这个流程和http传输。
主页面Activity代码如下:
/**
*
* @author briansxuan
* @QQ 1057943470
* @date 2017-12-16:3:06
*
*/
public class MainActivity extends Activity {
private static GridView gridView;
private static Context context;
private static ImageView imageView;
private GVAdapter adapter;
ArrayList<String> listfile=new ArrayList<String>();
private List<String> list = new ArrayList<String>();
private UploadFileUtil uploadFileUtil;
public static final int SUCCESS = 200;
public static final int FAIL = 400;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context = getApplicationContext();
imageView = (ImageView) findViewById(R.id.imageView);
gridView=(GridView) findViewById(R.id.listView1);
list.clear();
uploadFileUtil = new UploadFileUtil();
Bundle bundle= getIntent().getExtras();
if (bundle!=null) {
if (bundle.getStringArrayList("files")!=null) {
list.clear();
listfile= bundle.getStringArrayList("files");
gridView.setVisibility(View.VISIBLE);
for(String str : listfile){
uploadFileUtil.uoloadFile(new File(str),str);
}
}
}
uploadFileUtil.setUploadFileListener(new UploadFileListener() {
@Override
public void success(String path) {
list.add(path);
handler.sendEmptyMessage(SUCCESS);
}
@Override
public void fail(String path) {
handler.sendEmptyMessage(FAIL);
}
});
}
public void chise(View v){
Intent intent = new Intent();
intent.setClass(this,ImgFileListActivity.class);
startActivity(intent);
}
public Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case SUCCESS:
// 将图片显示到界面上
if(adapter == null){
adapter = new GVAdapter();
gridView.setAdapter(adapter);
}
adapter.notifyDataSetChanged();
break;
case FAIL:
Toast.makeText(context, "图片上传失败", 0).show();
break;
}
};
};
private class GVAdapter extends BaseAdapter{
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if(convertView == null){
holder = new ViewHolder();
convertView = View.inflate(MainActivity.this, R.layout.item, null);
holder.iv = (ImageView) convertView.findViewById(R.id.iv);
convertView.setTag(holder);
}else{
holder = (ViewHolder) convertView.getTag();
}
holder.iv.setImageBitmap(decodeSampledBitmapFromResource(list.get(position), 200, 300));
return convertView;
}
}
static class ViewHolder{
ImageView iv;
}
public static Bitmap decodeSampledBitmapFromResource(String imgPath,
int reqWidth, int reqHeight) {
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imgPath, options);
// 调用上面定义的方法计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(imgPath, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源图片的高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 计算出实际宽高和目标宽高的比率
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
// 一定都会大于等于目标的宽和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
}
整个代码还算是很简单的,这里对大图片进行了压缩,防止内存溢出。
至此,这个过程就结束了,首先代码没有做容错处理,也没有做健壮性判断,因为本篇主要目的并不是代码,而是熟悉在Android端利用HttpUrlConnection模仿Web浏览器上传文件这一个过程,也需要你去了解HTTP协议。
如有错误,希望大家指出哈。