17.2.3 URL URLConnection和URLPermission

17.2.3 URL URLConnection和URLPermission

URL

URL(Uniform Resource Locator)对象代表统一资源定位器,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂对象的引用,例如对数据库或搜索引擎的查询。在通常情况下,URL可以由协议名主机端口资源组成,即满足如下格式:

URL格式

1
protocol://host:port/resourceName

例如如下的URL地址:

1
https://lanlan2017.github.io/JavaReadingNotes/

URI

JDK中还提供了一个URI(Uniform Resource Identifiers)类,其实例代表一个统一资源标识符,JavaURI不能用于定位任何资源,它的唯一作用就是解析。与此对应的是**URL则包含一个可打开到达该资源的输入流**,可以将URL理解成URI的特例.

URL方法

URL类提供了多个构造器用于创建URL对象,一旦获得了URL对象之后,就可以调用如下方法来访问该URL对应的资源。

方法 描述
String getFile() 获取该URL的资源名
String getHost() 获取该URL的主机名
String getPath() 获取该URL的路径部分
int getPort() 获取该URL的端口号
String getProtocol() 获取该URL的协议名称
String getQuery() 获取该URL的查询字符串部分
URLConnection openConnection() 返回一个URLConnection对象,它代表了与URL所引用的远程对象的连接
InputStream openStream() 打开与此URL的连接,并返回一个用于读取该URL资源的InputStream

程序示例 多线程下载工具类

URL对象中的前面几个方法都非常容易理解,而该对象提供的openStream方法可以读取该URL资源的InputStream,通过该方法可以非常方便地读取远程资源—甚至实现多线程下载。如下程序实现了一个多线程下载工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.*;

public class DownUtil {
// 定义下载资源的路径
private String path;
// 指定所下载的文件的保存位置
private String targetFile;
// 定义需要使用多少线程下载资源
private int threadNum;
// 定义下载的线程对象
private DownThread[] threads;
// 定义下载的文件的总大小
private int fileSize;

public DownUtil(String path, String targetFile, int threadNum) {
this.path = path;
this.threadNum = threadNum;
// 初始化threads数组
threads = new DownThread[threadNum];
this.targetFile = targetFile;
}

public void download() throws Exception {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept",
"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");
// 得到文件大小
fileSize = conn.getContentLength();
conn.disconnect();
int currentPartSize = fileSize / threadNum + 1;
RandomAccessFile file = new RandomAccessFile(targetFile, "rw");
// 设置本地文件的大小
file.setLength(fileSize);
file.close();
for (int i = 0; i < threadNum; i++) {
// 计算每条线程的下载的开始位置
int startPos = i * currentPartSize;
// 每个线程使用一个RandomAccessFile进行下载
RandomAccessFile currentPart = new RandomAccessFile(targetFile, "rw");
// 定位该线程的下载位置
currentPart.seek(startPos);
// 创建下载线程
threads[i] = new DownThread(startPos, currentPartSize, currentPart);
// 启动下载线程
threads[i].start();
}
}

// 获取下载的完成百分比
public double getCompleteRate() {
// 统计多条线程已经下载的总大小
int sumSize = 0;
for (int i = 0; i < threadNum; i++) {
sumSize += threads[i].length;
}
// 返回已经完成的百分比
return sumSize * 1.0 / fileSize;
}

