1. 程式人生 > >Java網絡編程-URI和URL

Java網絡編程-URI和URL

似的 -c tde 附加 extern loader 網絡配置 prope 查詢參數

前提

前面的一篇文章《Java中的Internet查詢》分析完了如何通過IP地址或者主機名確定主機在因特網中的地址。任意給定主機上可能會有任意多個資源,這些資源需要有標識符方便主機之間訪問對方的資源,因此這篇文章深入分析一下URL和URI。

URI

URI全稱是Uniform Resource Identifier,也就是統一資源標識符,它是一種采用特定的語法標識一個資源的字符串表示。URI所標識的資源可能是服務器上的一個文件,也可能是一個郵件地址、圖書、主機名等。簡單記為:URI是標識一個資源的字符串(這裏先不必糾結標識的目標資源到底是什麽,因為使用者一般不會見到資源的實體),從服務器接收到的只是資源的一種字節表示(二進制序列,從網絡流中讀取)。URI的語法構成是:一個模式和一個模式特定部分。表示形式如下:

模式:模式特定部分

scheme:scheme specific part 

註意:模式特定部分的表示形式取決於所使用的模式。URI當前的常用模式包括:

  • data:鏈接中直接包含經過BASE64編碼的數據。
  • file:本地磁盤上的文件。
  • ftp:FTP服務器。
  • http:使用超文本傳輸協議。
  • mailto:電子郵件的地址。
  • magnet:可以通過對等網絡(端對端P2P,如BitTorrent)下載的資源。
  • telnet:基於Telnet的服務的連接。
  • urn:統一資源名(Uniform Resource Name)。

除此之外,Java中還大量使用了一些非標準的定制模式,如rmi、jar、jndi、doc、jdbc等,這些非標準的模式分別用於實現各種不同的用途。

URI中的模式特定部分沒有固定的語法,不過,很多時候都是采用一種層次結構形式,如:

//授權機構/路徑?查詢參數

//authority/path?query

URI的authority部分指定了負責解析該URI其他部分的授權機構(authority),很多時候,URI都是使用Internet主機作為授權機構。例如http://www.baidu.com/s?ie=utf-8,授權機構是www.baidu.com(在URL角度來看,主機名是www.baidu.com)。

URI的路徑(path)是授權機構用來確定所標識資源的字符串。不同的授權機構可能會把相同的路徑解析後指向不同的資源。其實這一點很明顯,試下你寫兩個不同的項目,主頁的路徑都是/index.html,它們一定是完全相同的html文檔。另外,路徑是可以分層的,分層的各個部分使用斜線"/"進行分隔,而"."和".."操作符用於在分層層次結構中的導航(後面這兩個操作符可能很少見到,了解即可)。

URI的語法

URI的模式的組成部分可以是小寫字母、數字、加號、點(.)和連號(-)。典型的URI的其他三個部分(模式特定部分,也就是授權機構、路徑和查詢參數)分別由ASCII字母組成(也就是字母A-Z、a-z和數字0-9),此外,還可以使用標點符號-、_、.、!和~,而定界符(如/、?、&和=)有其預定義的用途。所有的其他字符,包括ASCII中拉丁字母,都需要使用百分號(%)轉義,轉義後的格式是:%+字符按照UTF-8編碼再轉成16進制的字符串表示。註意一點,如果前面提到的定界符沒有作為定界符使用,也需要進行轉義。舉個簡單的例子,如URI中存在中文字符"木",木字的UTF-8編碼為0xE6 0x9C 0xA8,那麽它在URI中應該轉義為%E6%9C%A8。Jdk中的URLEncoder或者apache-codec中的相關類庫提供URI(URL)編碼的功能。

URI中還可以攜帶用戶的口令,因為會有安全漏洞,所以並不常見,這裏也不展開分析。

URI類

URI在Java中抽象為java.net.URI類。

構造URI實例

URI實例構造方法有很多個:

public URI(String str) throws URISyntaxException

public URI(String scheme,String userInfo, String host, int port,
        String path, String query, String fragment) throws URISyntaxException

public URI(String scheme, String authority,
       String path, String query, String fragment)throws URISyntaxException

