1.背景
平时,在做web开发的时候,例如一些网页的访问,数据的修改查看等等都是使用一些ajax、form表单去访问,这实际上是已经帮我们封装课大部分信息。当我们从web端转到移动端或者pc端的时候,可以考虑使用URL这一java提供的底层类来进行http的请求。
2.原理
1.首先粗略了解下http协议内容
http报文主体也就是http这个传输内容,通常分成两大部分。第一部分是报文首部,第二部分就是报文内容。这两大部分必须由一个空行分割开来作为分界线。

普通字段上传不涉及文件的时候
涉及多对象集合上传的时候
3.内容
1.首先了解下uri
2.接着了解url在java中的操作
1.进行普通的表单数据提交(name:value)
//发送JSON字符串 如果成功则返回成功标识。
public static String doJsonPost(String urlPath, String Json) {
//String path=properties.getProperty("server.url")+urlPath;
String path=server_host+urlPath;
String result = "";
Scanner reader = null;
try {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Content-Type","application/json; charset=UTF-8");
// 设置接收类型否则返回415错误
//conn.setRequestProperty("accept","*/*")此处为暴力方法设置接受所有类型,以此来防范返回415;
conn.setRequestProperty("accept","application/json");
// 往服务器里面发送数据
if (Json != null && !Json.equals("")) {
byte[] writebytes = Json.getBytes("utf-8");
// 设置文件长度
conn.setRequestProperty("Content-Length", String.valueOf(writebytes.length));
OutputStreamWriter outwritestream = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
outwritestream.write(Json);
outwritestream.flush();
outwritestream.close();
}
//读取数据
if (conn.getResponseCode() == 200) {
reader = new Scanner(new InputStreamReader(conn.getInputStream(),"utf-8"));
while(reader.hasNext()){
result+=reader.nextLine();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}
return result;
}
2.单线程文件上传
切记写上需要写入的数组长度,防止最后一个数组的时候写入一些非法值:out.write(bufferOut,0,bytes);
/**
* urlPath 服务器地址
* fileName 文件名(带后缀)
* file 文件
*/
public static String uploadFiles(String urlPath,String fileName,File files) {
//String path=properties.getProperty("server.url")+urlPath;
String result ="";
String path=server_host+urlPath;
HttpURLConnection conn = null;
// boundary就是request头和上传文件内容的分隔符(可自定义任意一组字符串)
String BOUNDARY = "---------------------------123821742118716";
// 用来标识payLoad+文件流的起始位置和终止位置(相当于一个协议,告诉你从哪开始,从哪结束)
String preFix = ("\r\n--" + BOUNDARY + "\r\n");
String hostFix = ("\r\n--" + BOUNDARY +"--"+ "\r\n");
try {
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(2000);
conn.setReadTimeout(30000);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方法
conn.setRequestMethod("POST");
// 设置header
conn.setRequestProperty("Accept","*/*");
conn.setRequestProperty("Connection", "keep-alive");
conn.setRequestProperty("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY);
conn.setRequestProperty("RANGE","bytes=0");
// 获取写输入流
OutputStream out = conn.getOutputStream();
// 获取上传文件
File file = files;
// 要上传的数据
StringBuffer strBuf = new StringBuffer();
// 标识payLoad + 文件流的起始位置
strBuf.append(preFix);
// 下面这三行代码,用来标识服务器表单接收文件的name和filename的格式
// 在这里,我们是file和filename.后缀[后缀是必须的]。
// 这里的fileName必须加个.jpg,因为后台会判断这个东西。
// 这里的Content-Type的类型,必须与fileName的后缀一致。
// 如果不太明白,可以问一下后台同事,反正这里的name和fileName得与后台协定!
// 这里只要把.jpg改成.txt,把Content-Type改成上传文本的类型,就能上传txt文件了。
strBuf.append("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName.replaceAll(" ","")+ "\"\r\n");
if(fileName.endsWith("jpg")){
strBuf.append("Content-Type: image/jpeg" + "\r\n\r\n");
}else if(fileName.endsWith("png")){
strBuf.append("Content-Type: application/x-png" + "\r\n\r\n");
}else{
strBuf.append("Content-Type: application/octet-stream" + "\r\n\r\n");
}
out.write(strBuf.toString().getBytes());
// 获取文件流
FileInputStream fileInputStream = new FileInputStream(file);
DataInputStream inputStream = new DataInputStream(fileInputStream);
// 每次上传文件的大小(文件会被拆成几份上传)
int bytes = 0;
// 计算上传进度
float count = 0;
// 获取文件总大小
int fileSize = fileInputStream.available();
// 每次上传的大小
byte[] bufferOut = new byte[1024];
// 上传文件
while ((bytes = inputStream.read(bufferOut)) != -1) {
out.write(bufferOut,0,bytes);
count += bytes;
System.out.println("progress:" +(count / fileSize * 100) +"%");
}
out.write(hostFix.getBytes());
inputStream.close();
if (conn.getResponseCode() == 200) {
Scanner reader = new Scanner(new InputStreamReader(conn.getInputStream(),"utf-8"));
while(reader.hasNext()){
result+=reader.nextLine();
}
reader.close();
}
out.flush();
out.close();
} catch (Exception e) {
//utils.logD("上传图片出错:"+e.toString());
} finally {
if (conn != null) {
conn.disconnect();
}
}
return result;
}
3.多线程文件上传
客户端,首先需要根据线程数量划分每个线程上传的范围,做简单的除法运算后将每一个范围的起点赋值到请求头部,以便于传送到后台进行文件的多线程拼接(RandowAccessFile),文件的读取范围也需要自己做判断,这个功能就是类似于hgttp的range又不字段的作用,http响应应该发送某个范围的数据给我们,而我们做多线程上传文件的原理也是如此。
public class UploadTest {
// private static String server_host="http://39.108.158.15:8888/";
public static String server_host="http://localhost:9999/";
private static int threadNum=3;
public static void main(String[] args) {
File file=new File("uuid.docx");
long length=file.length();
long size = length / threadNum;
for (int i = 0; i < threadNum; i++) {
long startIndex = i * size;
long endIndex = (i + 1) * size - 1;
if (i == threadNum - 1) endIndex = length;
System.out.println(startIndex+"====="+endIndex);
new UploadThread(startIndex, endIndex,i).start();
}
}
}
class UploadThread extends Thread{
private long startIndex;
private long endIndex;
private int number;
public UploadThread(long startIndex,long endIndex,int i) {
this.startIndex=startIndex;
this.endIndex=endIndex;
this.number=i;
}
public String uploadFiles(String urlPath, String fileName) {
String result ="";
String path=UploadTest.server_host+urlPath;
HttpURLConnection conn = null;
// boundary就是request头和上传文件内容的分隔符(可自定义任意一组字符串)
String BOUNDARY = "---------------------------123821742118716";
// 用来标识payLoad+文件流的起始位置和终止位置(相当于一个协议,告诉你从哪开始,从哪结束)
String preFix = ("\r\n--" + BOUNDARY + "\r\n");
String hostFix = ("\r\n--" + BOUNDARY +"--");
RandomAccessFile randomAccessFile;
try {
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(2000);
conn.setReadTimeout(30000);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方法
conn.setRequestMethod("POST");
// 设置header
conn.setRequestProperty("Accept","*/*");
conn.setRequestProperty("Connection", "keep-alive");
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
conn.setRequestProperty("LLGRange",String.valueOf(startIndex));
conn.setRequestProperty("User-Agent","Mozilla/5.0(Macintosh;U;IntelMacOSX10_6_8;en-us)AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50");
//conn.setRequestProperty("RANGE","bytes=0");
// 获取写输入流
OutputStream out = conn.getOutputStream();
//OutputStream out = System.out;
// 获取上传文件
randomAccessFile=new RandomAccessFile(new File(fileName),"rwd");
randomAccessFile.seek(startIndex);
// 要上传的数据
StringBuffer strBuf = new StringBuffer();
// 标识payLoad + 文件流的起始位置
strBuf.append(preFix);
strBuf.append("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName+ "\"\r\n");
strBuf.append("Content-Type: application/octet-stream" + "\r\n\r\n");
out.write(strBuf.toString().getBytes());
//DataInputStream inputStream = new DataInputStream(fileInputStream);
// 每次上传文件的大小(文件会被拆成几份上传)
int bytes = 0;
// 计算上传进度
float count = 0;
long sum=0;
// 每次上传的大小
byte[] bufferOut = new byte[1024];
// 上传文件
long finalLength=0;
while ((bytes = randomAccessFile.read(bufferOut)) != -1&&randomAccessFile.getFilePointer()<endIndex) {
out.write(bufferOut, 0, bytes);
//计算上传进度,此地方自行设计
//sum+=bytes;
//count += bytes;
}
//当读取到取值范围边界的数据数组的时候,应该判断下最终需要读取的数组大小。
out.write(bufferOut,0, (int) (bytes-randomAccessFile.getFilePointer()+endIndex));
out.write(hostFix.getBytes());
out.flush();
out.close();
randomAccessFile.close();
} catch (Exception e) {
} finally {
if (conn != null) {
conn.disconnect();
}
}
return result;
}
@Override
public void run() {
uploadFiles("user/upload","uuid.docx");
}
}
服务端,需要接受每个线程上传的断点(pos),断点可以从请求头部获取,不过这个请求头是自己从前台划分好断点后自己写上去的,需要自己定义。拿到每一个断点后,就可以通过RandomAccessFile的方式从本地读取已有文件然后设置文件指针位置,之后写入自己负责的部分,由于controller是单例模式,每个controoler的service方法都由一个线程去单独访问,局部变量互补干扰,所以可以放心数据文件的安全。
public StateCode uploadFile(MultipartFile file, String basePath, long pos) {
String filename="";
try {
File oldFile=new File(basePath);
RandomAccessFile randomAccessFile;
randomAccessFile=new RandomAccessFile(oldFile,"rwd");
randomAccessFile.seek(pos);
byte[] ssd=file.getBytes();
randomAccessFile.write(ssd,0,ssd.length);
} catch (IOException e) {
e.printStackTrace();
}
return new StateCode(true,filename);
}
4.单线程断点上传
客户端保持断点信息:
突然断网:由于http的性质可能导致这一次上传流被突然中断而不能成功到达后台服务器(没有写入结束分割线,后台无法成功截取数据)
主动点击暂停或者停止:可主动的把结束分割线写上,后台可以成功获取
针对上述两种情况:应该是等成功响应信息后写入到本地的缓存文件中,保存断点信息
服务器端保持断点信息:开销过大,不予考虑。
5.多线程断点上传
原理与上述类似,但是缓存文件从一份变成多份,客户端应该保存多份缓存文件,每一次继续上传的时候先扫描缓存文件,判断文件上传的断点范围,确认每一块的起点和终点
6.多文件同时上传
原理相同,首先开启一个线程防止堵塞,接着传入一个File数组,然后使用for循环的方式写入到请求报文,注意空行分割,如果FIle的Content-type都不一样,最好也顺便设置文件的格式,也可以统一成二进制流格式。是否需要多线程是否需要断点,自行判断,但是性能问题需要自己度量。
7.数据与文件上传
原理同多文件上传一样,可以参考上面的请求报文去拼接内容然后发送,注意分割线还有content-position属性的设置即可。
8.单线程文件断点下载(可中断或暂停)
File file = new File("test.zip");
HttpURLConnection connection = (HttpURLConnection) new URL("http://39.108.158.15:8888/test.zip").openConnection();
connection.setRequestMethod("GET");
long sum = 0;
if (file.exists()) {
sum = file.length();
// 设置断点续传的开始位置
connection.setRequestProperty("Range", "bytes=" + file.length() + "-");
}
int code = connection.getResponseCode();
System.out.println("code = " + code);
if (code == 200 || code == 206) {
int contentLength = connection.getContentLength();
System.out.println("contentLength = " + contentLength);
contentLength += sum;
InputStream is = connection.getInputStream();
/*
* 创建一个向具有指定 name 的文件中写入数据的输出文件流。
* true表示当文件在下载过程中出现中断,
* 当再次链接网络时,将会从断点处追加。
* */
FileOutputStream fos = new FileOutputStream(file, true);
byte[] buffer = new byte[102400];
int length;
long startTime = System.currentTimeMillis();
while ((length = is.read(buffer)) != -1) {
fos.write(buffer, 0, length);
sum += length;
float percent = sum * 100.0f / contentLength;
System.out.print("\r[");
int p = (int) percent / 2;
/*
* 实现进度条
* */
for (int i = 0; i < 50; i++) {
if (i < p) {
System.out.print('=');
} else if (i == p){
System.out.print('>');
} else {
System.out.print(' ');
}
}
System.out.print(']');
System.out.printf("\t%.2f%%", percent);
long speed = sum * 1000 / (System.currentTimeMillis() - startTime);
if (speed > (1 << 20)) {
System.out.printf("\t%d MB/s", speed >> 20);
} else if (speed > (1 << 10)) {
System.out.printf("\t%d KB/s", speed >> 10);
} else {
System.out.printf("\t%d B/s", speed);
}
}
}
7.多线程文件断点下载(可中断或暂停)
原理是使用RandomAccess随机文件的机制,可以任意指定文件指针位置,我们可以开多个线程,每个线程都new一个file文件,这样子可以同时多线程的传输文件,再用缓存文件(txt)记录每一个线程的下载点,方便中断后重新从断点处下载,这也充分使用了http的头部range属性。
public class Test {
public static String path = "http://39.108.158.15:8888/test/test.docx";//下载文件的路径
public static String store = "test.docx";//文件存储路径包括文件名字
public static String temp = "";//临时文件的存储路径
public static String method = "GET";//请求方法
public static int threadNum = 2;//设置开启的线程数量
public static long sum = 0;//总下载进度
public static void main(String[] args) {
HttpURLConnection connection = getConnection(path, method);
try {
connection.connect();//打开连接
if (connection.getResponseCode() == 200) {
//获取文件的大小
int length = connection.getContentLength();
System.out.println(length);
File file = new File(store);
//创建一个空文件占位置用
RandomAccessFile raf = new RandomAccessFile(file, "rw");
//设置临时文件的大小
raf.setLength(length);
int size = length / threadNum;
for (int i = 0; i < threadNum; i++) {
int startIndex = i * size;
int endIndex = (i + 1) * size - 1;
if (i == threadNum - 1) endIndex = length;
System.out.println("start: " + startIndex + " end: " + endIndex);
new DownloadThread(startIndex, endIndex, i, raf).start();
}
long startTime=System.currentTimeMillis();
new Thread(()->{
while (sum<=length-1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
float percent = sum * 100.0f / length;
System.out.print("\r[");
int p = (int) percent / 2;
/*
* 实现进度条
* */
for (int j = 0; j < 50; j++) {
if (j < p) {
System.out.print('=');
} else if (j == p){
System.out.print('>');
} else {
System.out.print(' ');
}
}
System.out.print(']');
System.out.printf("\t%.2f%%", percent);
long speed = sum * 1000 / (System.currentTimeMillis() - startTime);
if (speed > (1 << 20)) {
System.out.printf("\t%d MB/s", speed >> 20);
} else if (speed > (1 << 10)) {
System.out.printf("\t%d KB/s", speed >> 10);
} else {
System.out.printf("\t%d B/s", speed);
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 获取连接
*
* @param path
* @return
*/
public static HttpURLConnection getConnection(String path, String method) {
HttpURLConnection connection = null;
try {
URL url = new URL(path);
connection = (HttpURLConnection) url.openConnection();
connection.setReadTimeout(6000);//设置读取超时时间
connection.setConnectTimeout(6000);//设置连接超时时间
connection.setRequestMethod(method);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
}
class DownloadThread extends Thread {
public int startIndex;
public int endIndex;
public int threadId;
public RandomAccessFile raf;
public DownloadThread(int startIndex, int endIndex, int threadId, RandomAccessFile raf) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.threadId = threadId;
this.raf = raf;
}
@Override
public void run() {
try {
//首先先到临时文件中查看是否有已经下载的字节数记录
File progressFile = new File(Test.temp + threadId + ".txt");
RandomAccessFile tempRaf;//每个线程对应一个file对象,文件指针才有作用
if (progressFile.exists()) {
tempRaf = new RandomAccessFile(progressFile, "rwd");
BufferedReader reader = new BufferedReader(new FileReader(progressFile));
String startIndex_str = reader.readLine();
if (null == startIndex_str || "".equals(startIndex_str)) {
this.startIndex = startIndex;
} else {
this.startIndex = Integer.parseInt(startIndex_str) - 1;//设置下载起点
}
Test.sum+=startIndex;
reader.close();
} else {
tempRaf = new RandomAccessFile(progressFile, "rwd");
}
HttpURLConnection connection = Test.getConnection(Test.path, Test.method);
connection.setRequestProperty("RANGE", "bytes=" + startIndex + "-" + endIndex);
connection.connect();
if (connection.getResponseCode() == 206) {
RandomAccessFile finalRaf=new RandomAccessFile(new File(store),"rw");
finalRaf.seek(startIndex);
InputStream is = connection.getInputStream();
byte[] buffer = new byte[2048];
int len = 0;
int total = 0;
while ((len = is.read(buffer)) != -1) {
finalRaf.write(buffer, 0, len);
total += len;
//将执行进度写入临时文件中
Test.sum+=len;
tempRaf.seek(0);
tempRaf.write((startIndex + total + "").getBytes("UTF-8"));
}
tempRaf.close();
is.close();
finalRaf.close();
}
//只要线程执行到这一步的话,就已经表明他已经完成了自己应该爬取的字节范围啦,所以要删除临时文件
progressFile.delete();
} catch (Exception e) {
e.printStackTrace();
}
}
}
class ProgressThread extends Thread{
@Override
public void run() {
super.run();
}
}
4.总结
URL是较为底层的实现http通讯的方式,实际上整个通讯是模拟游览器的传输做法,利用java封装过的HTTPURLConncetion可以做到相同的功能,写法无非就是设置好请求头部信息,请求实体内容,响应完成后可以获取响应类型进行判断,获取响应内容。但是在做文件上传的时候文件过大容易发生io阻塞,接着请学习NIO流来解决问题。