Java網絡和代理

Java網絡和代理

1)簡介

在當今的網絡環境中,特別是企業網絡環境中,應用程序開發人員必須像系統管理員同樣頻繁地處理代理。在某些狀況下,應用程序應該使用系統默認設置,在其餘狀況下,咱們但願可以很是嚴格地控制經過哪一個代理服務器,而且在中間的某個地方,大多數應用程序都樂於經過爲用戶提供設置代理設置的GUI,來將決策委派給用戶,就像在大多數瀏覽器中同樣。html

在任何狀況下,像Java這樣的開發平臺應該提供處理這些強大且靈活的代理的機制。不幸的是,直到最近,Java平臺在該領域還不是很靈活。可是,爲了解決這個缺點,已經引入了J2SE 5.0做爲新API的全部變化,本文的目的是提供對全部這些API和機制的深刻解釋,舊的仍然有效,以及新的。java

2)系統屬性

直到J2SE 1.4系統屬性是在任何協議處理程序的Java網絡API中設置代理服務器的惟一方法。爲了使事情變得更復雜,這些屬性的名稱已從一個版本更改成另外一個版本,其中一些如今已通過時,即便它們仍然支持兼容性。web

使用系統屬性的主要限制是它們是「全有或全無」開關。這意味着一旦爲特定協議設置了代理,它將影響該協議的全部鏈接。這是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代理服務器 (咱們沒有指定任何端口,所以使用默認端口)。oracle

再看一個示例:app

$ 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
相關文章
相關標籤/搜索