public URI(String scheme, String host, String path, String fragment) throws URISyntaxException

public URI(String scheme, String ssp, String fragment) throws URISyntaxException

//靜態工廠方法,最終調用的是URI(String str)
public static URI create(String str)

註意的是構造URI實例的時候會檢查是否符合URI的語法,否則會拋出URISyntaxException異常。以上的所有方法都是基於URI的模式+模式特定部分或者URI的各個部分構造URI實例,而靜態工廠方法public static URI create(String str)主要是屏蔽了非檢查型異常URISyntaxException,轉化為檢查型異常IllegalArgumentException,這樣就不需要顯式捕獲異常。

獲取URI的屬性

前面也提到,URI引用包括最多三個部分:模式、模式特定部分和片段標識符,一般格式為:

模式:模式特定片段:片段標識符

基於這種語法規範,URI類提供了下面幾個方法獲取這些屬性:

public String getScheme()

public String getRawSchemeSpecificPart()

public String getSchemeSpecificPart()

public String getFragment()

public String getRawFragment()

PS:之所以沒有getRawScheme()方法,是因為URI規範中強制規定,所有的模式名稱必須由URI規範中合法的ASCII字符組成,也就是模式名稱中不允許存在百分號轉義。上面的getRawSchemeSpecificPart()是返回原始的模式特定部分,getSchemeSpecificPart()返回了經過解碼(decode)的模式特定部分。同理,getRawFragment()返回原始的片段標識符,而getFragment()返回經過解碼(decode)的片段標識符。當然,還有其他方法獲取URI的基礎屬性:

//是否絕對URI
public boolean isAbsolute()

//是否不透明的URI,如果isOpaque()返回true,URI是不透明的
//只能獲取到模式、模式特定部分和片段標識符,獲取不了host、port等
public boolean isOpaque()

public String getAuthority()

public String getRawAuthority()

public String getRawUserInfo()

public String getUserInfo()

public String getHost()

public int getPort()

public String getRawPath()

public String getPath()

public String getRawQuery()

public String getQuery()

PS:isOpaque()方法為true的時候,說明URI是不透明的,不透明的URI無法獲取授權機構、路徑、端口和查詢參數等。另外,上面的獲取屬性的方法有些方法存在getRawFoo()的對應方法,這些getRawFoo()方法就是獲取原始屬性的值,如果沒有Raw關鍵字,則返回解碼後的字符串值。

解析相對URI

URI中提供了三個方法用於絕對URI和相對URI之間的轉換:

public URI resolve(URI uri)

public URI resolve(String str)

public URI relativize(URI uri)

其中,resolve方法是基於絕對URI和相對URI把相對URI補全為絕對URI,例如:

public static void main(String[] args) throws Exception{
    URI absolute = URI.create("http://localhost:8080/index.html");
    URI relative = URI.create("/hello.html");
    URI resolve = absolute.resolve(relative);
    System.out.println(resolve);
}
//控制臺輸出
http://localhost:8080/hello.html

relativize方法是基於絕對URI和相對URI反解出絕對URI中的相對URI部分,例如:

public static void main(String[] args) throws Exception{
    URI absolute = URI.create("http://localhost:8080/index.html");
    URI relative = URI.create("http://localhost:8080/");
    URI resolve = relative.relativize(absolute);
    System.out.println(resolve);
}

//控制臺輸出
index.html

URI的比較

URI類實現了Comparable接口,因此URI可以排序。URI的相等性不是基於字符串直接比較,相等的URI在透明性上必須是一致的,例如都是不透明的,其他部分才可以進行比較。URI比較的時候,模式和授權機構是忽略大小寫的,其他部分必須區分大小寫,用於轉義無效字符串的十六進制數字除外。需要轉義的字符在轉義前和轉義之後進行比較,會認為是不同的URI。

//1.
URI uri1 = URI.create("http://localhost:8000/index.html");
URI uri2 = URI.create("http://LOCALHOST:8000/index.html");
System.out.println(uri1.equals(uri2));
//輸出:true

