1. 程式人生 > >初步接觸 Java Net 網路程式設計

初步接觸 Java Net 網路程式設計

本文目的是大概瞭解 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...