標籤:set 因此 http協議 anon 感知 機制 數字 dock 版本
前提
在深入理解URL、URI等概念,或者學些Socket相關的知識之,有必要系統理解一下Internet相關的一些基礎知識。
Internet地址
串連到Internet(網際網路)的裝置稱為節點(node),而任意一個電腦節點稱為主機(host)。每個節點或者主機都由至少一個唯一的數來標識,這稱為Internet地址或者IP地址。
IP和網域名稱
如果使用Java作為開發語言的話,不需要擔心IP或者網域名稱的工作原理,但是我們需要理解IP定址的一些基礎知識。我們目前常用的網路都是IPv4網路,每個電腦節點都是由一個4位元組(32bit)的數位識別碼,這個數位識別碼的格式是點分四段(dotted quad,形式是:xxx.xxx.xx.xx),其中的每個數字都是一個無符號位元組,取值範圍是0到255。當資料通過網路傳輸的時候,資料包的首部中要包括要發往的機器地址(目的地址)和發送這個資料包的機器地址(源地址)。
可以使用的IPv4類型的IP地址總量大概是40億多一點,因此無法做到地球上每個人都分配一個唯一的IPv4的IP地址,所以IPv6就誕生了,目前網路由IPv4向IPv6過度(不過這個過度過程相對緩慢,因素很多)。IPv6網路中的IP地址使用16位元組(128bit)的數位識別碼。IPv6地址的表示形式通常是以英文冒號分隔的8個地區,每個地區都是4個十六進位的數字,舉個例子:FEDC:BA98:7654:3210:FEDC:BA98:7654:3210就是一個合法的IPV6地址。而在IPv4和IPv6的混合網路中,IPv6地址的最後四個位元組有時候表示形式為IPv4地址的點分四段地址。IPv6地址只在Jdk1.4以及之後的版本支援,換言之,Jdk1.3或者之前的版本只能使用IPv4地址。
雖然電腦可以輕鬆地處理數字,但是人腦的記憶對於數字並不敏感,因此開發了網域名稱系統(Domain Name System,也就是DNS),用於將人腦易於記憶的主機名稱(如www.baidu.com)轉換為數字Internet地址(如183.232.231.173)。這裡不展開DNS的具體內容,作為開發人員,我們可以簡單理解為它就是一個巨型分散式資料庫,用於映射主機名稱(網域名稱)和IP地址,畫個圖大致如下:
連接埠
因為每台電腦都不是只做一件事,相當於電腦做的每一種商務邏輯需要從邏輯上隔離,例如FTP請求的處理要和電子郵件的處理分離,FTP請求處理也要和Web業務處理分離,所以每種商務邏輯的處理需要使用一個邏輯分離的標識,這個標識就是連接埠(port)。一般每台電腦有成千上萬個邏輯連接埠(確切來說,每個傳輸層協議都有65535個連接埠,Windows系統中,1~1023號連接埠是系統連接埠,使用者無法修改,1024~65534連接埠是系統為使用者預留的連接埠,而65535號連接埠為系統保留連接埠),每個連接埠可以分配給一個特定的服務。例如Web的底層協議Http協議通訊一般使用80連接埠,使用瀏覽器URL訪問伺服器的80連接埠可以省略URL中的連接埠號碼。
Java對網路的抽象InetAddress
單詞InetAddress是Internet Address的縮寫合并,代表網際網路地址。java.net.InetAddress類是Java對IP地址(包括IPv4和IPv6地址)的高度抽象表示。大多數網路編程相關的類都會用到InetAddress,如Socket、ServerSocket等,InetAddress兩個最核心的屬性是主機名稱(host)和IP地址,對應屬性hostName和address。
建立InetAddress執行個體
建立InetAddress執行個體主要依賴它的Factory 方法(實際上InetAddress的建構函式是包私人的,也就是無法通過new關鍵字建立執行個體),比較常用的一個靜態Factory 方法是:
static InetAddress getByName(String host) throws UnknownHostException
其中參數可以為主機名稱(網域名稱)或者點分四段地址,前者相當於通過主機名稱尋找一個可串連的IP地址,後者相當於通過IP地址反查主機名稱,值得注意的是,這個方法調用的使用會建立與本地DNS伺服器的串連進行主機名稱或者數字地址尋找,如果DNS伺服器找不到主機或者地址,會拋出UnknownHostException異常。
public static void main(String[] args) throws Exception{ InetAddress inetAddress = InetAddress.getByName("www.baidu.com"); System.out.println(inetAddress); }
InetAddress覆寫了toString
方法,返回結果是hostName/address格式,上面的main方法執行的一個可能的結果是:
www.baidu.com/14.215.177.39
有些時候,我們知道數字IP地址,就可以由數字地址直接建立一個InetAddress執行個體,這樣就可以不必使用getByName(String host)
方法和DNS互動。
static InetAddress getByAddress(byte[] addr)throws UnknownHostExceptionstatic InetAddress getByAddress(String host,byte[] addr)throws UnknownHostException
這兩個方法可以建立主機名稱不存在或者主機名稱無法解析的InetAddress執行個體。舉個例子:
public static void main(String[] args) throws Exception { byte[] bytes = {14, (byte) 215, (byte) 177, 39}; InetAddress inetAddress = InetAddress.getByAddress("www.doge.com",bytes); System.out.println(inetAddress); }
實際上,網域名稱www.doge.com並不存在,但是這個方法並不會拋出異常。
如果要查詢一個主機名稱的所有IP地址,可以使用:
static InetAddress[] getAllByName(String host) throws UnknownHostException
如果需要查詢原生主機名稱和IP地址,可以使用:
static InetAddress getLocalHost() throws UnknownHostException
注意這個方法會嘗試串連DNS去查詢本機電腦的真正的主機名稱和IP地址,如果查詢失敗,它就會返回傳回位址,也就是主機名稱是"localhost",IP地址是點分四段地址"127.0.0.1"。
InetAddress緩衝
DNS尋找的開銷可能相當大(如果請求需要經過多個中間伺服器或者嘗試解析一個不可達的主機,可能需要耗費幾秒的時間),所以InetAddress會緩衝DNS查詢結果,也就是一旦得到一個給定主機的地址,就不會再次尋找,即使為同一個主機建立多個InetAddress執行個體,也不會再次進行DNS查詢。這樣的緩衝機制對於效能來說是有好處的,但是也會帶來負面影響:
- 程式運行期間串連的主機的IP地址很大可能會發生變化,已緩衝的IP有可能不可用。
- 剛開始嘗試解析一個主機時候是失敗的,但是隨後嘗試解析的時候會成功,但是緩衝了首次解析失敗的記錄。
- 遠程DNS伺服器發送的資訊還在傳輸,第一次嘗試逾時,下一次請求即可成功。
因此,Java對於不成功的DNS查詢結果僅僅緩衝10秒,而且可以通過下面兩個系統變數控制緩衝的時間:
- networkaddress.cache.ttl:成功的DNS查詢結果的緩衝時間(秒數),-1表示不會逾時。
- networkaddress.cache.negative.ttl:成功的DNS查詢結果的緩衝時間(秒數),-1表示不會逾時。
InetAddress提供的基本屬性擷取方法
InetAddress提供四個基本屬性擷取方法,用於擷取當前InetAddress表示的主機名稱和IP地址。
public String getHostName();public String getCanonicalHostName();public byte[] getAddress();public String getHostAddress();
注意上面的幾個方法只有Getter,沒有Setter方法,說明這幾個屬性的設定許可權是java.net包中的類庫。
- getHostName:返回當前InetAddress執行個體的主機名稱,如果對應的機器沒有主機名稱或者安全管理器阻止確定主機名稱,則會返回點分四段數字IP地址。
- getCanonicalHostName:getCanonicalHostName與getHostName,不過getHostName方法只是在不知道主機名稱的情況下才串連DNS進行查詢,getCanonicalHostName方法總是串連DNS查詢主機名稱並且替換緩衝值,所以這個方法調用會比較耗時。
- getAddress:返回當前InetAddress執行個體的數字IP地址的byte數組,注意因為Java中沒有無符號的byte,因此負數byte值要+256變成int類型才是無符號的byte值。
- getHostAddress:實際上就返回getAddress方法中的byte數群組轉換成的IP地址點分四段表示形式的字串,也就是IP地址字串。
上面的getAddress()
方法還有一個特殊的判斷使用情境,就是它的傳回值byte數組的長度如果是4,那麼InetAddress一定是Inet4Address的執行個體,如果長度為16,那麼那麼InetAddress一定是Inet6Address的執行個體,由此可以判斷InetAddress中的IP地址到底是IPv4還是IPv6。
InetAddress提供的地址類型判斷方法
有些IP地址和地址模式有特殊的含義,例如127.0.0.1是本地傳回位址,244.0.0.0到239.255.255.255範圍內的IPv4地址是組播地址。InetAddress中提供10個公有執行個體方法來判斷InetAddress對象是否符合這些地址模式:
- 1、
boolean isAnyLocalAddress()
:如果地址是通配地址則返回true,所謂通配地址就是可以匹配本地系統中的任何地址,在IPv4中的通配地址是0.0.0.0,在IPv6中的通配地址是0:0:0:0:0:0:0:0(::)。
- 2、
boolean isLoopbackAddress()
:如果地址是傳回位址則返回true,在IPv4中的傳回位址是127.0.0.1,在IPv6中的傳回位址是0:0:0:0:0:0:0:1(::1)。
- 3、
boolean isLinkLocalAddress()
:如果地址是一個IPv6本地連結地址則返回true。
- 4、
boolean isSiteLocalAddress()
:如果地址是一個IPv6以磁碟為基礎的網站地址則返回true。
- 5、
boolean isMulticastAddress()
:如果地址是一個組播地址則返回true。
- 6、
boolean isMCGlobal()
:如果地址是一個全球組播地址則返回true。
- 7、
boolean isMCOrgLocal()
:如果地址是一個組織範圍組播地址則返回true。
- 8、
boolean isMCSiteLocal()
:如果地址是一個網站範圍組播地址則返回true。
- 9、
boolean isMCLinkLocal()
:如果地址是一個子網範圍組播地址則返回true。
- 10、
boolean isMCNodeLocal()
:如果地址是一個本地介面組播地址則返回true。
實際上,我們很少用到這十個方法。
InetAddress的可達性測試
InetAddress提供兩個isReaachable()
方法用於測試可達性。其實就是測試一個特定的節點對當前主機是否可達(兩者是否能夠建立一個網路連接)。因為網路連接有可能因為多種原因阻塞,列舉一些原因如下:
- 防火牆攔截。
- Proxy 伺服器攔截。
- 不能正常服務的路由器。
- 斷開的網路線纜。
- 嘗試串連的遠端電腦沒有開機。
public boolean isReachable(int timeout) throws IOExceptionpublic boolean isReachable(NetworkInterface netif, int ttl, int timeout) throws IOException
這兩個方法會嘗試使用Traceroute查看指定地址是否可達。Traceroute程式使用ICMP報文和IP首部中的TTL欄位(一般為64)。TTL欄位的目的是防止資料報在選路時候無休止的在網路中流動(當路由故障的時候,可能在兩個路由迴圈)。可以理解TTL欄位用於控制串連被丟棄之前的網路最大跳數。第一個方法isReachable(int timeout)
只有一個參數控制檢測可達性的逾時毫秒數,第二個方法可以控制指定本地的網路介面、TTL參數和逾時時間進行可達性測試。
public static void main(String[] args) throws Exception{ InetAddress inetAddress = InetAddress.getByName("www.baidu.com"); System.out.println(inetAddress.isReachable(2000)); }
比較不同的InetAddress
InetAddress中覆蓋了equals
和hashCode
方法,比較的時候,實際上比較的是address屬性,也就是IP地址,換言之,只要兩個InetAddress的IP地址一致,這兩個InetAddress對象就是相等的,並不要求兩個InetAddress對象的主機名稱一致。舉個例子:
public static void main(String[] args) throws Exception { while (true){ Thread.sleep(500); InetAddress inetAddress = InetAddress.getByName("www.baidu.com"); InetAddress other = InetAddress.getByName("14.215.177.39"); System.out.println(inetAddress.equals(other)); } }
上面的main方法執行之後,基本上列印true,取決於DNS的處理。
Inet4Address和Inet6Address
Inet4Address和Inet6Address兩個類都繼承自InetAddress,它們分別是IPv4和IPv6的IP地址的高度抽象表示。但是,一般情況下,開發人員使無須感知使用或者串連的IP地址到底是IPv4和IPv6的IP地址,因為通常我們都是通過主機名稱(host)去訪問。
本網介面
NetworkInterface是本網介面,實際上它可以表示一個物理介面,如乙太網路卡,它也可以表示一個虛擬介面,與機器的其他IP地址綁定到同一個物理硬體,最常見的就是現在的虛擬化容器如Docker提供的網卡。NetworkInterface類提供的一些方法可以枚舉所有的本地地址,通過這些本地地址建立的InetAddress對象,建立出來的這些InetAddress對象就可以使用在用戶端Socket或者服務端Socket。
NetworkInterface的建立方法
static NetworkInterface getByName(String name) throws SocketException
getByName(String name)
方法可以指定網路介面名字擷取對應的網路介面執行個體NetworkInterface,如果沒有這樣名字的網路介面則返回null。網路介面的名字格式和平台相關,例如典型的Unix系統上,乙太網路介面名字的形式為eth0、eth1等等,在Windows系統中名字類似於"CE31"、"ELX100"等字串,"lo"一般是本地傳回位址的網路介面名字。
另外,可以通過InetAddress返回指定IP綁定的網路介面(或者說返回的網路介面處理指定的IP地址),如果本地主機沒有網路介面和傳入的IP地址綁定則返回null,如果發生錯誤則拋出一個SocketException。
static NetworkInterface getByInetAddress(InetAddress addr) throws SocketException
最後,可以使用下面的方法枚舉本地主機上的所有網路介面。
static Enumeration<NetworkInterface> getNetworkInterfaces() throws SocketException
筆者用的是Windows10系統,嘗試枚舉一下所有的網路介面:
public static void main(String[] args) throws Exception{ Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); while (networkInterfaces.hasMoreElements()){ NetworkInterface networkInterface = networkInterfaces.nextElement(); System.out.println(networkInterface); } }
執行結果部分如下:
name:lo (Software Loopback Interface 1)name:ppp0 (WAN Miniport (PPPOE))name:net0 (Microsoft ISATAP Adapter #2)name:net1 (Microsoft ISATAP Adapter)name:net2 (WAN Miniport (L2TP))name:net3 (WAN Miniport (IKEv2))...
NetworkInterface提供的屬性擷取方法
NetworkInterface提供一個執行個體方法public Enumeration<InetAddress> getInetAddresses()
用於擷取綁定在一個網路介面上面的所有IP地址,雖然這種情況不常見,但是確實存在。
執行個體方法public String getName()
返回NetworkInterface執行個體的對象名稱,例如eth0、lo等。
執行個體方法public String getDsiplayName()
也是返回NetworkInterface執行個體的對象名稱,不過表示的形式更加友好,例如"Ethernet Card 0"(eth0),但是在Unix系統中和getName()
相同。
小結
只有理解網路編程中的一些基礎概念,才能鋪好學習URI(URL)、TCP協議、HTTP協議和通訊端(Socket)的路。
(本文完 c-2-d)
網路編程-Java中的Internet查詢