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.proxHostftp.proxyPortftp.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