//2.
URI uri3 = URI.create("http://localhost:8000/index/A");
URI uri4 = URI.create("http://LOCALHOST:8000/index/%41");
System.out.println(uri3.equals(uri4));
//輸出:false

URI的字符串表示

URI中有兩個方法返回其字符串表示:

public String toString()

public String toASCIIString()

toString()方法返回未編碼的字符串形式,也就是特殊的字符不使用百分號轉義,因此這個方法的返回值不能保證符合URI的語法,盡管它的各個部分是遵循URI的語法規範的。toASCIIString()方法返回經過編碼的字符串形式(US-ACSII編碼),也就是特殊的字符一定經過了百分號轉義。toString()存在的意義是提高URI的可讀性,toASCIIString()方法存在的意義是提高URI的可用性。

URL

URL全稱是Uniform Resource Location,也就是統一資源位置。實際上,URL就是一種特殊的URI,它除了標識一個資源,還會為資源提供一個特定的網絡位置,客戶端可以通過它來獲取URL對應的資源。

URL所表示的網絡資源位置通常包括用於訪問服務器的協議(如http、ftp等)、服務器的主機名或者IP地址、以及資源文件在該服務器上的路徑。典型的URL例如http://localhost/myProject/index.html,它指示本地服務器的myProject目錄下有一個名為index.html的文檔,這個文檔可以通過http協議訪問(實際上,URL不一定是指服務器中的真實的物理路徑,因為我們一般在服務器中部署應用,如Servlet應用,URL訪問的很可能是應用的接口,至於最終映射到什麽資源可以由應用自身決定)。

URL的語法

URL的語法表示形式為:

protocol://userInfo@host:port/path?query#fragment

協議://用戶信息@主機名:端口/路徑?查詢#片段

protocol:URL中的協議(protocol)是相應於URI中的模式(schema)的另一個叫法。URL中,協議部分可以是file、ftp、http、https、magnet、telnet或者其他定制協議字符串(但是不包括urn)。

userInfo:URL中的用戶信息(userInfo)是服務器的登錄信息,這部分信息是可選的。如果這部分信息存在,一般會包含一個用戶名,極少情況下會包含一個口令。實際上URL攜帶用戶信息是不安全的。

port:URL中的端口號(port)是指服務器中應用的運行端口,默認端口為80,此部分信息是可選的(也就如果不指定端口號就使用默認端口80)。

path:URL中的路徑(path)用於表示服務器上的一個特定的目錄(其實說一個特定的文件也可以),這個特定的目錄不一定是物理目錄,也有可能是邏輯目錄。這一點很容易說明,一般不可能把服務器上面的目錄直接公開讓所有人訪問,服務器上面跑的一般是Web(Java的話一般是Servlet)應用,路徑指向的實際數據來源甚至很大可能是在其他服務器上的MySQL中的查詢結果。

query:查詢參數(query)一般是一個字符串,它表示URL中向服務器提供的附加參數,一般只使用在http協議的URL中,其中包含了表單數據,來源於用戶的輸入,表示形式是key1=value1&key2=value2&keyn=valuen。

fragment:片段(fragment)表示遠程服務器資源的某個特定的部分。假如服務器資源是一個HTML文檔,此片段標識符將制定為該HTML文檔的一個錨(Anchor)。如果遠程資源是一個XML文檔,那麽這個片段標識符是一個XPointer。(如果用Markdown寫過博客就知道,添加了導航目錄之後,片段就是目錄將要導航到的目的章節)

相對URL

URL可以告知瀏覽器一個文檔(Document,假設URL對應服務器上的資源統一叫做文檔)的大量信息:獲取文檔所使用的協議、文檔所在的主機、文檔在該主機中的路徑等。文檔中可能也存在引用和當前URL相同的URL,因此,在該文檔中使用URL的時候並不要求完整地指定每一個URL,URL可以繼承其父文檔的協議、主機名和路徑。繼承了父文檔URL的部分信息的這類不完整的URL稱為相對URL(Reletive URL),相反,完整指定所有部分的URL稱為絕對URL(Absolute URL)。在相對URL中,缺少的各個部分與請求該文檔的URL對應的部分相同。舉個例子,我們訪問本地服務器的一個HTML文檔,URL為http://localhost:8080/myProject/index.html,index.html文檔中存在一個超鏈接<a href="login.html">,當我們點擊此超鏈接的時候,瀏覽器會從原始URL(http://localhost:8080/myProject/index.html)中截去index.html然後拼接login.html,最後訪問http://localhost:8080/myProject/login.html。

