1. 程式人生 > 其它 >【java網路】IO程式設計

【java網路】IO程式設計

Prequirement

  1. 在繼續閱讀這篇文章之前,請務必先閱讀前面這篇Java IO概述,因為Java把所有的IO都統一成流(Stream)了。
  2. TCP/IP協議棧。知道IP、埠、DNS、Socket、URL、TCP、UDP、HTTP等網路相關知識。

IP地址: InetAddress

java.net.InetAddress類是Java對IP地址(包括IPv4和IPv6)的封裝。一般來說,它同時包含主機名(hostname)和IP地址。

1. 建立方式(工廠方法)

  1. public static InetAddress getByName(String hostName) throws UnknownHostException;
  2. public static InetAddress[] getAllByName(String hostName) throws UnknownHostException;
  3. public static InnetAddress getLocalHost() throws UnknownHostException;

說明

  1. 所有這三個方法都可能在必要的時候連線本地DNS伺服器,進行域名解析。
  2. 由於DNS查詢成本相對比較高,InetAddress類會快取查詢的結果。不成功的DNS查詢快取預設是10s。這兩個快取時間可以通過networkaddress.cache.ttl和networkaddress.cache.negative.ttl控制。
  3. 雖然名稱寫著getByName(hostName),看起來是使用DNS查詢給定hostName對應的IP地址。但是其實這個方法是可以接收包含點分四段或者十六進位制形式的IP地址字串的。如InetAddress address =InetAddress.getByName("8.8.8.8");。JDK1.4及以後的版本,對這種情況提供了單獨的介面: 1. public static InetAddress getByName(byte[] address) throws UnknownHostException; 2. public static InetAddress getByName(String hostName, byte[] address) throws UnknownHostException; 陣列的長度必須是4或者16(即IPv4或者IPv6)。
  4. 當使用IP地址字串作為引數呼叫getByName()時,是不需要檢查DNS的。這表示可能為實際上不存在也無法連線的主機建立InetAddress物件。但是,當顯式地通過getHostName()請求此主機名時,會進行實際主機名的DNS查詢。但是這時候DNS查詢失敗,不會拋UnknownHostException異常。
  5. getAllByname(hostName)返回所有對應hostName的地址。雖然一個域名繫結多個IP並不少見,但是對於客戶端來說往往只需要連線其中的一個即可,所以這個方法並不常用。
  6. getLocalHost()方法返回執行機器的InetAddress。如果本機沒有固定IP地址或者域名,可能會得到localhost作為域名,127.0.0.1作為IP地址的InetAddress物件。

2. 常用的方法

前面說過InetAddress類是Java對IP地址(包括IPv4和IPv6)的封裝。一般來說,它同時包含主機名(hostname)和IP地址。所以通過InetAddress我們可以得到hostName或者ip地址:

  1. public String getHostName()
  2. public String getHostAddress()
  3. public byte[] getAddress()

說明

  1. 沒有setter方法,原因很明顯,不多說
  2. getHostName()方法一般返回主機名,如果這臺機器沒有主機名或者安全管理器阻止確定主機名,就返回點分四段格式的數字IP地址。
  3. 如果InetAddress是通過getByName(數字IP地址字串)構造的,那麼getHostName會在這裡進行DNS查詢。
  4. getHostAddress()返回包括點分四段格式IP地址的字串。
  5. getAddress()返回網路位元組順序的IP地址。返回的位元組是無符號的,因為Java都是有符號的。值大於127的位元組會被當作負數。因此,如果要對getAddress()返回的位元組數值進行操作,需要把位元組提升為int,進行適當的調整。比如:int unsignedByte= signedByte < 0 ? signedByte + 256 : signedByte;

3. Inet4Address 和 Inet6Address

Java 1.4引入了兩個新類,Inet4Address和Inet6Address,以此區分IPv4和IPv6地址:

public final class Inet4Address extends InetAddresspublic final class Inet6Address extends InetAddress

不過基本上你不需要考慮一個地址是IPv4還是IPv6地址。這兩個類屬於JDK本身實現細節,你只需要關注基類InetAddress即可,面向物件程式設計的好處在這裡體現出來。

網路介面: NetworkInterface

Java 1.4 添加了一個java.net.NetworkInterface類,表示計算機與網路的互聯點。一個NetworkInterface一般是指網絡卡地址(Network Interface Card(NIC)),但是不一定是硬體的形式。軟體模擬的網路介面也是用NetworkInterface表示。例如loopback interface(127.0.0.1 for IPv4或者::1 for IPv6)。總而言之,NetworkInterface用來表示物理和虛擬的網絡卡地址。下面這段程式碼:

Socket soc = new java.net.Socket();soc.connect(new InetSocketAddress(address, port));

作業系統會為我們選擇一個網路介面傳送和接收資料。但是我們也可以告訴作業系統使用哪個網絡卡:

NetworkInterface nif = NetworkInterface.getByName("eth0");Enumeration<InetAddress> nifAddresses = nif.getInetAddresses();Socket soc = new java.net.Socket();soc.bind(new InetSocketAddress(nifAddresses.nextElement(), 0));soc.connect(new InetSocketAddress(address, port));

工廠方法

  1. public static NetworkInterface getByName(String name) throws SocketException
  2. public static NetworkInterface getByInetAddress(InetAddress address) throws SocketException
  3. public static Enumeration getNetworkInterfaces() throws SocketException

說明

  1. 網路介面名稱與平臺相關,unix系統一般命名為eth0、eth1等等,本地迴路地址名可能是類似於lo的。
  2. 記住下面的對映關係:一臺機器可能有多個網路介面(NetworkInterface),一個網路介面(NetworkInterface)可能綁定了多個IP地址(InetAddress)。

TCP: Socket和ServerSocket

TCP是為可靠傳輸而設計的。它主要有如下特點:

  1. 面向連線
  2. 面向位元組流
  3. 丟包重傳和和亂序重排
  4. 流量控制

Socket類

Socket是兩臺主機之間的一個連線。它可以進行七項基本操作:

  1. 連線遠端機器
  2. 傳送資料
  3. 接收資料
  4. 關閉連線
  5. 繫結埠
  6. 監聽入站資料
  7. 在所繫結的埠上接收來自遠端機器的連線

說明

  1. Java的Socket類可同時用於客戶端和伺服器,它有對應於前四項操作的方法。後三項只有伺服器才需要,這些操作通過ServerSocket類實現。
  2. TCP是面向位元組流的協議,所以資料的傳送和接收通過socket關聯的輸入輸出流進行,操作起來跟檔案是類似的。

java.net.Socket類是Java執行客戶端TCP操作的基礎類。其他進行TCP網路連線的面向客戶端的類,如URL、URLConnection等,最終都會呼叫到Socket類的方法。

Socket在資料結構上,是 <IP, port> 的組合。其中IP可以通過InetAddress進行主機名和IP地址的轉換和表示,port是埠號,必須在0到65535之間。

