1)简介
在当今的网络环境中, 特别是企业网络环境中, 应用程序开发人员必须像系统管理员一样频繁地处理代理. 在某些情况下, 应用程序应该使用系统默认设置, 在其他情况下, 我们希望能够非常严格地控制通过哪个代理服务器, 并且在中间的某个地方, 大多数应用程序都乐于通过为用户提供设置代理设置的 GUI, 来将决策委派给用户, 就像在大多数浏览器中一样.
在任何情况下, 像 Java 这样的开发平台应该提供处理这些强大且灵活的代理的机制. 不幸的是, 直到最近, Java 平台在该领域还不是很灵活. 但是, 为了解决这个缺点, 已经引入了 J2SE 5.0 作为新 API 的所有变化, 本文的目的是提供对所有这些 API 和机制的深入解释, 旧的仍然有效, 以及新的.
2)系统属性
直到 J2SE 1.4 系统属性是在任何协议处理程序的 Java 网络 API 中设置代理服务器的唯一方法. 为了使事情变得更复杂, 这些属性的名称已从一个版本更改为另一个版本, 其中一些现在已经过时, 即使它们仍然支持兼容性.
使用系统属性的主要限制是它们是 "全有或全无" 开关. 这意味着一旦为特定协议设置了代理, 它将影响该协议的所有连接. 这是 VM 广泛的行为.
设置系统属性有两种主要方法:
作为调用 VM 时的命令行选项
使用该
System.setProperty(String, String)
方法, 当然假设您有权这样做.
现在, 让我们一个协议一个协议的看一下可用于设置代理的属性. 所有代理都由主机名和端口号定义. 后者是可选的, 如果未指定, 将使用标准默认端口.
2.1)HTTP
您可以设置 3 个属性来指定代理使用 http 协议处理程序:
http.proxyHost: 代理服务器的主机名
http.proxyPort: 端口号, 默认值为 80.
http.nonProxyHosts: 绕过代理直接到达的主机列表. 这是由 "|" 分隔的模式列表. 对于通配符, 模式可以以'*'开头或结尾. 匹配这些模式之一的任何主机都将通过直接连接而不是通过代理来访问.
让我们看几个例子, 假设我们正在尝试执行 GetURL 类的 main 方法:
$ java -Dhttp.proxyHost = webcache.mydomain.com GetURL
所有 http 连接都将通过侦听在 80 端口的 webcache.mydomain.com 代理服务器(我们没有指定任何端口, 因此使用默认端口).
再看一个示例:
- $ java -Dhttp.proxyHost=webcache.mydomain.com -Dhttp.proxyPort=8080
- -Dhttp.noProxyHosts="localhost|host.mydomain.com" GetURL
在这个示例中, 代理服务器仍然处于 webcache.mydomain.com, 但这次侦听端口 8080. 此外, 连接到 localhost 或 host.mydonain.com 时, 将不使用代理.
如前所述, 在 VM 的整个生命周期内, 这些设置都会影响使用这些选项调用的所有 http 连接. 但是, 使用 System.setProperty()方法可以实现稍微更动态的行为.
这是一段代码摘录, 展示了如何做到这一点:
- //Set the http proxy to webcache.mydomain.com:8080
- System.setProperty("http.proxyHost", "webcache.mydomain.com");
- System.setPropery("http.proxyPort", "8080");
- // Next connection will be through proxy.
- URL url = new URL("http://java.sun.com/");
- InputStream in = url.openStream();
- // Now, let's'unset' the proxy.
- System.setProperty("http.proxyHost", null);
- // From now on http connections will be done directly.
现在, 这种方法运行得相当好, 即使有点麻烦, 但如果您的应用程序是多线程的, 它会变得棘手. 请记住, 系统属性是 "VM wide" 设置, 因此所有线程都会受到影响. 这意味着, 这种方式将会带来副作用: 一个线程中的代码可能会使另一个线程中的代码无法运行.
2.2)HTTPS
https(http over SSL)协议处理程序有自己的一组属性:
- htttps.proxyHost
- https.proxyPort
正如你可能猜到这些工作方式与 http 对应方式完全相同, 所以我们不会详细介绍, 除非提到默认端口号, 和 http 不一样它是 443, 而对于 "非代理主机" 列表, HTTPS 协议处理程序将使用与 http 处理程序相同的方式(即 http.nonProxyHosts).
2.3)FTP
FTP 协议处理程序的设置遵循与 http 相同的规则, 唯一的区别是每个属性名称现在都以 "ftp." 为前缀. 而不是'http.'
因此系统属性是:
- ftp.proxHost
- ftp.proxyPort
- ftp.nonProxyHosts
请注意, 在这里,"非代理主机" 列表有一个单独的属性. 此外, 对于 http, 默认端口号值为 80. 应该注意的是, 当通过代理时, FTP 协议处理程序实际上将使用 HTTP 向代理服务器发出命令, 这很好的说明了为什么他们是相同的默认端口号.
我们来看一个简单的例子:
- $ java -Dhttp.proxyHost = webcache.mydomain.com
- -Dhttp.proxyPort = 8080 -Dftp.proxyHost = webcache.mydomain.com -Dftp.proxyPort = 8080 GetURL
在这里, HTTP 和 FTP 协议处理程序将在 webcache.mydomain.com:8080 上使用相同的代理服务器.
2.4)SOCKS
RFC 1928 中定义的 SOCKS 协议为客户端服务器应用程序提供了一个框架, 以便在 TCP 和 UDP 级别安全地遍历防火墙. 从这个意义上说, 它比更高级别的代理 (如 HTTP 或 FTP 特定代理) 更通用. J2SE 5.0 为客户端 TCP 套接字提供 SOCKS 支持.
有两个与 SOCKS 相关的系统属性:
socksProxyHost 用于 SOCKS 代理服务器的主机名
socksProxyPort 对于端口号, 默认值为 1080
请注意, 此时前缀后面没有点('.'). 这是出于历史原因并确保向后兼容性. 以这种方式指定 SOCKS 代理后, 将通过代理尝试所有 TCP 连接.
例:
$ java -DsocksProxyHost = socks.mydomain.com GetURL
在这里, 在执行代码期间, 每个传出的 TCP 套接字都将通过 SOCKS 代理服务器 socks.mydomain.com:1080.
思考一下, 当同时定义 SOCKS 代理和 HTTP 代理时会发生什么? 规则是, 更高级别协议 (如 HTTP 或 FTP) 的设置优先于 SOCKS 设置. 因此, 在该特定情况下, 在建立 HTTP 连接时, 将忽略 SOCKS 代理设置并且将使用 HTTP 代理. 我们来看一个例子:
- $ java -Dhttp.proxyHost = webcache.mydomain.com -Dhttp.proxyPort = 8080
- -DsocksProxyHost = socks.mydomain.com GetURL
这里, 一个 http URL 将通过 webcache.mydomain.com:8080 代理服务器, 因为 http 设置优先. 但是 ftp URL 怎么样? 由于没有为 FTP 分配特定的代理设置, 并且由于 FTP 位于 TCP 之上, 因此将通过 SOCKS 代理服务器尝试 FTP 连接 socks.mydomsain.com:1080. 如果已指定 FTP 代理, 则将使用该代理.
3)代理类
正如我们所看到的, 系统属性很强大, 但不灵活. 大多数开发人员都认为 "全有或全无" 的行为太严重了. 这就是为什么决定在 J2SE 5.0 中引入一个新的, 更灵活的 API, 以便可以使用基于连接的代理设置.
这个新 API 的核心是 Proxy 类, 它代表一个代理定义, 通常是一个类型 (http,socks) 和一个套接字地址. 从 J2SE 5.0 开始, 有 3 种可能的类型:
DIRECT 代表直接连接或缺少代理.
HTTP 表示使用 HTTP 协议的代理.
SOCKS 它代表使用 SOCKS v4 或 v5 的代理.
因此, 为了创建 HTTP 代理对象, 您可以调用:
- SocketAddress addr = new InetSocketAddress("webcache.mydomain.com", 8080);
- Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);
请记住, 这个新的代理对象代表了一个代理定义, 仅此而已. 我们如何使用这样的对象? URL 类中添加了一个新方法 openConnection(), 并将 Proxy 作为参数, 它的工作方式与不带参数 openConnection()的方式相同, 但它强制通过指定的代理建立连接, 忽略所有其他设置, 包括上文提到的系统属性.
所以继续前面的例子, 我们现在可以添加:
- URL url = new URL("http://java.sun.com/");
- URConnection conn = url.openConnection(proxy);
很简单, 不是吗?
可以使用相同的机制来指定必须直接访问特定 URL, 例如, 它位于 Intranet 上. 这就是 DIRECT 类型发挥作用的地方. 但是, 您不需要使用 DIRECT 类型创建代理实例, 您只需使用 NO_PROXY 静态成员:
- URL url2 = new URL("http://infos.mydomain.com/");
- URLConnection conn2 = url2.openConnection(Proxy.NO_PROXY);
现在, 这可以保证您通过绕过任何其他代理设置的直接连接来检索此特定 URL, 这很方便.
请注意, 您也可以强制 URLConnection 通过 SOCKS 代理:
- SocketAddress addr = new InetSocketAddress("socks.mydomain.com", 1080);
- Proxy proxy = new Proxy(Proxy.Type.SOCKS, addr);
- URL url = new URL("ftp://ftp.gnu.org/README");
- URLConnection conn = url.openConnection(proxy);
将通过指定的 SOCKS 代理尝试该特定的 FTP 连接. 如您所见, 它非常简单.
最后, 但并非最不重要的是, 您还可以使用新引入的套接字构造函数为各个 TCP 套接字指定代理:
- SocketAddress addr = new InetSocketAddress("socks.mydomain.com", 1080);
- Proxy proxy = new Proxy(Proxy.Type.SOCKS, addr);
- Socket socket = new Socket(proxy);
- InetSocketAddress dest = new InetSocketAddress("server.foo.com", 1234);
- socket.connect(dest);
这里套接字将尝试通过指定的 SOCKS 代理连接到其目标地址(server.foo.com:1234).
对于 URL, 可以使用相同的机制来确保无论全局设置是什么, 都应该尝试直接 (即不通过任何代理) 连接:
- Socket socket = new Socket(Proxy.NO_PROXY);
- socket.connect(new InetAddress("localhost", 1234));
请注意, 从 J2SE 5.0 开始, 这个新构造函数只接受两种类型的代理: SOCKS 或 DIRECT(即 NO_PROXY 实例).
4)ProxySelector
正如您所看到的, 使用 J2SE 5.0, 开发人员在代理方面获得了相当多的控制和灵活性. 仍然有一些情况下, 人们想要决定动态使用哪个代理, 例如在代理之间进行一些负载平衡, 或者取决于目的地, 在这种情况下, 到目前为止描述的 API 将非常麻烦. 这就是 ProxySelector 发挥作用的地方.
简而言之, ProxySelector 是一段代码, 它将告诉协议处理程序对任何给定的 URL 使用哪个代理(如果有). 例如, 请考虑以下代码:
- URL url = new URL("http://java.sun.com/index.html");
- URLConnection conn = url.openConnection();
- InputStream in = conn.getInputStream();
此时调用 HTTP 协议处理程序, 它将查询 proxySelector. 对话框可能是这样的:
Handler: 嘿伙计, 我正在尝试访问 java.sun.com, 我应该使用代理吗?
ProxySelector: 您打算使用哪种协议?
Handler:http, 当然!
ProxySelector: 在默认端口上?
Handler: 让我查一下...... 是的, 默认端口.
ProxySelector: 我明白了. 您将在端口 8080 上使用 webcache.mydomain.com 作为代理.
Handler: 谢谢.<pause> Dude,webcache.mydomain.com:8080 似乎没有响应! 还有其他选择吗?
ProxySelector:Dang! 好的, 也可以尝试在端口 8080 上使用 webcache2.mydomain.com.
Handler: 当然. 似乎工作. 谢谢.
ProxySelector: 没有汗水. 再见.
当然我点缀了一下, 但你应该能够明白了.
关于 ProxySelector 的最好的事情是它是可插拔的! 这意味着如果您的需求未被默认需求覆盖, 您可以为其编写替代品并将其插入!
什么是 ProxySelector? 我们来看看类定义:
- public abstract class ProxySelector {
- public static ProxySelector getDefault();
- public static void setDefault(ProxySelector ps);
- public abstract List<Proxy> select(URI uri);
- public abstract void connectFailed(URI uri,
- SocketAddress sa, IOException ioe);
- }
我们可以看到, ProxySelector 是一个抽象类, 有 2 个静态方法来设置或获取默认实现, 以及 2 个实例方法, 协议处理程序将使用它们来确定使用哪个代理或通知代理似乎无法到达. 如果要提供自己的 ProxySelector, 您只需扩展此类, 为这两个实例方法提供实现, 然后调用 ProxySelector.setDefault()将新类的实例作为参数传递. 此时协议处理程序 (如 http 或 ftp) 将在尝试确定要使用的代理时查询新的 ProxySelector.
在我们详细了解如何编写这样的 ProxySelector 之前, 让我们来谈谈默认的. J2SE 5.0 提供了一个强制向后兼容的默认实现. 换句话说, 默认的 ProxySelector 将检查前面描述的系统属性, 以确定要使用的代理. 但是, 有一个新的可选功能: 在最近的 Windows 系统和 Gnome 2.x 平台上, 可以告诉默认的 ProxySelector 使用系统代理设置(Windows 和 Gnome 2.x 的最新版本都允许您设置代理全球通过他们的用户界面). 如果是系统属性 java.NET.useSystemProxies 设置为 true(默认情况下, 为了兼容性将其设置为 false), 然后默认的 ProxySelector 将尝试使用这些设置. 您可以在命令行上设置该系统属性, 也可以编辑 JRE 安装文件 lib.NET.properties, 这样您只需在给定系统上更改一次.
现在让我们来研究如何编写和安装新的 ProxySelector.
这是我们想要实现的目标: 除了 http 和 https 之外, 我们对默认的 ProxySelector 行为非常满意. 在我们的网络上, 我们有多个这些协议的可能代理, 我们希望我们的应用程序按顺序尝试它们(即: 如果第一个没有响应, 那么尝试第二个, 依此类推). 更重要的是, 如果其中一个失败的时间过多, 我们会将其从列表中删除, 以便稍微优化一下.
我们需要做的只是子类 java.NET.ProxySelector 并提供 select()和 connectFailed()方法的实现.
select()在尝试连接到目标之前, 协议处理程序会调用该方法. 传递的参数是描述资源 (协议, 主机和端口号) 的 URI. 然后该方法将返回代理列表. 例如以下代码:
- URL url = new URL("http://java.sun.com/index.html");
- InputStream in = url.openStream();
将在协议处理程序中触发以下伪调用:
List<Proxy> l = ProxySelector.getDefault().select(new URI("http://java.sun.com/"));
在我们的实现中, 我们所要做的就是检查 URI 中的协议是否确实是 http(或 https), 在这种情况下, 我们将返回代理列表, 否则我们只委托默认代理. 为此, 我们需要在构造函数中存储对旧默认值的引用, 因为我们的默认值将成为默认值.
所以它开始看起来像这样:
- public class MyProxySelector extends ProxySelector {
- ProxySelector defsel = null;
- MyProxySelector(ProxySelector def) {
- defsel = def;
- }
- public java.util.List<Proxy> select(URI uri) {
- if (uri == null) {
- throw new IllegalArgumentException("URI can't be null.");
- }
- String protocol = uri.getScheme();
- if ("http".equalsIgnoreCase(protocol) ||
- "https".equalsIgnoreCase(protocol)) {
- ArrayList<Proxy> l = new ArrayList<Proxy>();
- // Populate the ArrayList with proxies
- return l;
- }
- if (defsel != null) {
- return defsel.select(uri);
- } else {
- ArrayList<Proxy> l = new ArrayList<Proxy>();
- l.add(Proxy.NO_PROXY);
- return l;
- }
- }
- }
首先请注意保留对旧的默认选择器的引用的构造函数. 其次, 请注意 select()方法中的非法参数检查以遵守规范. 最后, 请注意代码如何在必要时遵循旧的默认值(如果有的话). 当然, 在这个例子中, 我没有详细说明如何填充 ArrayList, 因为它没有特别的兴趣, 但如果你很好奇, 可以在附录中找到完整的代码.
实际上, 由于我们没有为该 connectFailed()方法提供实现, 因此该类是不完整的. 这是我们的下一步.
connectFailed()只要协议处理程序无法连接到该 select()方法返回的代理之一, 该方法就会被调用. 传递了 3 个参数: 处理程序尝试访问的 URI, 应该 select()是调用时使用的 URI, 处理 SocketAddress 程序尝试联系的代理程序以及尝试连接到代理程序时抛出的 IOException. 有了这些信息, 我们将只执行以下操作: 如果代理在我们的列表中, 并且失败了 3 次或更多次, 我们只需将其从列表中删除, 确保将来不再使用它. 所以代码现在是:
- public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
- if (uri == null || sa == null || ioe == null) {
- throw new IllegalArgumentException("Arguments can't be null.");
- }
- InnerProxy p = proxies.get(sa);
- if (p != null) {
- if (p.failed()>= 3)
- proxies.remove(sa);
- } else {
- if (defsel != null)
- defsel.connectFailed(uri, sa, ioe);
- }
- }
非常简单不是它. 我们必须再次检查参数的有效性(规范再次). 我们在这里唯一考虑的是 SocketAddress, 如果它是我们列表中的代理之一, 那么我们会处理它, 否则我们再次推迟到默认选择器.
既然我们的实现大部分都是完整的, 那么我们在应用程序中所要做的就是注册它, 我们就完成了:
- public static void main(String[] args) {
- MyProxySelector ps = new MyProxySelector(ProxySelector.getDefault());
- ProxySelector.setDefault(ps);
- // REST of the application
- }
当然, 为了清楚起见, 我简化了一些事情, 特别是你可能已经注意到我没有做太多异常捕捉, 但我相信你可以填补空白.
应该注意的是, Java Plugin 和 Java Webstart 都会使用自定义的 ProxySelector 替换默认的 ProxySelector, 以便更好地与底层平台或容器 (如 Web 浏览器) 集成. 因此, 在处理 ProxySelector 时请记住, 默认的通常是特定于底层平台和 JVM 实现. 这就是为什么提供自定义的一个好主意, 以保持对旧版本的引用, 就像我们在上面的示例中所做的那样, 并在必要时使用它.
5)结论
正如我们现在已经建立的 J2SE 5.0 提供了许多处理代理的方法. 从非常简单 (使用系统代理设置) 到非常灵活(更改 ProxySelector, 尽管仅限有经验的开发人员), 包括 Proxy 类的每个连接选择.
附录
以下是我们在本文中开发的 ProxySelector 的完整源代码. 请记住, 这只是出于教育目的而编写的, 因此有目的地保持简单.
- import java.NET.*;
- import java.util.List;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.io.IOException;
- public class MyProxySelector extends ProxySelector {
- // Keep a reference on the previous default
- ProxySelector defsel = null;
- /*
- * Inner class representing a Proxy and a few extra data
- */
- class InnerProxy {
- Proxy proxy;
- SocketAddress addr;
- // How many times did we fail to reach this proxy?
- int failedCount = 0;
- InnerProxy(InetSocketAddress a) {
- addr = a;
- proxy = new Proxy(Proxy.Type.HTTP, a);
- }
- SocketAddress address() {
- return addr;
- }
- Proxy toProxy() {
- return proxy;
- }
- int failed() {
- return ++failedCount;
- }
- }
- /*
- * A list of proxies, indexed by their address.
- */
- HashMap<SocketAddress, InnerProxy> proxies = new HashMap<SocketAddress, InnerProxy>();
- MyProxySelector(ProxySelector def) {
- // Save the previous default
- defsel = def;
- // Populate the HashMap (List of proxies)
- InnerProxy i = new InnerProxy(new InetSocketAddress("webcache1.mydomain.com", 8080));
- proxies.put(i.address(), i);
- i = new InnerProxy(new InetSocketAddress("webcache2.mydomain.com", 8080));
- proxies.put(i.address(), i);
- i = new InnerProxy(new InetSocketAddress("webcache3.mydomain.com", 8080));
- proxies.put(i.address(), i);
- }
- /*
- * This is the method that the handlers will call.
- * Returns a List of proxy.
- */
- public java.util.List<Proxy> select(URI uri) {
- // Let's stick to the specs.
- if (uri == null) {
- throw new IllegalArgumentException("URI can't be null.");
- }
- /*
- * If it's a http (or https) URL, then we use our own
- * list.
- */
- String protocol = uri.getScheme();
- if ("http".equalsIgnoreCase(protocol) ||
- "https".equalsIgnoreCase(protocol)) {
- ArrayList<Proxy> l = new ArrayList<Proxy>();
- for (InnerProxy p : proxies.values()) {
- l.add(p.toProxy());
- }
- return l;
- }
- /*
- * Not HTTP or HTTPS (could be SOCKS or FTP)
- * defer to the default selector.
- */
- if (defsel != null) {
- return defsel.select(uri);
- } else {
- ArrayList<Proxy> l = new ArrayList<Proxy>();
- l.add(Proxy.NO_PROXY);
- return l;
- }
- }
- /*
- * Method called by the handlers when it failed to connect
- * to one of the proxies returned by select().
- */
- public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
- // Let's stick to the specs again.
- if (uri == null || sa == null || ioe == null) {
- throw new IllegalArgumentException("Arguments can't be null.");
- }
- /*
- * Let's lookup for the proxy
- */
- InnerProxy p = proxies.get(sa);
- if (p != null) {
- /*
- * It's one of ours, if it failed more than 3 times
- * let's remove it from the list.
- */
- if (p.failed()>= 3)
- proxies.remove(sa);
- } else {
- /*
- * Not one of ours, let's delegate to the default.
- */
- if (defsel != null)
- defsel.connectFailed(uri, sa, ioe);
- }
- }
- }
来源: https://www.cnblogs.com/ibigboy/p/11251435.html