如果相對URL以"/"開頭,那麽它是相對於文檔的根目錄,而不是當前的文檔。舉個例子,我們訪問本地服務器的一個HTML文檔,URL為http://localhost:8080/myProject/index.html,index.html文檔中存在一個超鏈接<a href="/login.html">,當我們點擊此超鏈接的時候,瀏覽器會跳轉到http://localhost:8080/login.html。

相對URL有兩個顯著的優點:

  • 1、減少文檔編寫量,畢竟可以省略一部分URL內容,不過這個不是重要的優點。
  • 2、重要的優點是:相對URL允許使用多協議來提供一個文檔樹,例如http和ftp,使用相對URL編寫的文檔可以從一個網站直接復制或者遷移到另一個網站而不會破壞文檔內部URL鏈接。

URL類

java.net.URL類(後面直接叫URL)是JDK對URL的統一抽象,它是一個final修飾的類,也就是不允許派生子類。實際上,URL設計的時候采用了策略模式,不同的協議的處理器就是不同的策略,而URL類構成了上下文,通過它決定選用何種策略。URL的核心屬性包括協議(或者叫模式)、主機名、端口、查詢參數字符串和片段標識符(在JDK中被命名為ref)等,每個屬性都可以單獨設置。一旦一個URL對象被構造之後,它的所有屬性都不能改變,也就是它的實例是線程安全的。

構造URL實例

URL實例的主要構造方法如下:

//基於URL的各個部分構造URL實例,其中file相當於path、query和fragment三個部分組成
public URL(String protocol, String host, int port, String file) throws MalformedURLException

//基於URL的各個部分構造URL實例,其中file相當於path、query和fragment三個部分組成,使用默認端口80
public URL(String protocol, String host, String file) throws MalformedURLException

//基於URL模式構造URL實例
public URL(String spec) throws MalformedURLException

//基於上下文(父)URL和URL模式構造相對URL實例
public URL(URL context, String spec) throws MalformedURLException

下面基於上面幾個構造URL實例的方法舉幾個簡單的編碼例子:

//1.
//註意file要帶斜桿前綴/
URL url = new URL("http", "127.0.0.1", 8080, "/index");
//輸出http://127.0.0.1:8080/index
System.out.println(url.toString());

//2.
URL url = new URL("http://127.0.0.1:8080/index");
//輸出http://127.0.0.1:8080/index
System.out.println(url.toString());

//3.
URL url = new URL("http", "127.0.0.1", "/index");
//輸出http://127.0.0.1/index
System.out.println(url.toString());

//4.
URL context = new URL("http", "127.0.0.1", "/index");
//構造相對URL,保留協議、host、port部分
URL url = new URL(context, "/login");
//輸出http://127.0.0.1/login
System.out.println(url);

上面只說到通過URL類去構造對象,其實還有其他方法獲取URL實例,例如:

URL systemResource = ClassLoader.getSystemResource(String name)

Enumeration<URL> systemResources = ClassLoader.getSystemResources(String name)

URL resource = UrlMain.class.getResource(String name)

URL resource = UrlMain.class.getClassLoader().getResource(String name)

Enumeration<URL> resources = UrlMain.class.getClassLoader().getResources(String name)

