说说Web开发之远程与本地文件下载的细节
我想在开发Web项目的过程中,免不了的功能,应该就是上传和下载了吧。虽然这个功能很常见,但是我想,遇到坑的也不是一两个了吧。所以,就用这篇文章来写一写,在这开发中的问题。
关于上传的问题,这个还算好解决,没有什么大问题,在上传的时候,路径的话,可以选择为本地路径,或者为服务器的路径,如果是公司开发项目的话,这个肯定是放在服务器的某个目录下面的,或者放在存储云上(比如,七牛云存储,这个个人还是觉得比较好的),所以,上传的问题不是很多,而且在我的另外的文章中,对这个已经进行了几种方法的代码演示,所以这里就不进行过多描述了,如果不是很明白的,可以看看另外一篇文章“手把手教你玩转SpringMvc”。。。
咳咳,好了,进入本篇文章的正题了。关于下载文件的话,也同样有两种,一种就是下载本地的路径上的文件。比如:”C:\helloworld.pdf“,另外一种就是从远程服务器进行下载对应url的文件,就比如http://p09v5ioet.bkt.clouddn.com/797ec79c-56c2-4d7d-98d9-aeaa705fd336.pdf,其实就是网络资源。。好了,重点来了,发现两种路径有啥最大的不同了吗?是的,如果是本地的话,那么其实运用下载的时候使用的就是file协议,而当进行的是网络资源的时候,那么就会出现http,或者https这类似的协议(这两种用得是最多的)。所以,协议都不一样,那么通过不同的方法,肯定实现的方法也就会有差别了。下面的话,就介绍针对不同的情况,来进行文件的下载。
第一种:下载本地路径的文件
如果想实现的是这种情况的文件下载的话,可以采取最初的servlet的文件流下载的代码,这个不是很难,可以直接百度就可以明白。这里,说一下,如何基于SpringMVC框架的一种很清晰,明了的一种写法。
@ResponseBody
@RequestMapping(value = "/downloadcontract")
public ResponseEntity<byte[]> downLoadCurrentContract(Long id , String fileUrl , HttpServletRequest request,
HttpServletResponse response) throws Exception {
//这种方法只能读本地的文件,切记切记切记
try {
File file=new File(fileUrl);
HttpHeaders headers = new HttpHeaders();
String fileName = "合同文件.doc";
fileName = new String(fileName.getBytes("UTF-8"),"iso-8859-1");//为了解决中文名称乱码问题
headers.setContentDispositionFormData("attachment", fileName);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),
headers, HttpStatus.CREATED);
} catch (Exception e) {
e.printStackTrace();
//throw new RuntimeException("文件流内容读取错误");
}
return null;
这种方法是不是很简单呢?但是。请特别的记住一下,如果用这样的方式去请求下载一个网络资源的url的文件的话,那么在使用这个方法的时候,就会报一个”文件路径无法不存在“的错误,但是呢,直接在浏览器通过url进行访问的话,就会直接下载了对应的文件,这是为什么呢?其实,很简单,这是由于这种方法,它只能够进行本地文件的下载。所以,使用的时候注意一下就好了。
第二种:远程文件下载
直接上代码吧。远程的含义就不多说了,就是网络资源路径。。。。。。。。。
方法一:
@ResponseBody
@RequestMapping(value = "/downloadcontract")
public void downLoadCurrentContract(Long id , String fileUrl , HttpServletRequest request, HttpServletResponse response) throws Exception {
int pointIndex = fileUrl.lastIndexOf(".");
BufferedInputStream bis ;
//如果有找到符号".",
if(pointIndex != -1) {
String fileName = "合同文件" + fileUrl.substring(pointIndex, fileUrl.length());
URL url = new URL(fileUrl);
URLConnection conn = url.openConnection();
int filesize = conn.getContentLength(); // 取数据长度
bis = new BufferedInputStream(conn.getInputStream());
// 清空response
response.reset();
// 文件名称转换编码格式为utf-8,保证不出现乱码,这个文件名称用于浏览器的下载框中自动显示的文件名
response.addHeader("Content-Disposition",
"attachment;filename=" + new String(fileName.getBytes("utf-8"), "iso8859-1"));
response.addHeader("Content-Length", "" + filesize);
BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
// 从输入流中读入字节流,然后写到文件中
byte[] buffer = new byte[1024];
int nRead;
while ((nRead = bis.read(buffer, 0, 1024)) > 0) { // bis为网络输入流
os.write(buffer, 0, nRead);
}
bis.close();
os.flush();
os.close();
}
大家可以看到,这种方法就能够实现远程文件的下载了,其实就是用的原生的Java网络编程中的URLConnection对象来进行的,之前在学习的时候确实没怎么关注,只是单纯的知道这么个对象,现在回头看看,这才是靠近底层的内容,其他的框架封装的下载方法,很多都是基于这样的一种方法的。
另外,通过这种方法的话,默认下载的地方就是浏览器下载内容的保存地方,而且如果使用的是谷歌浏览器的话,是不会弹出一个对话框的,这是谷歌浏览器默认情况;如果是使用的火狐浏览器的话,那么就会弹出一个对话框来提示用户是否确认进行下载,还是下载完成后直接打开的选择。这是一个需要区分的地方。。
咳咳,还有,就是可能会碰到一个需求就是说,默认将下载的内容,放在一个地方,这样的话,就方便以后对下载文件内容的管理,比如就是想放在”C盘”,那么还可以利用下面的方法进行,其实类似上面的方法,只是提供给需要的人看看好了。
@ResponseBody
@RequestMapping(value = "/downloadcontract")
public void downLoadCurrentContract(Long id , String fileUrl , HttpServletRequest request,
URL theURL = new URL(fileUrl);
String filePath = "C:\\";
URLConnection con = theURL.openConnection();
String urlPath = con.getURL().getFile();
String fileFullName = "合同.doc";
if(fileFullName !=null){
byte[] buffer = new byte[4*1024];
int read;
String path = filePath + "/" +fileFullName;
File fileFolder = new File(filePath);
if(!fileFolder.exists()){
fileFolder.mkdirs();
}
InputStream in = con.getInputStream();
FileOutputStream os = new FileOutputStream(path);
while((read = in.read(buffer)) > 0){
os.write(buffer, 0, read);
}
os.close();
in.close();
}
}
三:网络编程的一些知识点
(1)首先说一下关于网络的两种协议,TCP与UDP
TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。
UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
比较:
UDP:
-
每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。
-
UDP传输数据时是有大小限制的,每个被传输的数据报必须限定在64KB之内。
-
UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。
TCP:
-
面向连接的协议,在socket之间进行数据传输之前必然要建立连接,所以在TCP中需要连接时间。
-
TCP传输数据大小限制,一旦连接建立起来,双方的socket就可以按统一的格式传输大的数据。
-
TCP是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。
应用:
TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输。但是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。
UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。例如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。
(2)细说Java网络编程中的常用类(用到的时候就知道多重要了,看起来并不是那么重要,但是至少需要了解)1:InteAddress类
Java中的InetAddress是一个代表IP地址的封装。IP地址可以由字节数组和字符串来分别表示,InetAddress将IP地址以对象的形式进行封装,可以更方便的操作和获取其属性。InetAddress没有构造方法,可以通过两个静态方法获得它的对象。
//根据主机名来获取对应的InetAddress实例
InetAddress ip = InetAddress.getByName("www.baidu.com");
//判断是否可达
System.out.println("baidu是否可达:" + ip.isReachable(2000));
//获取该InetAddress实例的IP字符串
System.out.println(ip.getHostAddress());
//根据原始IP地址(字节数组形式)来获取对应的InetAddress实例
InetAddress local = InetAddress.getByAddress(new byte[]{127,0,0,1});
System.out.println("本机是否可达:" + local.isReachable(5000));
//获取该InetAddress实例对应的全限定域名
System.out.println(local.getCanonicalHostName());
2.URL和URLConnection类
网络中的URL(Uniform Resource Locator)是统一资源定位符的简称。它表示Internet上某一资源的地址。通过URL我们可以访问Internet上的各种网络资源,比如最常见的WWW,FTP站点。
URL可以被认为是指向互联网资源的“指针”,通过URL可以获得互联网资源相关信息,包括获得URL的InputStream对象获取资源的信息,以及一个到URL所引用远程对象的连接URLConnection。 URLConnection对象可以向所代表的URL发送请求和读取URL的资源。通常,创建一个和URL的连接,需要如下几个步骤:
-
创建URL对象,并通过调用openConnection方法获得URLConnection对象;
-
设置URLConnection参数和普通请求属性;
-
向远程资源发送请求;
-
远程资源变为可用,程序可以访问远程资源的头字段和通过输入流来读取远程资源返回的信息。
这里需要重点讨论一下第三步:如果只是发送GET方式请求,使用connect方法建立和远程资源的连接即可;如果是需要发送POST方式的请求,则需要获取URLConnection对象所对应的输出流来发送请求。
这里需要注意的是,由于GET方法的参数传递方式是将参数显式追加在地址后面,那么在构造URL对象时的参数就应当是包含了参数的完整URL地址,而在获得了URLConnection对象之后,就直接调用connect方法即可发送请求。而POST方法传递参数时仅仅需要页面URL,而参数通过需要通过输出流来传递。另外还需要设置头字段。以下是两种方式的代码:
//1. 向指定URL发送GET方法的请求
String urlName = url + "?" + param;
URL realUrl = new URL(urlName);
//打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent","Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
//建立实际的连接
conn.connect();
//2. 向指定URL发送POST方法的请求
URL realUrl = new URL(url);
//打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
//发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
//获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
//发送请求参数
out.print(param);
3.URLDecoder和URLEncoder
这两个类可以别用于将application/x-www-form-urlencoded MIME类型的字符串转换为普通字符串,将普通字符串转换为这类特殊型的字符串。使用URLDecoder类的静态方法decode()用于解码,URLEncoder类的静态方法encode()用于编码。具体使用方法如下:
//将application/x-www-form-urlencoded字符串转换成普通字符串
String keyWord = URLDecoder.decode("%E6%9D%8E%E5%88%9A+j2ee", "UTF-8");
System.out.println(keyWord);
//将普通字符串转换成 application/x-www-form-urlencoded字符串
String urlStr = URLEncoder.encode( "ROR敏捷开发最佳指南" , "GBK");
System.out.println(urlStr);
4.Socket和ServerSocket类
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。 但是,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必然联系的。
在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。 Server端Listen(监听)某个端口是否有连接请求,Client端向Server端发出Connect(连接)请求,Server端向Client端发回Accept(接受)消息。一个连接就建立起来了。Server端和Client端都可以通过Send,Write等方法与对方通信。
5.DatagramSocket类
UDP协议是一种不可靠的网络协议,它在通讯实例的两端个建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送和接受数据报的对象。 包java.net中提供了两个类DatagramSocket和DatagramPacket用来支持数据报通信,DatagramSocket用于在程序之间建立传送数据报的通信连接, DatagramPacket则用来表示一个数据报。 DatagramSocket的构造方法:
DatagramSocket();
DatagramSocket(int prot);
DatagramSocket(int port, InetAddress laddr);
其中,port指明socket所使用的端口号,如果未指明端口号,则把socket连接到本地主机上一个可用的端口。laddr指明一个可用的本地地址。给出端口号时要保证不发生端口冲突,否则会生成SocketException类例外。注意:上述的两个构造方法都声明抛弃非运行时例外SocketException,程序中必须进行处理,或者捕获、或者声明抛弃。
用数据报方式编写client/server程序时,无论在客户方还是服务方,首先都要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket类对象作为传输数据的载体。好了,关于本地和远程的下载及其网络编程的一些知识,就介绍这么多了,说到底,都是通过二进制和流的方式进行的,所以要明白这其中的底层内容就会比较的好理解,只是想说,在开发的过程遇到的问题,努力努力的去钻研一下,多看看底层的具体代码就可以看出不一样的,最后就会修复BUG,继续加油呗~!~~另外,感谢网络上面的关于这方面的一些资料和一位博主的共同探讨。。
推荐阅读