天天看點

Java Language——網絡程式設計1.Java網絡API

計算機網絡主要功能包括資源共享、資訊傳輸和集中處理、負載均衡和分布式處理、綜合資訊服務等。實際上 Java 的網絡程式設計就是伺服器通過 ServerSocket 建立監聽,用戶端通過 Socket 連接配接到指定伺服器後,通信雙方就可以通過 IO 流進行通信了。

OSI 七層模型:

OSI 七層模型 TCP/IP概念層模型 功能 包含協定
應用層 應用層 檔案傳輸、電子郵件、檔案服務、虛拟終端 HTTP、SNMP、FTP、TFTP、SMTP、DNS、Telnet
表示層 資料格式化、代碼轉換、資料加密 沒有協定
會話層 解除或建立與别的接點的聯系 沒有協定
傳輸層 傳輸層 提供端對端的接口 TCP(傳輸控制協定, 安全度高)、UDP(效率快, 安全度低, 可能會有資料丢失)、SSL、TLS
網絡層 網絡層 為資料包選擇路由 IP、ICMP、RIP、OSPF、BGP、IGMP
資料鍊路層 鍊路層 傳輸有位址的幀以及錯誤檢測功能 SLIP、CSLIP、PPP、ARP、RARP、MTU
實體層 以二進制資料形式在實體媒體上傳輸資料 ISO2110、IEEE802、IEEE802.2

通信協定通常由語義部分、文法部分、變換規則三部分組成。其實所謂的協定就是在資料傳輸基礎上封裝自己的文本内容,先自上而下,後自下而上處理資料頭部:

Java Language——網絡程式設計1.Java網絡API

IP位址:32位整數(4個8位二進制數),NIC 統一負責全球 IP 位址的規劃、管理,而 Intel NIC、APNIC、RIPE 三大網絡資訊中心具體負責美國及其他地區的 IP 位址配置設定,APNIC(總部在日本東京大學)負責亞太地區的 IP 管理,我國申請 IP 位址也要通過 APNIC。IP 位址被分為 A、B、C、D、E 五類。

A類:10.0.0.0-10.255.255.255
B類:172.16.0.0-172.31.255.255
C類:192.168.0.0-192.168.255.255
端口号:16位整數,0-65535
公認端口:0-1023
注冊端口:1024-49151
動态和私有端口:49152-65535
           

1.Java網絡API

Java 提供了四大網絡通信相關的類 :

  • InetAddress:用于辨別網絡上的硬體資源,表示 IP 位址;
  • URL:統一資源定位符,格式為 協定名稱和資源名稱,中間用冒号隔開);
  • Sockets:使用 TCP 協定實作的網絡通信的 Socket 相關的類);
  • Datagram:使用 UDP 協定,将資料儲存在資料報中,通過網絡進行通信)。

java.net 包下 URL 和 URLConnection 等類提供了以程式設計方式通路 web 服務的功能,URLDecoder 和 URLEncoder 提供了普通字元串和 application/x-www-form-urlencoded MIME 字元串互相轉換的靜态方法。

1.InetAddress

Java 提供了 InetAddress 類表示 IP 位址:

InetAddress ip = InetAddress.getByName("www.baidu.com"); // 根據主機名來擷取對應的InetAddress執行個體
boolean b = ip.isReachable(1000); // 判斷是否可達
String address = ip.getHostAddress(); // 擷取該InetAddress執行個體的IP字元串

InetAddress local = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); //根據原始IP位址來擷取對應的InetAddress執行個體
boolean b1 = ip.isReachable(1000); // 判斷是否可達
String hostName = local.getCanonicalHostName(); // 擷取該InetAddress執行個體對應的全限定域名
           

2.URL編碼

URLDecoder 和 URLEncoder 提供了 URL 編碼解碼的功能,用于普通字元串和 application/x-www-form-urlencoded MIME 字元串之間的互相轉換:

// URL編碼
String s = URLDecoder.decode("%E5%8C%97%E4%BA%AC", "UTF-8");

// URL解碼
String s1 = URLEncoder.encode("北京" , "UTF-8");
           

3、URLConnection

URLConnection 指應用程式與 URL 之間的通信連接配接,HttpURLConnection 指 URL 與 URL 之間的 HTTP 連接配接,程式可以通過 URLConnection 執行個體向 URL 發送請求、讀取 UR 引用的資源。

4.TCP協定

TCP 協定(傳輸控制協定)是面向連接配接的、可靠的、有序的、重量級的、基于位元組流的傳輸層通信協定,TCP 将應用層的資料流分割成封包段并發送給目标節點的 TCP 層。

TCP 為了保證不丢失包,所有資料包都有序号,對方收到則發送 ACK 确認,未收到則重傳。TCP 還會使用校驗和來檢驗資料在傳輸過程中是否有誤。