建構函式

  1. public Socket(String host, int port) throws UnknownHostException, IOException
  2. public Socket(InetAddress host, int port) throws IOException
  3. public Socket(String host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException
  4. public Socket(InetAddress host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException
  5. public Socket()
  6. public Socket(SocketImpl impl)
  7. public Socket(Proxy proxy)

說明

  1. 上面前四個建構函式,在建立Socket的時候就會嘗試連線指定的伺服器。後三個建構函式用於建立未連線的socket物件。
  2. 第三和第四個建構函式,連線到前兩個引數指定的主機和埠,從後兩個引數指定的本機網路介面和埠進行連線。如果0傳遞給localPort引數,Java會隨機選擇一個1024~65535之間的可用埠。

常用方法

  1. public InetAddress getInetAddress()
  2. public int getPort()
  3. public int getLocalPort()
  4. public InetAddress getLocalAddress()
  5. public InputStream getInputStream() throws IOException
  6. public OutputStream getOutputStream() throws IOException
  7. public void close() throws IOException
  8. public void shutdownInput() throws IOException
  9. public void shutdownOutput() throws IOException

說明

  1. TCP是面向位元組流的協議,Socket其實就是一個檔案控制代碼,所以通過它關聯的輸入輸出流實現資料的讀寫。
  2. shutdownInput和shutdownOutput僅僅關閉連線的一半。需要注意的是shutdown方法隻影響socket的流,並不釋放與socket相關的資源,如佔用的埠等。所以仍然需要在結束使用後close掉該socket。

設定Socket選項

  • TCP_NODELAY
  • SO_BINDADDR
  • SO_TIMEOUT
  • SO_LINGER
  • SO_REUSEADDR
  • SO_SNDBUF
  • SO_RCVBUF
  • SO_KEEPALIVE
  • OOBINLINE

說明

  1. 正常情況下,小的包在傳送前會組合為大點的包。在傳送另一個包之前,本地主機要等待遠端系統對前一個包的迴應,這稱之為Nagle演算法。Nagle演算法主要是為了解決“糊塗視窗綜合症”的。但是Nagle演算法也會帶來一些問題。如果遠端系統沒有儘可能快地將回應傳送會本地系統,那麼依賴於小資料量資訊穩定傳播的應用程式會變得很慢。設定TCP_NODELAY為true可以打破這種緩衝模式,這樣所有的包一就緒就能傳送。
  2. SO_LINGER選項規定,當socket關閉時如何處理尚未傳送的資料包。預設情況下,close()方法將立即返回,但系統仍會嘗試傳送剩餘的資料。如果延遲時間設定為0,那麼當socket關閉時,所有為傳送的資料將都被丟棄。如果延遲hi見設定為任意正數,那麼close()方法將會阻塞指定秒數,等待資料傳送和接收回應,然後socket就會被關閉,所有剩餘的資料都不會發送,也不會收到迴應。這個引數和SO_REUSEADDR選項可以在一定程度上解決time_wait狀態佔用埠問題,但是同樣會帶來一些問題。具體可以參考筆者前面寫的一篇文章如何解決time_wait狀態佔用埠問題
  3. 當socket關閉時,為了確保收到所有定址到此埠的延遲資料,會等待一段時間,也就是進入所謂的time_wait狀態。系統不會對後接收的任何包進行操作,只是希望確保這些資料不會偶然地傳入綁定於相同埠的新程序。如果開啟SO_REUSEADDR(預設情況是關閉),就允許另一個socket繫結到一個尚未釋放的埠,儘管此時仍有可能存在前一個socket未接收的資料。
  4. 如果啟用SO_KEEPALIVE,客戶端會偶爾通過一個空閒連線傳送一個數據包(一般兩小時一次),以確保伺服器為崩潰。如果伺服器沒有響應此包,客戶端會嘗試11分鐘多的時間,知道接收到響應為止。如果在12分鐘內未收到響應,客戶端就關閉socket。沒有SO_KEEPALIVE,不活動的客戶端可能會永久存在下去,而不會注意到伺服器已經崩潰。SO_KEEPALIVE預設值是false。

SocketAddress

SocketAddress類的主要用途是為暫時的socket連線資訊(IP地址和埠)提供方便的儲存,這些資訊可以重用以建立新的socket,即使最初的socket已斷開並被垃圾回收。為此,Socket類提供了兩個返回SocketAddress的方法:

  1. getRemoteSocketAddress
  2. getLocalSocketAddress

對於未連線的socket,通過connect()方法進行連線時就必須使用SocketAddress:

public void connect(SocketAddress endpoint) throws IOExceptionpublic void connect(SocketAddress endpoint, int timeout) throws IOException

ServerSocket

對於接收連線的伺服器,Java提供了伺服器Socket的ServerSocket類。

建構函式

  1. public ServerSocket(int port) throws BindException, IOException
  2. public ServerSocket(int port, int queueLength) throws BindException, IOException
  3. public ServerSocket(int port, int queueLenght, InetAddress bindAddress) throws IOException
  4. public ServerSocket() throws IOException

無參建構函式需要在以後使用bind()進行繫結:

  1. public void bind(SocketAddress endpoint) throws IOException
  2. public void bind(SocketAddress endpoint, int queueLength) throws IOException

主要用途是,允許程式在繫結埠前設定伺服器Socket的選項。

ServerSocket ss = new ServerSocket();// 設定socket選項SocketAddress http = new InetSocketAddress(80);ss.bind(http);

接受和關閉連線

  1. public Socket accept() throws IOException
  2. public void close() throws IOException

UDP: DatagramPacket和DatagramSocket

UDP速度很快,但是不可靠。它沒有連線的概念。其次,不想TCP是面向位元組流的,UDP是面向資料包的,是有報文邊界的。

TIPS

TCP和UDP的區別一般可以通過電話系統和郵局來做對照解釋。TCP就像電話系統,當你撥號時,電話會得到應答,在雙方之間建立起一個連線。當你撥號時,你知道另一方會以你說的順序聽到你說的話。如果電話忙或者沒有人應答,你會馬上發現。相反,UDP就像郵局系統。你向一個地址傳送郵件包,大多數信件都會到達,但有些可能會在路上丟失。信件可能以傳送的順序到達,但無法保證這點。離接收方越遠,郵件就越有可能丟失或者亂序到達。如果這是個問題,你可以在信封上寫上序號,然後要求接收方以正確的順序排列,並向你發郵件來告訴哪些郵件已到達,這樣可以重新發送丟失的郵件。但是,你和對方需要預先約定協商好此協議,郵局不會為你做這件事情。

Java中UDP的實現分為兩個類:DatagramPacket和DatagramSocket。DatagramPacket類將資料位元組填充到稱為資料報(datagram)的UDP包中。而DatagramSocket可以收發DatagramPacket資料報。

DatagramPacket

由於埠號是以2位元組無符號整數給出,因此每臺主機有65536個不同的UDP埠可以使用。因為TCP埠和UDP埠沒有關聯,所以TCP和UDP是可以使用相同的埠號的。

因為長度也是以2位元組無符號整數給出,所以資料報中的位元組數限制為65536-8個位元組(首部要用8個位元組)。不過,這與IP首部中的資料報長度欄位是冗餘的,IP首部將資料報限制在65467~65507位元組之間(具體是多少取決於IP首部的大小)。

雖然UDP包中的資料的理論最大數量是65507位元組,但實際上幾乎總是比這少得多。在許多平臺下,實際的限制是8192位元組(8K)。因此,如果程式依賴於傳送長於8K資料的UDP包,要對這些程式多加小心。大多數時候,更大的包會被簡單地擷取到8K資料,Java程式將得不到任何通知(畢竟UDP是一種不可靠的協議)。

在Java中,UDP資料報用DatagramPacket類的例項表示:

public final class DatagramPacket extends Object

接收資料報的建構函式

  1. public DatagramPacket(byte[] buffer, int length)
  2. public DatagramPacket(byte[] buffer, int offset, int length)

說明 length 必須 <= buffer.length - offset。否則會拋IllegalArgumentException。

示例程式碼:

byte[] buffer = new byte[8192];DatagramPacket dp = new DategramPacket(buffer, buffer.length);

傳送資料報的建構函式

  1. public DatagramPacket(byte[] data, int length, InetAddress destination, int port)
  2. public DatagramPacket(byte[] data, int offset, int length, InetAddress destination, int port) // Java 1.2
  3. public DatagramPacket(byte[] data, int length, SocketAddress destination, int port) // Java 1.4
  4. public DatagramPacket(byte[] data, int offset, int length, SocketAddress destination, int port) // Java 1.4

獲取和設定資料包中的資料

  1. public byte[] getData()
  2. public void setData(byte[] data)

TIPS

可以看到,雖然UDP是面向報文的,但是實際上操作的也是位元組陣列。傳送和獲取UDP資料都是如此。所以如何與byte陣列打交道才是最重要的。一般來說,如果是文字內容,可以將byte[]與String進行轉換,注意編碼問題:

String s = new String(datagramPacket.getData(), "UTF-8");

如果是二進位制內容,那麼可以轉換為ByteArrayInputStream:

InputStream in = new ByteArrayInputStream(packet.getData(), packet.getOffset(), packet.getLength());

然後ByteArrayInputStream可以連結到DataInputStream:

DataInputStream din = new DataInputStream(in);

接下來,就可以使用DataInputStream的readLong()、readInt()、readChar()及其他方法讀取資料了。當然,這是假定資料報的傳送方使用的資料格式與Java使用的資料格式相同的情況的做法。如果不是,那麼不能這樣子反序列化資料。

注意:當構造ByteArrayInputStream時,必須指明offset和length。因為packet.getData()返回的陣列可能包括沒有拿到網路資料填充的額外空間。這些空間包含的資料即為構造DatagramPacket時該陣列相應部分中包含的任意隨機值。

DatagramSocket

建構函式

  1. public DatagramSocket() throws SocketException // 繫結匿名埠
  2. public DatagramSocket(int port) throws SocketException // 指定埠
  3. public DatagramSocket(int port, InetAddress interface) throws SocketException
  4. public DatagramSocket(SocketAddress interface) throws SocketException // Java 1.4
  5. public DatagramSocket(DatagramSocketImpl imp) throws SocketException // Java 1.4

收發資料報

  1. public void send(DatagramPacket dp) throws IOException
  2. public void receive(DatagramPacket dp) throws IOException

說明

  1. receive()方法會阻塞呼叫執行緒,直到資料報到達。可以通過SO_TIMEOUT設定超時時間。
  2. 資料報的緩衝區應當足夠大,以儲存接收的資料。否則,receive()會在緩衝區中放置能儲存的儘可能多的資料;其他資料就會丟失。因為UDP資料報的資料部分最長為65507位元組,所以最多需要分配65507位元組空間就可以了。具體的值可以協商確定。

轉載自:http://ju.outofmemory.cn/entry/87284