Java网络和代理
Java网络和代理
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://docs.oracle.com/javase/6/docs/technotes/guides/net/proxies.html