1. 程式人生 > >初步接觸 Java Net 網絡編程

初步接觸 Java Net 網絡編程

同時 resolve tor 作用 meta 都沒有 底層 想象 啟動

本文目的是大概了解 Java 網絡編程體系,需要一點點 Java IO 基礎,推薦教程 系統學習 Java IO。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程《Java Networking》 http://tutorials.jenkov.com/java-networking/index.html

Java 網絡編程概覽

Java 有一個相當容易使用的內置網絡 API,可以很容易地通過互聯網上的 TCP / IP 套接字或 UDP 套接字進行通信。 TCP 通常比 UDP 使用得更頻繁。

即使 Java Networking API 允許通過套接字打開和關閉網絡連接,但所有通信都通過 Java IO 類 InputStream 和 OutputStream 實現的。
或者,我們可以使用 Java NIO API 中的網絡類。 用法類似於 Java Networking API 中的類,但 Java NIO API 可以在非阻塞模式下工作。 在某些情況下,非阻塞模式可提升性能。

Java TCP 網絡基礎

通常,客戶端會打開與服務器的 TCP / IP 連接,然後開始與服務器通信,當通信結束後客戶端關閉連接。如下圖:
技術分享圖片

客戶端可以通過一個已打開的連接發送多個請求,實際上,客戶端可以向服務器發送盡可能多的數據。 當然,如果需要,服務器也可以關閉連接。

Java 中 Socket 類和 ServerSocket 類

當客戶端想要打開到服務器的 TCP / IP 連接時,它使用 Java Socket 類來實現。 套接字被告知連接到哪個 IP 地址和 TCP 端口,其余部分由 Java 完成。

如果要啟動服務器以偵聽來自某個 TCP 端口上的客戶端的傳入連接,則必須使用 Java ServerSocket 類。 當客戶端通過客戶端套接字連接到服務器的 ServerSocket 時,服務器上會為該連接分配一個 Socket 。 客戶端和服務器的通信就是 Socket 到 Socket 的通信了。

Socket和ServerSocket在後面的文本中有更詳細的介紹。

Java UDP 網絡基礎

UDP 的工作方式與 TCP 略有不同。 使用 UDP ,客戶端和服務器之間沒有連接。 客戶端可以向服務器發送數據,並且服務器可以(或可以不)接收該數據。 客戶端永遠不會知道數據是否在另一端收到。 從服務器到客戶端發送的數據也是如此。
由於無法保證數據傳輸,因此 UDP 協議的協議開銷較小。

在一些情況下,無連接 UDP 模型優於 TCP ,比如傳輸視頻等多媒體文件,缺少一些數據是不影響觀看的。

TCP Socket(套接字)

為了通過 Internet 連接到服務器(通過TCP / IP),需要創建一個 Socket 並將其連接到服務器。 或者,如果您更喜歡使用 Java NIO ,則可以使用 Java NIO SocketChannel 。

創建一個Socket
Socket socket = new Socket("baidu.com", 80);

第一個參數是地址,可以是 ip 或者域名字符串,第二個參數是端口,端口80是Web服務器端口。

寫入 Socket

要寫入 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 讀取

要從 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

可以使用 ServerSocket 來實現 Java 服務器,這樣就可以通過 TCP / IP 偵聽來自客戶端的傳入連接。如果更喜歡使用 Java NIO 而不是 Java Networking(標準API),那麽也可以使用 ServerSocketChannel 。

創建一個 ServerSocket

這是一個簡單的代碼示例,它創建一個偵聽端口 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 多線程服務器。

關閉客戶端 Sockets

一旦客戶端請求完成,並且不會從該客戶端收到進一步的請求,必須關閉該Socket,就像關閉普通客戶端Socket一樣。調用:socket.close();

關閉服務端 Sockets

一旦服務器關閉,就需要關閉 ServerSocket 。 調用:serverSocket.close();

UDP DatagramSocket(UDP數據報套接字)

DatagramSocket 是 Java 通過 UDP 而不是 TCP 進行網絡通信的機制。 UDP 也是 IP 協議的上層。 可以使用 DatagramSocket 來發送和接收 UPD 數據報。

UDP 對比 TCP

通過 TCP 發送數據時,首先要創建連接。 建立 TCP 連接後,TCP 保證數據到達另一端,或者它會告訴你發生了錯誤。

使用 UDP,只需將數據包(數據報)發送到網絡上的某個 IP 地址。 無法保證數據會到達,也無法保證 UDP 數據包到達的順序。 這意味著 UDP 比 TCP 具有更少的協議開銷(沒有流完整性檢查)。

UDP 適用於數據傳輸,如果數據包在轉換過程中丟失則無關緊要。 例如,想象一下通過互聯網傳輸直播電視信號,如果一兩幀丟失,這是無關緊要的。我們更不希望直播延遲只是為了確保所有幀都顯示出來。 寧願跳過錯過的幀,並直接查看最新的幀。

還有實時監控視頻,寧願丟失一兩幀,也不想延遲於現實 30 秒。與攝像機錄像的存儲有點不同,將圖像從相機錄制到磁盤時, 為了保證完整性,可能不希望丟失單幀,而是更願意稍微延遲。

DatagramPacket 類

此類表示數據報包。數據報包用來實現無連接包投遞服務。

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

其中

  • InetAddress 類表示互聯網協議 (IP) 地址,可以通過靜態方法 getByName(String host) 獲得其對象。
  • SocketAddress 類裏面什麽都沒有。其子類 InetSocketAddress是(IP地址+端口號)類型,也就是端口地址類型,同樣可以使用靜態方法 createUnresolved(String host, int port) 獲取對象,另外也能由構造函數 InetSocketAddress(InetAddress addr, int port) 創建,其中 InetAddress 對象可省略,也可用字符串代替。
通過 DatagramSocket 發送數據(DatagramPacket )

要通過 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 接收數據是通過首先創建 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 服務器對傳入連接一樣。

URL + URLConnection

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>
HTTP GET 和 POST

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 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

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...

初步接觸 Java Net 網絡編程