private class DownThread extends Thread {
// 当前线程的下载位置
private int startPos;
// 定义当前线程负责下载的文件大小
private int currentPartSize;
// 当前线程需要下载的文件块
private RandomAccessFile currentPart;
// 定义已经该线程已下载的字节数
public int length;

public DownThread(int startPos, int currentPartSize, RandomAccessFile currentPart) {
this.startPos = startPos;
this.currentPartSize = currentPartSize;
this.currentPart = currentPart;
}

@Override
public void run() {
try {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept",
"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");
InputStream inStream = conn.getInputStream();
// 跳过startPos个字节,表明该线程只下载自己负责哪部分文件。
inStream.skip(this.startPos);
byte[] buffer = new byte[1024];
int hasRead = 0;
// 读取网络数据,并写入本地文件
while (length < currentPartSize && (hasRead = inStream.read(buffer)) != -1) {
currentPart.write(buffer, 0, hasRead);
// 累计该线程下载的总大小
length += hasRead;
}
currentPart.close();
inStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

上面程序中定义了DownThread线程类,该线程负责读取从start开始,到end结束的所有字节数据,并写入RandomAccessfile对象。这个DownThread线程类的run()方法就是一个简单的输入、输出实现。
程序中DownUtils类中的download方法负责按如下步骤来实现多线程下载

  1. 创建URL对象。
  2. 获取指定URL对象所指向资源的大小(通过getContentLength()方法获得),此处用到了URLConnection类,该类代表Java应用程序和URL之间的通信链接。后面还有关于URLConnection更详细的介绍。
  3. 在本地磁盘上创建一个与网络资源具有相同大小的空文件。
  4. 计算每个线程应该下载网络资源的哪个部分(从哪个字节开始,到哪个字节结束)
  5. 依次创建、启动多个线程来下载网络资源的指定部分。

如何实现断点下载

上面程序已经实现了多线程下载的核心代码,如果要实现断点下载,则需要额外増加个配置文件(读者可以发现,所有的断点下载工具都会在下载开始时生成两个文件:一个是与网络资源具有相同大小的空文件,一个是配置文件),该配置文件分别记录每个线程已经下载到哪个字节,当网络断开后再次开始下载时,每个线程根据配置文件里记录的位置向后下载即可
有了上面的DownUtils工具类之后,接下来就可以在主程序中调用该工具类的down()方法执行下载,如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MultiThreadDown {
public static void main(String[] args) throws Exception {
// 初始化DownUtil对象
// final DownUtil downUtil = new DownUtil(
// "http://www.crazyit.org/" +
// "attachments/month_1403/1403202355ff6cc9a4fbf6f14a.png", "ios.png", 4);
String urlStr = "https://lanlan2017.github.io/download/TestJavaDownload_DoNotDelete/TestDownLoad.txt";
String targetFile = "TestDownLoad.md";
final DownUtil downUtil = new DownUtil(urlStr, targetFile, 4);
// 开始下载
downUtil.download();
new Thread(() -> {
while (downUtil.getCompleteRate() < 1) {
// 每隔1秒查询一次任务的完成进度,
// GUI程序中可根据该进度来绘制进度条
System.out.println("已完成:" + (downUtil.getCompleteRate() * 100) + "%");
try {
Thread.sleep(1000);
} catch (Exception ex) {
}
}
System.out.println("已完成:" + "100%");
}).start();

}
}

运行上面程序,即可看到程序从lanlan2017.github.io下载得到一份名为TestDownLoad.md的文本文件。
上面程序还用到URLConnectionHttpURLConnection对象,其中URLConnection表示应用程序和URL之间的通信连接,HttpURLConnection表示与URL之间的HTTP连接。程序可以通过URLConnection实例向该URL发送请求、读取URL引用的资源

URLPermission

Java8新增了一个URLPermission工具类,用于管理HttpURLConnection的权限问题,如果在HttpURLConnection安装了安全管理器,通过该对象打开连接时就需要先获得权限。

通常创建一个和URL的连接,并发送请求、读取此URL引用的资源需要如下几个步骤

  • 通过调用URL对象的openConnection方法来创建URLConnection对象。
  • 设置URLConnection的参数和普通请求属性。
  • 如果只是发送GET方式请求,则使用connect()方法建立和远程资源之间的实际连接即可;
  • 如果需要发送POST方式的请求,则需要获取URLConnection对象对应的输出流来发送请求参数。
  • 远程资源变为可用,程序可以访问远程资源的头字段或通过输入流读取远程资源的数据

URLConnection设置请求头方法

在建立和远程资源的实际连接之前,程序可以通过如下方法来设置请求头字段

方法 描述
void setAllowUserInteraction(boolean allowuserinteraction) 设置该URLConnectionallowUserInteraction请求头字段的值
void setDoInput(boolean doinput) 设置该URLConnectiondoInput请求头字段的值。
void setDoOutput(boolean dooutput) 设置该URLConnectiondoOutput请求头字段的值。
void setIfModifiedSince(long ifModifiedSince) 设置该URLConnectionifModifiedSince请求头字段的值
void setUseCaches(boolean usecaches) 设置该URLConnectionuseCaches请求头字段的值。

URLConnection设置通用请求头

除此之外,还可以使用如下方法来设置或增加通用头字段。

方法 描述
void setRequestProperty(String key, String value) 设置该URLConnectionkey请求头字段的值为value
void addRequestProperty(String key, String value) 为该URLConnectionkey请求头字段增加value值,该方法并不会覆盖原请求头字段的值,而是将新值追加到原请求头字段中。
1
urlConnection.setRequestProperty("accept","*/*");

访问头字段和内容

当远程资源可用之后,程序可以使用以下方法来访问头字段和内容

方法 描述
String getContentEncoding() 获取content-encoding响应头字段的值。
int getContentLength() 获取content-Length响应头字段的值。
String getContentType() 获取content-type响应头字段的值。
long getDate() 获取date响应头字段的值。
long getExpiration() 获取expires响应头字段的值。
long getLastModified() 获取last-modified响应头字段的值。

如果既要使用输入流读取URLConnection响应的内容,又要使用输出流发送请求参数,则一定要先使用输出流,再使用输入流

程序示例 发送GET请求 发送POST请求

下面程序示范了如何向Web站点发送GET请求、POST请求,并从Web站点取得响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import java.io.*;
import java.net.*;
import java.util.*;

public class GetPostTest {
/**
* 向指定URL发送GET方法的请求
*
* @param url 发送请求的URL
* @param param 请求参数,格式满足name1=value1&name2=value2的形式。
* @return URL所代表远程资源的响应
*/
public static String sendGet(String url, String param) {
String result = "";
String urlName = url + "?" + param;
try {
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();
// 获取所有响应头字段
Map<String, List<String>> map = conn.getHeaderFields();
// 遍历所有的响应头字段
for (String key : map.keySet()) {
System.out.println(key + "--->" + map.get(key));
}
try (
// 定义BufferedReader输入流来读取URL的响应
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
String line;
while ((line = in.readLine()) != null) {
result += "\n" + line;
}
}
} catch (Exception e) {
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
return result;
}

/**
* 向指定URL发送POST方法的请求
*
* @param url 发送请求的URL
* @param param 请求参数,格式应该满足name1=value1&name2=value2的形式。
* @return URL所代表远程资源的响应
*/
public static String sendPost(String url, String param) {
String result = "";
try {
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);
try (
// 获取URLConnection对象对应的输出流
PrintWriter out = new PrintWriter(conn.getOutputStream())) {
// 发送请求参数
out.print(param);
// flush输出流的缓冲
out.flush();
}
try (
// 定义BufferedReader输入流来读取URL的响应
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
String line;
while ((line = in.readLine()) != null) {
result += "\n" + line;
}
}
} catch (Exception e) {
System.out.println("发送POST请求出现异常!" + e);
e.printStackTrace();
}
return result;
}

// 提供主方法,测试发送GET请求和POST请求
public static void main(String args[]) {
// 发送GET请求
String s = GetPostTest.sendGet("http://localhost:8080/abc/a.jsp", null);
System.out.println(s);
// 发送POST请求
String s1 = GetPostTest.sendPost("http://localhost:8080/abc/login.jsp", "name=crazyit.org&pass=leegang");
System.out.println(s1);
}
}

如果发送GET请求时,只需将请求参数放在URL字符串之后,以?隔开,然后程序直接调用URLConnection对象的connect()方法即可发送Get请求;
如果程序要发送POST请求,则需要先设置doIndoOut两个请求头字段的值,再使用URLConnection对应的输出流来发送请求参数:

1
2
3
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);

不管是发送GET请求,还是发送POST请求,程序获取URLConnection响应的方式完全一样.如果程序可以确定远程响应是字符流,则可以使用字符流来读取;如果程序无法确定远程响应是字符流则使用字节流读取即可。

上面程序中发送请求的两个URL是部署在本机的Web应用abc,
由于程序可以使用这种方式向服务器发送请求,相当于提交Web应用中的登录表单页,这样就可以让程序不断地变换用户名、密码来提交登录请求,直到返回登录成功,这就是所谓的暴力破解

Web应用abc

该Web应用的源代码如下:

展开/折叠

项目结构

1
2
3
4
5
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\17\17.2\abc
├─a.jsp
├─login.jsp
└─WEB-INF\
└─web.xml

a.jsp

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title> 测试页面 </title>
<meta name="website" content="http://www.crazyit.org"/>
</head>
<body>
服务器时间为:<%=new java.util.Date()%>
</body>
</html>

login.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ page contentType="text/html; charset=UTF-8" language="java" errorPage="" %>
<%
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");
String pass = request.getParameter("pass");
if(name.equals("crazyit.org")
&& pass.equals("leegang"))
{
out.println("登录成功!");
}
else
{
out.println("登录失败!");
}
%>

web.xml

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">

</web-app>