其中,ClassLoader.getSystemResource(String name)ClassLoader.getSystemResources(String name)都是先判斷是否存在SystemClassLoader,若存在則使用SystemClassLoader加載資源,否則使用BootstrapClassLoader(BootstrapClassPath)進行資源加載,簡單來說,這兩個方法就是使用系統類加載器加載資源,加載資源時候,從類路徑中加載資源,例如使用IDEA,則從編譯後的/target目錄中加載資源,這一點可以使用ClassLoader.getSystemResource("")進行驗證。其他三個方法Class#getResource(String name)Class#getClassLoader()#getResource(String name)Class#getClassLoader()#getResources(String name)本質上都是基於AppClassLoader進行資源加載,它們加載資源的時候是從當前的Class所在的類路徑(包括類的包路徑,如果使用IDEA一般也是/target/類所在的包目錄)進行加載,例如有一個類club.throwable.Main.class,如果目錄club.throwable存在一張圖片doge.jpg,則加載圖片的時候可以這樣子club.throwable.Main.class.getResource("doge.jpg")。值得註意的一點是,如果需要加載的資源是在一個特定目錄中,那麽Class#getResource(String name)中的name必須以文件路徑分隔符開頭,例如Window系統中用‘/‘,其他兩個基於通過ClassLoader實例直接加載的不需要使用文件路徑分隔符開頭,這一點可以看Class#getResource(String name)方法中的resolveName(name)方法。

獲取URL的屬性

URL實例提供幾個方法用於獲取數據:

public final InputStream openStream() throws java.io.IOException

public URLConnection openConnection() throws java.io.IOException

public URLConnection openConnection(Proxy proxy) throws java.io.IOException

public final Object getContent() throws java.io.IOException

public final Object getContent(Class[] classes) throws java.io.IOException

InputStream openStream()

openStream()方法連接到URL所引用的資源,在客戶端和服務器之間完成必要的握手之後,返回一個InputStream實例,用於讀取網絡流數據。此方法返回的InputStream實例讀取到的內容是Http請求體的原始報文(如果使用Http協議的話),因此有可能是一個原始文本片段或者是一個二進制的序列(例如圖片)。舉個例子:

    public static void main(String[] args) throws Exception{
        URL url = new URL("https://www.baidu.com");
        InputStream inputStream = url.openStream();
        int c;
        while ((c= inputStream.read()) != -1){
            System.out.print(c);
        }
        inputStream.close();
    }

URLConnection openConnection()

openConnection()openConnection(Proxy proxy)是相似的,只是後者可以使用代理。openConnection()方法為指定的URL新建一個socket,並且返回一個URLConnection實例,它表示一個網絡資源打開的連接,我們可以從這個打開的連接獲取一個InputStream實例,用於讀取網絡流數據。如果上面的過程出現調用失敗,會拋出一個IOException。

Object getContent()
Object getContent()Object getContent(Class[] classes)是相似的,後者可以通過Class數組指定獲取到的URL中的請求體內容轉化為對應的類型的實體。Object getContent()實際上是調用了URLConnection實例中的getContent方法,一般來說,不建議使用這兩個方法,因為轉換的邏輯執行後的結果一般是不合符我們預想的結果。

獲取URL屬性

URL的屬性獲取可以理解為分解URL成URL組成的各個部分,這些部分的信息可以單獨獲取。前面提到過URL的各個組成部分,這裏重復一下,URL分別由下面的幾個部分組成:

  • 模式(schema),也稱協議(protocol)。
  • 用戶信息(UserInfo),這個並不常用。
  • 授權機構,一般是"主機名(Host):port"的格式。
  • 路徑。
  • 片段標識符,在Java中稱為段或者ref。
  • 查詢字符串。

URL類中提供對應的方法分別是:


//獲取模式(協議)
public String getProtocol()

//獲取主機名
public String getHost()

//獲取授權機構,一般是host:port的形式
public String getAuthority()

//獲取端口號port
public int getPort()

//返回協議的默認端口,如http協議的默認端口號為80,如果沒有指定協議的默認端口則返回-1
public int getDefaultPort()

//返回URL字符串中從主機名後的第一個斜桿/一直到片段標識符的#字符之前的所有字符
public String getFile()

//返回的值和getFile()相似,但是不包含查詢字符串
public String getPath()

//返回URL的片段標識符部分
public String getRef()

//返回URL的查詢字符串
public String getQuery()

//返回URL中的用戶信息,不常用
public String getUserInfo()

其中需要註意的是getFile()getPath(),舉個例子:

