【java網路】IO程式設計
Prequirement
- 在繼續閱讀這篇文章之前,請務必先閱讀前面這篇Java IO概述,因為Java把所有的IO都統一成流(Stream)了。
- TCP/IP協議棧。知道IP、埠、DNS、Socket、URL、TCP、UDP、HTTP等網路相關知識。
IP地址: InetAddress
java.net.InetAddress類是Java對IP地址(包括IPv4和IPv6)的封裝。一般來說,它同時包含主機名(hostname)和IP地址。
1. 建立方式(工廠方法)
- public static InetAddress getByName(String hostName) throws UnknownHostException;
- public static InetAddress[] getAllByName(String hostName) throws UnknownHostException;
- public static InnetAddress getLocalHost() throws UnknownHostException;
說明
- 所有這三個方法都可能在必要的時候連線本地DNS伺服器,進行域名解析。
- 由於DNS查詢成本相對比較高,InetAddress類會快取查詢的結果。不成功的DNS查詢快取預設是10s。這兩個快取時間可以通過networkaddress.cache.ttl和networkaddress.cache.negative.ttl控制。
- 雖然名稱寫著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)。 - 當使用IP地址字串作為引數呼叫getByName()時,是不需要檢查DNS的。這表示可能為實際上不存在也無法連線的主機建立InetAddress物件。但是,當顯式地通過getHostName()請求此主機名時,會進行實際主機名的DNS查詢。但是這時候DNS查詢失敗,不會拋UnknownHostException異常。
- getAllByname(hostName)返回所有對應hostName的地址。雖然一個域名繫結多個IP並不少見,但是對於客戶端來說往往只需要連線其中的一個即可,所以這個方法並不常用。
- getLocalHost()方法返回執行機器的InetAddress。如果本機沒有固定IP地址或者域名,可能會得到localhost作為域名,127.0.0.1作為IP地址的InetAddress物件。
2. 常用的方法
前面說過InetAddress類是Java對IP地址(包括IPv4和IPv6)的封裝。一般來說,它同時包含主機名(hostname)和IP地址。所以通過InetAddress我們可以得到hostName或者ip地址:
- public String getHostName()
- public String getHostAddress()
- public byte[] getAddress()
說明
- 沒有setter方法,原因很明顯,不多說
- getHostName()方法一般返回主機名,如果這臺機器沒有主機名或者安全管理器阻止確定主機名,就返回點分四段格式的數字IP地址。
- 如果InetAddress是通過getByName(數字IP地址字串)構造的,那麼getHostName會在這裡進行DNS查詢。
- getHostAddress()返回包括點分四段格式IP地址的字串。
- 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));
工廠方法
- public static NetworkInterface getByName(String name) throws SocketException
- public static NetworkInterface getByInetAddress(InetAddress address) throws SocketException
- public static Enumeration getNetworkInterfaces() throws SocketException
說明
- 網路介面名稱與平臺相關,unix系統一般命名為eth0、eth1等等,本地迴路地址名可能是類似於lo的。
- 記住下面的對映關係:一臺機器可能有多個網路介面(NetworkInterface),一個網路介面(NetworkInterface)可能綁定了多個IP地址(InetAddress)。
TCP: Socket和ServerSocket
TCP是為可靠傳輸而設計的。它主要有如下特點:
- 面向連線
- 面向位元組流
- 丟包重傳和和亂序重排
- 流量控制
Socket類
Socket是兩臺主機之間的一個連線。它可以進行七項基本操作:
- 連線遠端機器
- 傳送資料
- 接收資料
- 關閉連線
- 繫結埠
- 監聽入站資料
- 在所繫結的埠上接收來自遠端機器的連線
說明
- Java的Socket類可同時用於客戶端和伺服器,它有對應於前四項操作的方法。後三項只有伺服器才需要,這些操作通過ServerSocket類實現。
- TCP是面向位元組流的協議,所以資料的傳送和接收通過socket關聯的輸入輸出流進行,操作起來跟檔案是類似的。
java.net.Socket類是Java執行客戶端TCP操作的基礎類。其他進行TCP網路連線的面向客戶端的類,如URL、URLConnection等,最終都會呼叫到Socket類的方法。
Socket在資料結構上,是 <IP, port> 的組合。其中IP可以通過InetAddress進行主機名和IP地址的轉換和表示,port是埠號,必須在0到65535之間。
建構函式
- public Socket(String host, int port) throws UnknownHostException, IOException
- public Socket(InetAddress host, int port) throws IOException
- public Socket(String host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException
- public Socket(InetAddress host, int port, InetAddress interface, int localPort) throws IOException, UnknownHostException
- public Socket()
- public Socket(SocketImpl impl)
- public Socket(Proxy proxy)
說明
- 上面前四個建構函式,在建立Socket的時候就會嘗試連線指定的伺服器。後三個建構函式用於建立未連線的socket物件。
- 第三和第四個建構函式,連線到前兩個引數指定的主機和埠,從後兩個引數指定的本機網路介面和埠進行連線。如果0傳遞給localPort引數,Java會隨機選擇一個1024~65535之間的可用埠。
常用方法
- public InetAddress getInetAddress()
- public int getPort()
- public int getLocalPort()
- public InetAddress getLocalAddress()
- public InputStream getInputStream() throws IOException
- public OutputStream getOutputStream() throws IOException
- public void close() throws IOException
- public void shutdownInput() throws IOException
- public void shutdownOutput() throws IOException
說明
- TCP是面向位元組流的協議,Socket其實就是一個檔案控制代碼,所以通過它關聯的輸入輸出流實現資料的讀寫。
- 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
說明
- 正常情況下,小的包在傳送前會組合為大點的包。在傳送另一個包之前,本地主機要等待遠端系統對前一個包的迴應,這稱之為Nagle演算法。Nagle演算法主要是為了解決“糊塗視窗綜合症”的。但是Nagle演算法也會帶來一些問題。如果遠端系統沒有儘可能快地將回應傳送會本地系統,那麼依賴於小資料量資訊穩定傳播的應用程式會變得很慢。設定TCP_NODELAY為true可以打破這種緩衝模式,這樣所有的包一就緒就能傳送。
- SO_LINGER選項規定,當socket關閉時如何處理尚未傳送的資料包。預設情況下,close()方法將立即返回,但系統仍會嘗試傳送剩餘的資料。如果延遲時間設定為0,那麼當socket關閉時,所有為傳送的資料將都被丟棄。如果延遲hi見設定為任意正數,那麼close()方法將會阻塞指定秒數,等待資料傳送和接收回應,然後socket就會被關閉,所有剩餘的資料都不會發送,也不會收到迴應。這個引數和SO_REUSEADDR選項可以在一定程度上解決time_wait狀態佔用埠問題,但是同樣會帶來一些問題。具體可以參考筆者前面寫的一篇文章如何解決time_wait狀態佔用埠問題。
- 當socket關閉時,為了確保收到所有定址到此埠的延遲資料,會等待一段時間,也就是進入所謂的time_wait狀態。系統不會對後接收的任何包進行操作,只是希望確保這些資料不會偶然地傳入綁定於相同埠的新程序。如果開啟SO_REUSEADDR(預設情況是關閉),就允許另一個socket繫結到一個尚未釋放的埠,儘管此時仍有可能存在前一個socket未接收的資料。
- 如果啟用SO_KEEPALIVE,客戶端會偶爾通過一個空閒連線傳送一個數據包(一般兩小時一次),以確保伺服器為崩潰。如果伺服器沒有響應此包,客戶端會嘗試11分鐘多的時間,知道接收到響應為止。如果在12分鐘內未收到響應,客戶端就關閉socket。沒有SO_KEEPALIVE,不活動的客戶端可能會永久存在下去,而不會注意到伺服器已經崩潰。SO_KEEPALIVE預設值是false。
SocketAddress
SocketAddress類的主要用途是為暫時的socket連線資訊(IP地址和埠)提供方便的儲存,這些資訊可以重用以建立新的socket,即使最初的socket已斷開並被垃圾回收。為此,Socket類提供了兩個返回SocketAddress的方法:
- getRemoteSocketAddress
- getLocalSocketAddress
對於未連線的socket,通過connect()方法進行連線時就必須使用SocketAddress:
public void connect(SocketAddress endpoint) throws IOExceptionpublic void connect(SocketAddress endpoint, int timeout) throws IOException
ServerSocket
對於接收連線的伺服器,Java提供了伺服器Socket的ServerSocket類。
建構函式
- public ServerSocket(int port) throws BindException, IOException
- public ServerSocket(int port, int queueLength) throws BindException, IOException
- public ServerSocket(int port, int queueLenght, InetAddress bindAddress) throws IOException
- public ServerSocket() throws IOException
無參建構函式需要在以後使用bind()進行繫結:
- public void bind(SocketAddress endpoint) throws IOException
- public void bind(SocketAddress endpoint, int queueLength) throws IOException
主要用途是,允許程式在繫結埠前設定伺服器Socket的選項。
ServerSocket ss = new ServerSocket();// 設定socket選項SocketAddress http = new InetSocketAddress(80);ss.bind(http);
接受和關閉連線
- public Socket accept() throws IOException
- 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
接收資料報的建構函式
- public DatagramPacket(byte[] buffer, int length)
- 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);
傳送資料報的建構函式
- public DatagramPacket(byte[] data, int length, InetAddress destination, int port)
- public DatagramPacket(byte[] data, int offset, int length, InetAddress destination, int port) // Java 1.2
- public DatagramPacket(byte[] data, int length, SocketAddress destination, int port) // Java 1.4
- public DatagramPacket(byte[] data, int offset, int length, SocketAddress destination, int port) // Java 1.4
獲取和設定資料包中的資料
- public byte[] getData()
- 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
建構函式
- public DatagramSocket() throws SocketException // 繫結匿名埠
- public DatagramSocket(int port) throws SocketException // 指定埠
- public DatagramSocket(int port, InetAddress interface) throws SocketException
- public DatagramSocket(SocketAddress interface) throws SocketException // Java 1.4
- public DatagramSocket(DatagramSocketImpl imp) throws SocketException // Java 1.4
收發資料報
- public void send(DatagramPacket dp) throws IOException
- public void receive(DatagramPacket dp) throws IOException
說明
- receive()方法會阻塞呼叫執行緒,直到資料報到達。可以通過SO_TIMEOUT設定超時時間。
- 資料報的緩衝區應當足夠大,以儲存接收的資料。否則,receive()會在緩衝區中放置能儲存的儘可能多的資料;其他資料就會丟失。因為UDP資料報的資料部分最長為65507位元組,所以最多需要分配65507位元組空間就可以了。具體的值可以協商確定。
轉載自:http://ju.outofmemory.cn/entry/87284