計算機網絡主要功能包括資源共享、資訊傳輸和集中處理、負載均衡和分布式處理、綜合資訊服務等。實際上 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 |
通信協定通常由語義部分、文法部分、變換規則三部分組成。其實所謂的協定就是在資料傳輸基礎上封裝自己的文本内容,先自上而下,後自下而上處理資料頭部:
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 三次握手的流程:
- 第一次握手:建立連接配接時,Client 發送 SYN 封包(seq=x)到 Server,并進入 SYN_SEND 狀态,等待 Server 确認;
- 第二次握手:Server 收到 SYN 封包,必須确認 Client 的 SYN(ack=x+1),同時自己也發送一個 SYN 封包(seq=y),即 SYN + ACK 封包,此時 Server 進入 SYN_RECV 狀态;
- 第三次握手: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 四次揮手的流程:
TCP 連接配接必須經過時間 2MSL 後才真正釋放掉。
- 第一次揮手:Client 發送一個 FIN 封包,用來關閉 Client 到 Server 的資料傳送,Client 進入 FIN_WAIT_1 狀态;
- 第二次揮手:Server 收到 FIN 封包後,發送一個 ACK 封包給 Client,确認序号為收到序号 +1(與 SYN 相同,一個 FIN 占用一個序号),Server 進入 CLOSE_WAIT 狀态;
- 第三次揮手:Server 發送一個 FIN 封包,用來關閉 Server 到 Client 的資料傳送,Server 進入 LAST_ACK 狀态;
- 第四次揮手: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 會話的發送方,任何時候其發送緩存内的資料,都可以分為四類:
- 已經發送并且得到 ACK 回應的;
- 已經發送但沒有得到 ACK 回應的;
- 未發送,但對端允許發送的;
- 未發送且由于達到了滑動視窗的大小,對端不允許發送的;
2、3 這兩部分資料所組成的連續空間就是滑動視窗。
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。
多點廣播示意圖如下:
建立 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 優化。