本文目的是大概瞭解 Java 網絡編程體系,須要一點點 Java IO 基礎,推薦教程 系統學習 Java IO。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程《Java Networking》 http://tutorials.jenkov.com/java-networking/index.htmlhtml
Java 有一個至關容易使用的內置網絡 API,能夠很容易地經過互聯網上的 TCP / IP 套接字或 UDP 套接字進行通訊。 TCP 一般比 UDP 使用得更頻繁。java
即便 Java Networking API 容許經過套接字打開和關閉網絡鏈接,但全部通訊都經過 Java IO 類 InputStream 和 OutputStream 實現的。
或者,咱們可使用 Java NIO API 中的網絡類。 用法相似於 Java Networking API 中的類,但 Java NIO API 能夠在非阻塞模式下工做。 在某些狀況下,非阻塞模式可提高性能。git
一般,客戶端會打開與服務器的 TCP / IP 鏈接,而後開始與服務器通訊,當通訊結束後客戶端關閉鏈接。以下圖:
github
客戶端能夠經過一個已打開的鏈接發送多個請求,實際上,客戶端能夠向服務器發送儘量多的數據。 固然,若是須要,服務器也能夠關閉鏈接。編程
當客戶端想要打開到服務器的 TCP / IP 鏈接時,它使用 Java Socket 類來實現。 套接字被告知鏈接到哪一個 IP 地址和 TCP 端口,其他部分由 Java 完成。數組
若是要啓動服務器以偵聽來自某個 TCP 端口上的客戶端的傳入鏈接,則必須使用 Java ServerSocket 類。 當客戶端經過客戶端套接字鏈接到服務器的 ServerSocket 時,服務器上會爲該鏈接分配一個 Socket 。 客戶端和服務器的通訊就是 Socket 到 Socket 的通訊了。服務器
Socket和ServerSocket在後面的文本中有更詳細的介紹。網絡
UDP 的工做方式與 TCP 略有不一樣。 使用 UDP ,客戶端和服務器之間沒有鏈接。 客戶端能夠向服務器發送數據,而且服務器能夠(或能夠不)接收該數據。 客戶端永遠不會知道數據是否在另外一端收到。 從服務器到客戶端發送的數據也是如此。
因爲沒法保證數據傳輸,所以 UDP 協議的協議開銷較小。多線程
在一些狀況下,無鏈接 UDP 模型優於 TCP ,好比傳輸視頻等多媒體文件,缺乏一些數據是不影響觀看的。socket
爲了經過 Internet 鏈接到服務器(經過TCP / IP),須要建立一個 Socket 並將其鏈接到服務器。 或者,若是您更喜歡使用 Java NIO ,則可使用 Java NIO SocketChannel 。
Socket socket = new Socket("baidu.com", 80);
第一個參數是地址,能夠是 ip 或者域名字符串,第二個參數是端口,端口80是Web服務器端口。
要寫入 Socket,必須獲取其 OutputStream :
Socket socket = new Socket("baidu.com", 80); OutputStream outputStream = socket.getOutputStream(); outputStream.write("some data".getBytes()); outputStream.flush(); outputStream.close(); socket.close();
當真的但願經過互聯網向服務器發送數據時,不要忘記調用 flush() 。操做系統中的底層 TCP / IP 實現會先緩衝數據,緩衝塊的大小是與 TCP / IP 數據包的大小相適應的,這就是說,調用 flush() 只是通知系統發送,但系統並非當即就幫忙發出去。
要從 Socket 讀取,須要獲取其 InputStream :
Socket socket = new Socket("baidu.com", 80); InputStream in = socket.getInputStream(); int data = in.read(); //... read more data... in.close(); socket.close();
記住,在讀取時咱們不能使用讀取 InputStream 返回 -1 來判斷數據讀取結束 ,由於只有在服務器關閉鏈接時才返回 -1 。 可是服務器可能並不老是關閉鏈接,好比經過同一鏈接發送多個請求。 在這種狀況下,關閉鏈接將是很是愚蠢的。
相反,必須知道從 Socket 的 InputStream 中讀取多少字節。 服務器會告知 Socket 它發送的字節數,或者經過查找特殊的數據結束字符來完成。
使用 Socket 後,必須關閉它以關閉與服務器的鏈接,這能夠經過調用 Socket 對象的 close() 方法完成。
可使用 ServerSocket 來實現 Java 服務器,這樣就能夠經過 TCP / IP 偵聽來自客戶端的傳入鏈接。若是更喜歡使用 Java NIO 而不是 Java Networking(標準API),那麼也可使用 ServerSocketChannel 。
這是一個簡單的代碼示例,它建立一個偵聽端口 9000 的 ServerSocket:
ServerSocket serverSocket = new ServerSocket(9000);
要接受傳入鏈接,必須調用 ServerSocket.accept() 方法。 accept() 方法返回一個 Socket ,其行爲相似於普通的 Socket ,示例:
ServerSocket serverSocket = new ServerSocket(9000); boolean isStopped = false; while(!isStopped){ Socket clientSocket = serverSocket.accept(); //do something with clientSocket }
每次調用 accept() 方法時只打開一個傳入鏈接。
此外,只有在運行服務器的線程調用 accept() 時才能接受傳入鏈接。 線程在此方法以外執行的全部時間都沒有客戶端能夠鏈接。 所以,「accept」線程一般將傳入鏈接(Socket)傳遞給工做線程池,而後工做線程與客戶端進行通訊。 有關多線程服務器設計的更多信息,請參閱教程跟蹤 Java 多線程服務器。
一旦客戶端請求完成,而且不會從該客戶端收到進一步的請求,必須關閉該Socket,就像關閉普通客戶端Socket同樣。調用:socket.close();
一旦服務器關閉,就須要關閉 ServerSocket 。 調用:serverSocket.close();
DatagramSocket 是 Java 經過 UDP 而不是 TCP 進行網絡通訊的機制。 UDP 也是 IP 協議的上層。 可使用 DatagramSocket 來發送和接收 UPD 數據報。
經過 TCP 發送數據時,首先要建立鏈接。 創建 TCP 鏈接後,TCP 保證數據到達另外一端,或者它會告訴你發生了錯誤。
使用 UDP,只需將數據包(數據報)發送到網絡上的某個 IP 地址。 沒法保證數據會到達,也沒法保證 UDP 數據包到達的順序。 這意味着 UDP 比 TCP 具備更少的協議開銷(沒有流完整性檢查)。
UDP 適用於數據傳輸,若是數據包在轉換過程當中丟失則可有可無。 例如,想象一下經過互聯網傳輸直播電視信號,若是一兩幀丟失,這是可有可無的。咱們更不但願直播延遲只是爲了確保全部幀都顯示出來。 寧願跳過錯過的幀,並直接查看最新的幀。
還有實時監控視頻,寧願丟失一兩幀,也不想延遲於現實 30 秒。與攝像機錄像的存儲有點不一樣,將圖像從相機錄製到磁盤時, 爲了保證完整性,可能不但願丟失單幀,而是更願意稍微延遲。
此類表示數據報包。數據報包用來實現無鏈接包投遞服務。
Java 使用 DatagramSocket 表明 UDP 協議的 Socket ,DatagramSocket 自己只是碼頭,不維護狀態,不能產生IO流,它的惟一做用就是接收和發送數據報,使用 DatagramPacket 來表明數據報,DatagramSocket 接收和發送的數據都是經過 DatagramPacket 對象完成的。
每條報文僅根據該包中包含的信息從一臺機器路由到另外一臺機器。從一臺機器發送到另外一臺機器的多個包可能選擇不一樣的路由,也可能按不一樣的順序到達。不對包投遞作出保證。
引用自 李剛《瘋狂Java講義(第2版)》
其全部構造器以下:
方法 | 描述 |
---|---|
DatagramPacket(byte[] buf, int length) | 構造 DatagramPacket,用來接收長度爲 length 的數據包。 |
DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 構造數據報包,用來將長度爲 length 的包發送到指定主機上的指定端口號。 |
DatagramPacket(byte[] buf, int length, SocketAddress address) | 構造數據報包,用來將長度爲 length 的包發送到指定主機上的指定端口號。 |
以上3個在 byte[] buf 參數後面追加 int offset | 爲長度爲 length 的包設置偏移量爲 offset |
其中
要經過 DatagramSocket 發送數據,必須首先建立一個 DatagramPacket :
byte[] buffer = new byte[65508]; InetAddress address = InetAddress.getByName("baidu.com"); DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, 9000);
字節緩衝區(字節數組)是要在 UDP 數據報中發送的數據。 上述緩衝區的長度(65508字節)是能夠在單個 UDP 數據包中發送的最大數據量。
DatagramPacket 構造函數中的 buffer.length 是要發送的緩衝區中數據的長度,忽略該數據量以後緩衝區中的全部數據。
InetAddress 實例包含發送 UDP 數據包的節點(例如服務器)的地址。 InetAddress 類表示 IP 地址(Internet地址)。 getByName() 方法返回一個 InetAddress 實例,其 IP 地址與給定的主機名匹配。
port 參數是服務器接收數據正在偵聽的 UDP 端口,UDP 和 TCP 端口是不同的。同一臺計算機能夠有不一樣的線程同時監聽 UDP 的 80 端口和 TCP 中的 80 端口。不一樣協議下,端口號互不干擾,端口只是應用程序的標識。
建立一個 DatagramSocket :
DatagramSocket datagramSocket = new DatagramSocket();
要發送數據,請調用 send() 方法,以下所示:
datagramSocket.send(packet);
這是一個完整的例子:
public class DatagramExample { public static void main(String[] args) throws Exception { DatagramSocket datagramSocket = new DatagramSocket(); byte[] buffer = "123456789".getBytes(); InetAddress receiverAddress = InetAddress.getLocalHost(); DatagramPacket packet = new DatagramPacket(buffer, buffer.length, receiverAddress, 80); datagramSocket.send(packet); } }
經過 DatagramSocket 接收數據是經過首先建立 DatagramPacket 而後經過 DatagramSocket 的 receive() 方法接收數據來完成的。 這是一個例子:
DatagramSocket socket = new DatagramSocket(80); byte[] buffer = new byte[10]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet);
使用傳遞給構造函數的參數值 80 來實例化 DatagramSocket , 此參數是 DatagramSocket 接收 UDP 數據包的 UDP 端口。 如前所述,TCP 和 UDP 端口不相同,所以不重疊。 能夠在 TCP 和 UDP 80 端口上偵聽兩個不一樣的進程,而不會發生任何衝突。
其次,建立字節緩衝區和 DatagramPacket 。 注意 DatagramPacket 沒有關於要發送數據的節點的信息,就像建立 DatagramPacket 用於發送數據時同樣。 這是由於咱們將使用 DatagramPacket 接收數據而不是發送數據,所以,不須要目標地址。
最後調用 DatagramSocket 的 receive() 方法。 此方法將一直阻塞,直到收到 DatagramPacket 。
收到的數據位於 DatagramPacket 的字節緩衝區中。 這個緩衝區能夠經過調用以下代碼獲取:
byte[] buffer = packet.getData();
緩衝區會接收多少數據應該由你找到答案。 正在使用的協議應指定每一個 UDP 數據包發送的數據量,或指定能夠查找到的數據結束標記。真正的服務器程序可能會在循環中調用 receive() 方法,並將全部收到的 DatagramPacket 傳遞給工做線程池,就像 TCP 服務器對傳入鏈接同樣。
java.net 包中兩個有趣的類:URL 類和 URLConnection 類,這些類可用於建立與 Web 服務器(HTTP 服務器)的客戶端鏈接。 這是一個簡單的代碼示例:
public class URLExample { public static void main(String[] args) throws IOException { URL url = new URL("http://baidu.com"); URLConnection urlConnection = url.openConnection(); InputStream inputStream = urlConnection.getInputStream(); int data = inputStream.read(); while (data != -1) { System.out.print((char) data); data = inputStream.read(); } inputStream.close(); } }
將會輸出
<html> <meta http-equiv="refresh" content="0;url=http://www.baidu.com/"> </html>
URLConnection 類的做用是構造一個到指定 URL 的 URL 鏈接。它只有一個構造函數:URLConnection(URL url)
。
默認狀況下,URLConnection 向 Web 服務器發送 HTTP GET 請求,即查詢數據。若是要發送 HTTP POST 請求提交數據,請調用URLConnection.setDoOutput(true) 方法,以下所示:
URL url = new URL("http://baidu.com"); URLConnection urlConnection = url.openConnection(); urlConnection.setDoOutput(true);
一旦設置了 setDoOutput(true) ,由於要提交數據,因此須要輸出流。能夠打開 URLConnection 的 OutputStream ,以下所示:
OutputStream output = urlConnection.getOutputStream();
使用此 OutputStream ,能夠在 HTTP 請求的正文中編寫所需的任何數據。 請記住對其進行 URL 編碼(參考 【基礎進階】URL詳解與URL編碼 ,並記得在完成向其寫入數據後關閉 OutputStream 。
URL 類還可用於訪問本地文件系統中的文件。 所以,若是須要代碼處理來源不明的文件,好比是來自網絡仍是本地文件系統,則 URL 類是打開文件的便捷方式。
如下是使用 URL 類在本地文件系統中打開文件的示例:
URL url = new URL("file:/D:/test/test.txt"); URLConnection urlConnection = url.openConnection(); InputStream input = urlConnection.getInputStream(); int data = input.read(); while(data != -1){ System.out.print((char) data); data = input.read(); } input.close();
請注意,這和經過 HTTP 訪問 Web 服務器上的文件的惟一區別是 URL :"file:/D:/test/test.txt"
和 "http://baidu.com"
JarURLConnection 類用於鏈接 Java Jar 文件。 鏈接後能夠獲取有關 Jar 文件內容的信息。 這是一個簡單的例子:
String urlString = "http://butterfly.jenkov.com/" + "container/download/" + "jenkov-butterfly-container-2.9.9-beta.jar"; URL jarUrl = new URL(urlString); JarURLConnection connection = new JarURLConnection(jarUrl); Manifest manifest = connection.getManifest(); JarFile jarFile = connection.getJarFile(); //do something with Jar file...