1. URL类:
1) 其对象用来表示一个URL地址;
2) URL格式:protocol://host_name:port/resource_path?arg1=val1&arg2=val2...
3) 构造器:URL(String spec); // spce就是一个URL地址(字符串)
4) 通过URL提供的对象方法获取关于URL的信息:
i. String getProtocol(); // 返回protocol部分
ii. String getHost(); // 返回host_name部分
iii. int getPort(); // 返回port这个数字(整型)
iv. String getPath(); // 返回/resource_path,注意开头的/表示应用程序的环境根目录
v. String getQuery(); // 返回arg1=val1&arg2=val2...部分,即请求参数部分
vi. String getFile(); // 返回/resource_path到最后(包括中间的?),这里的file指的是资源,返回的是资源的完整名称,包括请求参数部分
!!以上方法对URL的解析完全就是基于字符串的,如果你传的URL是"www.xxx.com",而你调用的是getProtocol则会抛出异常,因为你给的字符串中并没有协议部分!
!!因此URL给出的解析方法仅仅就是纯字符串层面上的解析,并不会像大多数浏览器那样可以智能解析,比如你在浏览器中只输入"www.xxx.com"就可以自动判断出该网站使用的通信协议,而在底层自动补齐缺少的URL部分;
5) URL的最重要的对象方法就是openConnection,用于和服务器建立连接:
i. 原型:URLConnection openConnection();
ii. 该方法会根据里面包装的URL地址(构造时传入的URL地址)和目标服务器建立连接,并返回表示该连接的句柄URLConnection对象;
iii. 该步骤就是三次握手协议的第一步,即建立连接(后面两步是请求、应答),但是其它什么都没做;
2. 使用URLConnection和服务器进行交流:
1) URLConnection类的对象表示本机与URL所引用的资源的远程连接的对象;
2) 获的了URLConnection连接句柄后只是完成了三次握手的第一步,即建立连接,但并没有发生实质的数据交流;
3) 但在进行数据交流之前通常应该先商量好一些前提,比如客户端能接受什么语言、什么编码,这次只交换什么类型的数据等等,这都需要在进行数据交换之前先商量好,这就需要设置请求条件了;
4) 设置请求条件:使用URLConnection提供的对象方法进行设置
i. void setConnectTimeout(int timeout); // 设置客户端等待响应时长(单位是毫秒)
ii. void setRequestMethod(String method); // 设置请求方式,只能是"GET"、"POST"、"TRACE"等字符串中之一,但必须是服务器端支持的,否则会抛出异常
iii. void setRequestProperty(String key, String value); // 设置请求属性(即标头中的键值对,在标头中格式是"key:value"),如果有多条属性那就多次调用该方法一条一条添加
!!只有设置完请求条件之后才能正常通信;
5) 通过connection向服务器端发送请求:还是调用URLConnection的对象方法完成,这里只介绍几种常用的方法
i. int getContentLength(); // 请求资源的大小(服务器返回URL定位的资源的大小)
ii. InputStream getInputStream(); // 请求远程资源的下载链接,返回的InputStream其实不在本地的内存中,而是在服务器端的内存中,但用起来跟平时使用的InputStream没有两样,Java在底层隐藏了这些细节,在调用其read方法时其实是从远程下载到本地
!!完整的一次上述方法的调用就是完成了三次握手的后两步(请求-响应);
6) 实际使用的时候用的是URLConnection的实现类,URLConnection过于抽象,现实中进行网络通信都是要基于某种通信协议的,其中最常用的就是HTTP协议,因此用的最广泛的一个实现类就是HttpURLConnection了,前面讲的方法它都有,这里再补充一个HttpURLConnection独有的方法disconnect:
i. 原型:void HttpURLConnection.disconnect(); // 断开本次连接
ii. 每当完成一次完整的请求响应就应该及时关闭连接资源,这很重要,否则这些资源会一直占用内存,如果局部数据庞大会导致运行效率下降;
3. 示例:多线程下载工具
// DownUitl.java
public class DownUtil {
private String url; // 目标资源的URL
private String savePath; // 下载的文件保存在本机的位置
private int threadNum; // 线程数
private int resSize; // 资源的大小(字节)
private DownThread[] threads; // 下载线程组
public DownUtil(String url, String savePath, int threadNum) {
this.url = url;
this.savePath = savePath;
this.threadNum = threadNum;
threads = new DownThread[threadNum];
}
private static HttpURLConnection getConnection(String url) throws IOException {
URL urlObj = new URL(url); // 创建URL对象
HttpURLConnection conn = (HttpURLConnection)urlObj.openConnection(); // 建立Http连接
conn.setConnectTimeout(5000); // 设置响应时间不超过5秒
conn.setRequestMethod("GET"); // 请求方式为GET
// 设置请求属性(标头中的内容)
conn.setRequestProperty("Accept", // 设置请求端可以接受的资源类型(用MIME指定)
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, " +
"application/x-shockwave-flash, application/xaml+xml, " +
"application/vnd.ms-xpsdocument, application/x-ms-xbap, " +
"application/x-ms-application, application/vnd.ms-excel, " +
"application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language", "zh-CN"); // 设置请求端可接受的编码
conn.setRequestProperty("Charset", "UTF-8"); // 设置通信使用的字符集
conn.setRequestProperty("Connection", "Keep-Alive"); // 要求连接不是一次性的,而是长时间保持
return conn;
}
public void download() throws IOException {
HttpURLConnection conn = getConnection(url);
resSize = conn.getContentLength(); // 获取资源的大小(字节)
conn.disconnect(); // 此次建立连接仅仅是为了获取resSize信息为后面的下载做准备,因此就先关闭连接
int partSize = resSize / threadNum + 1; // 计算每条线程平均下载多少字节
RandomAccessFile totalFile = new RandomAccessFile(savePath, "rw"); // 然后创建保存的文件
totalFile.setLength(resSize); // 只预留空间,但先不下载东西进去,下载的工作留给线程处理
totalFile.close(); // 仅仅创建一个文件,创建完就关掉
for (int i = 0; i < threadNum; i++) {
// 每个线程写的都是同一个文件,因此文件不能加锁(不同步)
// 直接并发访问,只不过每条线程访问文件的不同部分,这些部分之间没有交集
// 因此没关系
int startPos = i * partSize; // 每个线程的起始下载点
RandomAccessFile partFile = new RandomAccessFile(savePath, "rw"); // 获取文件句柄
partFile.seek(startPos); // 定位到起始点
threads[i] = new DownThread(i + 1, startPos, partSize, partFile); // 创建并启动线程
threads[i].start();
}
}
public double getCompleteRate() {
int sum = 0;
for (int i = 0; i < threadNum; i++) {
sum += threads[i].getCompleted();
}
return sum * 100.0 / resSize;
}
private class DownThread extends Thread {
private int threadNo;
private int startPos;
private int partSize;
private RandomAccessFile partFile;
private int completed; // 当前完成了多少字节
public DownThread(int threadNo, int startPos, int partSize, RandomAccessFile partFile) {
this.threadNo = threadNo;
this.startPos = startPos;
this.partSize = partSize;
this.partFile = partFile;
this.setName("Sub Thread " + this.threadNo);
}
public int getCompleted() {
return completed;
}
@Override
public void run() {
// TODO Auto-generated method stub
// super.run();
try {
System.out.println(getName() + " started!");
HttpURLConnection conn = getConnection(url); // 获取连接
InputStream is = conn.getInputStream(); // 获取远程资源的输入流
is.skip(startPos); // 跳过startPos个字节,从自己负责的那部分开始下载
byte[] buf = new byte[1024]; // 下载缓冲区
int lenRead = 0; // 每次读取了多少个字节
while (completed < partSize && (lenRead = is.read(buf)) != -1) {
partFile.write(buf, 0, lenRead);
completed += lenRead;
} // 多处partSize的部分会被其他线程覆盖掉,没关系
partFile.close(); // 关闭资源
is.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
// Test.java
public class Test {
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
final DownUtil downUtil = new DownUtil("http://www.crazyit.org/"
+ "attachments/month_1403/1403202355ff6cc9a4fbf6f14a.png"
, "ios.png", 4);
downUtil.download();
Thread showComplete = new Thread(() -> { // 实时显示完成比例的线程放入后台
while (true) {
System.out.println(downUtil.getCompleteRate() + "% compeleted.");
try {
Thread.sleep(1000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, "Complete Rate Thread");
showComplete.setDaemon(true);
showComplete.start();
}
}
!!还可以基于上面的代码实现断点下载,只不过断点下载还需要一个配置文件,记录每次断开之前下载到了哪个位置;