URL url = new URL("https://localhost:8080/search?name=doge#anchor-1");
System.out.println(String.format("path=%s", url.getPath()));
System.out.println(String.format("file=%s", url.getFile()));
//控制臺輸出
path=/search
file=/search?name=doge

比較

URL實例的比較通常是使用equals()hashCode()方法,當且僅當兩個URL指向相同的主機、端口和路徑上的資源,並且兩者的片段標識符和查詢字符串都相同的時候,才會認為兩個URL是相等的。equals()調用的時候會嘗試使用DNS解析主機,此方法有可能是一個阻塞的IO操作,會造成比較大的性能消耗,這個時候需要考慮使用緩存,或者把URL轉化為URI進行比較。

轉換

URL類中有三個常用的實例方法用於轉換為另一種形式:

public String toString()

public String toExternalForm()

public URI toURI()

實際上,toString()最終調用的是toExternalForm(),而toExternalForm()方法是使用StringBuilder把URL的各個組成部分拼接,返回的字符串可以方便直接使用在瀏覽器中。toURI()方法就是把URL實例轉化為URI實例。

URL的編碼和解碼

我們在做Web項目或者使用POSTMAN的時候經常會看到x-www-form-urlencoded,它是常用的媒體類型或者說內容類型(ContentType),使用這種類型的時候,URL中的字符需要進行編碼,那麽為什麽需要進行編碼呢?這個是歷史遺留原因,因為發明Web或者說Http(s)協議的時候,Unicode編碼並未完全普及,URL中使用的字符必須來自ASCII的一個固定子集,這個固定子集是:

  • 大寫字母A-Z。
  • 小寫字母a-z。
  • 數字0-9。
  • 標點符號字符-_.!~*‘(和,)

其他字符如:/&?@#;$+=%也可以使用,但是它們限定於使用在特定的用途,如果這些字符串出現在URL路徑或者查詢字符串,它們以及路徑和查詢字符串的內容必須進行編碼。

這裏有個需要註意的地方:URL編碼只針對URL路徑和URL路徑之後的部分,因為URL規範中規定,路徑之前的部分必須滿足ASCII固定子集的內容。

URL編碼的方法很簡單:除了ASCII數字、字母和部分指定的標點符號之外,所有的其他字符都要轉化為字節表示,每個字節要轉化為百分號(%)後面加2位十六進制數字。空格是一種特殊的字符,它使用得比較普遍,一般空格可以編碼為%20或者加號(+),但是加號本身的編碼為%2B。而/#=&和?不作為分隔符的時候,必須進行編碼。

解碼過程就是上面編碼過程的逆向操作,這裏不具體展開。

Java中提供兩個類java.net.URLEncoder和java.net.URLDecoder分別用於URL的編碼和解碼,註意需要使用兩個參數的靜態方法encode(String value,String charset)decode(String value,String charset),單參數的方法已經過期,不建議使用。註意在使用java.net.URLEncoder和java.net.URLDecoder的時候,它們的API不會判斷URL的什麽部分需要編碼和解碼,什麽部分不需要編碼和解碼,直接整個URL字符串丟進去編碼一定會出現意料之外的結果。舉個反面列子:

String url = "http://localhost:9090/index?name=張大doge";
String encode = URLEncoder.encode(url, "UTF-8");
System.out.println(encode);
//輸出:http%3A%2F%2Flocalhost%3A9090%2Findex%3Fname%3D%E5%BC%A0%E5%A4%A7doge

實際上,我們只需要對Path和Path之後的字符進行編碼和解碼,例如對於URLhttp://localhost:9090/index?name=張大doge,我們需要編碼和解碼的部分只有index、name和張大doge這三個部分,其他部分應該保持原樣。正確的例子如下:

