天天看點

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

本文目的是大概了解 Java 網絡程式設計體系,不深入。主要參考 JavaDoc 和 Jakob Jenkov 的英文教程《Java Networking》

本文目的是大概了解 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 Net 網絡程式設計

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

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)
以上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...
           

如果覺得本文有所幫助,歡迎點【推薦】!文章錯誤之處煩請留言。

轉載說明:轉載後必須在文章開頭明顯地給出作者和原文連結;引用必須注明出處;需要二次修改釋出請聯系作者征得同意。