1.TCP的三次握手

“握手” 是為了建立連接配接,三次握手的過程由用戶端進行觸發,TCP 三次握手的流程:

Java Language——網絡程式設計1.Java網絡API
  1. 第一次握手:建立連接配接時,Client 發送 SYN 封包(seq=x)到 Server,并進入 SYN_SEND 狀态,等待 Server 确認;
  2. 第二次握手:Server 收到 SYN 封包,必須确認 Client 的 SYN(ack=x+1),同時自己也發送一個 SYN 封包(seq=y),即 SYN + ACK 封包,此時 Server 進入 SYN_RECV 狀态;
  3. 第三次握手:Client 收到 Server 的 SYN + ACK 封包,向 Server 發送确認封包 ACK(ack=y+1),此包發送完畢,Client 和 Server 進入 ESTAB_LISHED 狀态,完成三次握手。

常見的問題:

1、為什麼需要三次握手才能建立起連接配接?

為了初始化 Sequence Number 的初始值,通信雙方需要通知對方自己的 Sequence Number,也就是圖中的 x 和 y,這個号會作為以後資料通信的序号,以保證應用層接收到的資料不會因為網絡的問題而亂序,TCP 會用這個序号來拼接資料,是以在伺服器回發它的 Sequence Number 及第二次握手之後,用戶端還需要發送确認封包給服務端,告知服務端用戶端已經收到服務端 Sequence Number 了。

2、首次握手 SYN 逾時?

服務端收到用戶端的 SYN,回複 SYN-ACK 的時候未收到 ACK 确認,服務端就會不斷重試直至逾時,Linux 預設等待 63 秒才斷開連接配接。

3、建立連接配接後,用戶端出現故障怎麼辦?

TCP 有保活機制,在一段時間,連接配接處于非活動狀态,開啟保活功能的一端将向對方發送保活探測封包,如果未收到響應則繼續發送,嘗試次數達到保活探測數仍未收到響應則中斷連接配接。

2.TCP的四次揮手

“揮手” 是為了斷開連接配接,四次揮手的過程由用戶端或服務端執行 close 進行觸發,這裡我們假設由用戶端主動觸發 close,TCP 四次揮手的流程:

Java Language——網絡程式設計1.Java網絡API

TCP 連接配接必須經過時間 2MSL 後才真正釋放掉。

  1. 第一次揮手:Client 發送一個 FIN 封包,用來關閉 Client 到 Server 的資料傳送,Client 進入 FIN_WAIT_1 狀态;
  2. 第二次揮手:Server 收到 FIN 封包後,發送一個 ACK 封包給 Client,确認序号為收到序号 +1(與 SYN 相同,一個 FIN 占用一個序号),Server 進入 CLOSE_WAIT 狀态;
  3. 第三次揮手:Server 發送一個 FIN 封包,用來關閉 Server 到 Client 的資料傳送,Server 進入 LAST_ACK 狀态;
  4. 第四次揮手:Client 收到 FIN 封包後,Client 進入 TIME_WAIT 狀态,接着發送一個 ACK 封包給 Server,确認序号為收到序号 +1,Server 進入 CLOSED 狀态,完成四次揮手。

常見的問題:

1、為什麼需要四次揮手才能斷開連接配接?

因為 TPC 是全雙工通信,發送方和接收方都需要 FIN 封包和 ACK 封包,發送方和接收方各自需要兩次揮手即可,隻不過有一方是被動的。

2、為什麼會有 TIME_WAIT 狀态?

確定有足夠的時間讓對方收到 ACK 封包;避免新舊連接配接混淆。

3、伺服器出現大量 CLOSE_WAIT 狀态的原因?

對方關閉 socket 連接配接後,我方忙于讀或寫,沒有及時關閉連接配接。多數情況是程式裡有 bug,需要檢查代碼,特别是釋放資源的代碼;檢查配置,特别是處理請求的線程配置

3.TCP的滑動視窗

RTT:發送一個資料包到收到對應的 ACK 所花費的時間。

RTO:重傳時間間隔。

TCP 使用滑動視窗做流量控制和亂序重排。滑動視窗保證了 TCP 的可靠性和流控特性。

對于 TCP 會話的發送方,任何時候其發送緩存内的資料,都可以分為四類:

  1. 已經發送并且得到 ACK 回應的;
  2. 已經發送但沒有得到 ACK 回應的;
  3. 未發送,但對端允許發送的;
  4. 未發送且由于達到了滑動視窗的大小,對端不允許發送的;

2、3 這兩部分資料所組成的連續空間就是滑動視窗。

Java Language——網絡程式設計1.Java網絡API

5.TCP通信