public static void main(String[] args) throws Exception {
    String raw= "http://localhost:9090/index?name=張大doge";
    String base = raw.substring(raw.lastIndexOf("//"));
    String pathLeft = base.substring(base.lastIndexOf("/") + 1);
    String[] array = pathLeft.split("\\?");
    String path = array[0];
    String query = array[1];
    base = raw.substring(0,raw.lastIndexOf(path));
    path = URLEncoder.encode(path, "UTF-8");
    String[] queryResult = query.split("=");
    String queryKey = URLEncoder.encode(queryResult[0], "UTF-8");
    String queryValue = URLEncoder.encode(queryResult[1], "UTF-8");
    System.out.println(base + path + "?" + queryKey + "=" + queryValue);
}
//輸出結果:http://localhost:9090/index?name=%E5%BC%A0%E5%A4%A7doge
//其中UTF-8編碼中張的十六進制表示為E5 BC A0,大的十六進制編碼為E5 A4 A7

代理(Proxy)

許多系統會通過代理服務器訪問Web應用或者其他服務器中的資源,代理服務器接收從本地客戶端發出的請求再轉發請求到遠程服務器,然後遠程服務器返回請求的結果到代理服務器,代理服務器接收到結果之後會把結果回傳到本地服務器。這樣做有兩個比較重要的原因:

  • 1、安全原因,防止遠端的主機了解關於本地網絡配置的細節。
  • 2、過濾出站請求,限制瀏覽一些禁用的網站。

在Java中除了TCP連接使用傳輸層的SOCKET代理,其他應用層代理都不支持。Java對於SOCKET沒有提供禁用代理的選項,但是可以通過下面三個系統屬性配置來開啟和限制代理:

http.proxyHost:代理服務器的主機名,默認不設置此系統屬性。
http.proxyPort:代理服務器的端口號,默認不設置此系統屬性。
http.noneProxyHosts:不需要代理訪問的主機名,多個用豎線|分隔,默認不設置此系統屬性。

舉個例子:
System.setProperty("http.proxyHost", "localhost");
System.setProperty("http.proxyPort", 1080);
System.setProperty("http.noneProxyHosts", "www.baidu.com|github.com");

Proxy類

java.net.Proxy類提供了對代理服務器更細粒度的控制,也就是說這個類允許在編程的使用使用不同的遠程服務器作為代理服務器,而不是通過系統屬性全局配置代理。Proxy目前支持三種代理類型,分別是:

  • Proxy.Type.DIRECT:直連,也就是不使用代理。
  • Proxy.Type.HTTP:HTTP代理。
  • Proxy.Type.SOCKS:socket的V4或者V5版本的代理。

使用的時候如下:

SocketAddress socketAddress = new InetSocketAddress("localhost", 80);
Proxy proxy = new Proxy(Proxy.Type.HTTP, socketAddress);
Socket socket = new Socket(proxy);
//...

ProxySelector類

每個運行中的Java虛擬機都會存在一個java.net.ProxySelector實例對象,用來確定不同連接使用的代理服務器。默認的java.net.ProxySelector的實現是sun.net.spi.DefaultProxySelector的實例,它會檢查各種系統屬性和URL的協議,再決定如果連接到不同的遠程代理服務器,當然,開發者也可以繼承和實現自定義的java.net.ProxySelector,從而可以根據協議、主機、路徑日期等其他標準來選擇不同的代理服務器。java.net.ProxySelector的幾個核心的抽象方法如下:

//獲取默認的ProxySelector實例
public static ProxySelector getDefault()

//設置默認的ProxySelector實例
public static void setDefault(ProxySelector ps)

//通過URI獲取可用的代理列表
public abstract List<Proxy> select(URI uri)

//告知ProxySelector不可用的代理和出現的異常
public abstract void connectFailed(URI uri, SocketAddress sa, IOException ioe)

如果需要擴展的話,最好加入緩存的功能,緩存可用的Proxy列表,一旦出現Proxy不可用,通過connectFailed進行清理和剔除不可用的代理節點即可。

小結

URL和URI是當前的網絡世界或者系統中資源的重要標識符,了解它們的基礎知識才能更好地進行網絡編程。URI是統一資源標識符,它可以表示任何介質中的資源。而URL是統一資源位置,一般特指因特網中的網絡資源位置,使用於Http或者Https協議中。很顯然,URI可以表示的範圍更大,URI實際上是包含了URL。而兩者的區別可以參看上面的幾個小節。

(c-5-d e-20181003)

Java網絡編程-URI和URL