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