Socket 是 Java 裡的 TCP/IP 實作。TCP/IP 協定是一種可靠協定,它在通信兩端各建立一個 Socket,進而在通信兩端之間形成網絡虛拟鍊路進行通信,Java 使用 Socket 對象來代表兩端的通信接口,并通過 Socket 産生 IO 流來進行網絡通信。

基于 TCP 協定實作網絡通信的類包含用戶端的 Socket 類(實作了 TCP/IP 協定,可以連接配接到服務端收發資料),服務端的 ServerSocket 類。

IP 位址 + 端口就組成了所謂的 Socket,Socket 是網絡上運作的程式之間雙向通信鍊路的終結點,是 TCP 和 UDP 的基礎。

1、單用戶端與單服務端的通信

服務端開啟線程,啟動 Socket 服務監聽,等待用戶端連接配接,連接配接成功讀取資料:

ServerSocket serverSocket = new ServerSocket(10000); // 建立ServerSocket
Socket socket = serverSocket.accept();               // 開始監聽端口,等待用戶端連接配接,若連接配接上則繼續往下執行
DataInputStream reader = new DataInputStream(socket.getInputStream()); // 擷取資料輸入流
String msg = reader.readUTF();                       // 讀一個UTF-8的資訊
socket.shutdownInput();                              // 關閉輸入流
System.out.println(msg);                             // 輸出到控制台

DataOutputStream writer = new DataOutputStream(socket.getOutputStream()); // 擷取資料輸出流
writer.writeUTF("服務端響應消息: 呵呵..");                            // 寫一個UTF-8的資訊
socket.shutdownOutput();                             // 關閉輸出流

// 關閉相關資源
// 對于同一個socket,如果關閉了輸出/輸入流,則與該輸出流關聯的socket也會被關閉,是以一般不用關閉流,直接關閉socket即可。
socket.close();
serverSocket.close();                                // 關閉socket也會關閉流
           

用戶端需要在子線程中建立服務端連接配接并發送消息:

Socket socket = new Socket("localhost", 10000);      // 建立用戶端Socket
DataOutputStream writer = new DataOutputStream(socket.getOutputStream()); // 擷取資料輸出流
writer.writeUTF("用戶端發送消息: 嘿嘿..");                            // 寫一個UTF-8的資訊
socket.shutdownOutput();                             // 關閉輸出流

DataInputStream reader = new DataInputStream(socket.getInputStream()); // 擷取資料輸入流
String msg = reader.readUTF();                       // 讀一個UTF-8的資訊
socket.shutdownInput();                              // 關閉輸入流
System.out.println(msg);                             // 輸出到控制台

socket.close();                                      // 關閉socket也會關閉流
           

最終就可以在服務端和用戶端的控制台上分别看到 “用戶端發送消息: 嘿嘿…” 與 “服務端響應消息: 呵呵…” 資訊了。

2、多用戶端與單服務端的通信

實作多用戶端通信則需要在服務端建立 ServerSocket,循環調用 accept() 等待用戶端連接配接。首先建立服務端線程處理類:

public class ServerThread extends Thread {
    Socket socket = null; // 和線程相關的Socket
    DataInputStream reader;
    DataOutputStream writer;
    public ServerThread(Socket socket) {
        this.socket = socket;
    }
    // 線程執行的操作,響應用戶端的請求
    @Override
    public void run() {
        try {
            DataInputStream reader = new DataInputStream(socket.getInputStream());// 擷取資料輸入流
            String msg = reader.readUTF();            // 讀一個UTF-8的資訊
            socket.shutdownInput();                   // 關閉輸入流
            System.out.println(msg);                  // 輸出到控制台

            writer = new DataOutputStream(socket.getOutputStream());// 擷取資料輸出流
            writer.writeUTF("服務端響應消息: 呵呵..");   // 寫一個UTF-8的資訊
            socket.shutdownOutput();                  // 關閉輸出流
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (socket!=null)
                    socket.close();                   // 關閉socket也會關閉流
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           

服務端代碼修改為:

ServerSocket serverSocket = new ServerSocket(10000); // 建立ServerSocket
Socket socket = null;
// 循環監聽等待用戶端連接配接
while (true) {
    socket = serverSocket.accept();                  // 開始監聽端口,等待用戶端連接配接,若連接配接上則繼續往下執行
    ServerThread serverThread = new ServerThread(socket);
    // 未設定優先級可能會導緻運作時速度非常慢,可降低優先級。
    serverThread.setPriority(4);                     // 設定線程優先級,範圍[1,10],預設5
    serverThread.start();                            // 啟動線程
}
           

用戶端代碼不變。最終就可以運作多個用戶端來進行通信了。

另外在實際應用中,更多的是傳遞對象,傳遞對象可以使用 ObjectOutputStream 對象序列化流,傳遞對象:

Socket socket = new Socket("localhost", 10000);      // 建立用戶端Socket
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(object);                             // 寫一個對象,序列化流
socket.shutdownOutput();                             // 關閉輸出流
socket.close();                                      // 關閉socket也會關閉流
           

6.UDP協定

UDP 協定(使用者資料報協定)是無連接配接的、不可靠的、無序的、面向封包的、輕量級的,UDP 協定以資料報作為資料傳輸的載體,UDP 資料包報頭隻有 8 個位元組,相比于 TCP 資料包報頭有 20 個位元組,額外開銷較小。

UDP 速度要比 TCP 快,多用于線上視訊媒體、電視廣播、多人線上遊戲等。

7.UDP通信

進行資料傳輸時,首先需要将要傳輸的資料定義成資料報(Datagram),在資料報中指明資料所要達到的 Socket(主機位址和端口号),然後再将資料報發送出去。基于 UDP 協定實作網絡通信的類包含 DatagramPacket 類(表示資料報包,UDP 通信中的資料單元)和 DatagramSocket 類(進行端到端通信的類)。

1、使用 DatagramSocket 發送、接受資料

服務端代碼:

DatagramSocket socket = new DatagramSocket(10000); // 建立服務端DatagramSocket
byte[] data = new byte[1024];                      // 建立位元組數組
DatagramPacket packet = new DatagramPacket(data, data.length); // 建立資料報,用于接收用戶端發送的資料
socket.receive(packet);                            // 接收用戶端發送的資料,接收到資料報才繼續執行(會阻塞)

String info = new String(data, 0, packet.getLength());  // 讀取資料
System.out.println("服務端接收到資訊: "+info);

// 向用戶端響應資料
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 ="歡迎您!".getBytes();
DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port); // 建立資料報,包含響應的資料
socket.send(packet2);                              // 響應用戶端
socket.close();                                    // 關閉資源
           

用戶端代碼:

// 定義伺服器位址、端口号、資料
InetAddress address = InetAddress.getByName("localhost");
int port = 10000;
byte[] data ="嘿嘿..".getBytes();

DatagramPacket packet = new DatagramPacket(data, data.length, address, port); // 建立資料報,包含發送的資料
DatagramSocket socket = new DatagramSocket();      // 建立用戶端DatagramSocket
socket.send(packet);                               // 向服務端發送資料

// 接受服務端響應的資料
byte[] data2 = new byte[1024];                     // 建立位元組數組
DatagramPacket packet2 = new DatagramPacket(data2, data2.length); // 建立資料報,用于接收用戶端發送的資料
socket.receive(packet2);                           // 接收服務端響應資料,接收到資料報才繼續執行(會阻塞)

String info2 = new String(data, 0, packet.getLength());  // 讀取資料
System.out.println("服務端響應資訊: "+info2);
           

最終就可以在服務端和用戶端的控制台上看到相應的資訊了。實作 UDP 多用戶端單服務端通信可以參考 TPC 開啟多線程的方式,這裡不再贅述。

2、使用 MulticastSocket 實作多點廣播

DatagramSocket 隻允許資料報發送到指定的目标位址,而 MulticastSocket 可以将資料報以廣播方式發送到多個用戶端。MulticastSocket 繼承于 DatagramSocket。

多點廣播示意圖如下:

Java Language——網絡程式設計1.Java網絡API

建立 MulticastSocket 對象後還需要加入到指定的多點廣播位址,MulticastSocket 使用 joinGroup(InetAddress multicastAddr) 方法加入到指定組,使用 leaveGroup(InetAddress multicastAddr) 方法脫離一個組。

MulticastSocket 用于發送、接受資料報的方法和 DatagramSocket 完全一樣。但 MulticastSocket 多了一個 setTimeToLive(int ttl) 方法,ttl 參數用于設定資料報最多可以跨過多少個網絡(0:停留在本地主機;1:本地區域網路;32:隻能發送到本站點的網絡上;64:保留在本地區;128:保留在本大洲;255:所有地方),預設1。

8.代理伺服器

從 Java5 開始,java.net 包下提供了 Proxy(表示代理伺服器)、ProxySelector(表示代理選擇器)兩個類。

常見問題

1、http 和 https 的差別?

https 即安全超文本傳輸協定,https 在傳輸層增加了 SSL 層,SSL 采用身份認證和資料加密保證網絡通信的安全和資料的完整性。

  • https 需要到 CA 申請證書,而 http 不需要;
  • https 是密文傳輸,而 http 是明文傳輸;
  • 連接配接方式不同,https 預設使用 443 端口,http 使用 80 端口;
  • https = http + 加密 + 認證 + 完整性保護,較 http 安全。

2、https 真的安全嗎?

不一定,浏覽器預設填充 http://,請求需要進行轉發到 https 的端口,有被劫持的風險。這一點可以使用 HSTS 優化。