一、網絡編程套接字
1.1 基礎概念
【網絡編程】指網絡上的主機,通過不同的進程,以編程的方式實現網絡通信;當然,我們只要滿足進程不同就行,所以即便是同一個主機,只要是不同進程,基于網絡來傳輸數據,也屬于網絡編程
【套接字】其實是socket的直譯,套接字就是傳輸層給應用層提供的網絡編程API(接口)通過這個接口,應用層程序可以通過這個接口使用傳輸層提供的服務,而不需要知道它的具體實現
套接字分為兩類:流式套接字和數據報套接字
流式套接字是基于TCP協議(一個傳輸層協議)實現的,TCP是一種面向連接型、可靠傳輸型、面向字節流、全雙工的傳輸層協議,流式套接字利用這些特性為應用層提供了一個簡單的接口,用于發送和接收數據流
數據報套接字是基于UDP協議(也是一個傳輸層協議)實現的,UDP是一種面向無連接型、不可靠傳輸型、面向數據報、全雙工的傳輸層協議
1.2 協議特點
接下來講解以下上述提到的 TCP協議和UDP協議的特點
1)面向有連接型 vs 面向無連接型:通過網絡發送數據分為面向有連接和面向無連接
有連接指在發送數據之前,發送端要先和接收端建立一條邏輯意義上的連接,連接建好后才能真正發送數據,數據發送完畢后要斷開連接;
就好比打電話,在說話之前,對方要先同意接聽,接聽并說完話后再掛斷電話
無連接則無需考慮建立連接和斷開連接,發送端可以在任何時候發送數據,接收端不知道自己會在何時收到數據,所以要時常檢查是否收到數據;
這個就像發送電子郵件,發送端可以隨時發送,無需讓接收端同意,接收端則要時常檢查是否有收到郵件
2)可靠傳輸 vs 不可靠傳輸:
可靠傳輸指將要傳輸的數據盡可能的傳輸給對方,在網絡通信的過程中,會存在"丟包"的情況:A給B傳輸10個數據報,B收到了9個;
原因是A傳輸給B,中間可能會經歷很多交換機和路由器,這些交換機和路由器不只是轉發你的數據,要轉發很多數據,當數據很多時,可能會超過它們自身的硬件水平,此時多出來的數據無法轉發,會被直接丟棄掉。
TCP為了對抗丟包,內部實現了一些機制(重發)來實現可靠傳輸(機制后面會詳細講)
不可靠傳輸指再出現丟包后,也不負責重發,不可靠傳輸更注重效率,在一些注重效率,對準確性要求不高的場景使用不可靠傳輸,可靠傳輸能盡可能保證數據傳給接收端,但效率上會大打折扣
3)面向字節流 vs 面向數據報
面向字節流指傳輸的數據以字節為單位
面向數據報指傳輸的數據以數據報為單位,傳輸數據是一個一個數據報,一次讀寫只能讀寫完整的數據報,不能讀寫半個
4)全雙工 vs 半雙工
全雙工指一條鏈路,能夠進行雙向通信,后續代碼創建socket對象,既可以讀(接收)也可以寫(發送)
半雙工指一條鏈路,只能進行單向通信
二、UDP-數據報套接字編程
socket API 是由傳輸層給應用層提供的API,傳輸層是封裝于操作系統內核態的,由操作系統內核直接管理,所以可以理解為socket api是由操作系統內核管理的,而Java對于系統這些API進行了封裝,所以使得用戶程序可以直接使用這些API
UDP的socket API 有兩個重要的類
2.1 DatagramSocket
屬于UDP Socket,創建DatagramSocket的對象就可以發送和接收UDP數據報,先來看構造方法:
構造方法 | 描述 |
DatagramSocket( ) | 創建一個UDP數據報套接字的Socket,綁定到本機任意一個隨機端口 |
DatagramSocket( int port ) | 創建一個UDP數據報套接字的Socket,綁定到本機指定的端口號 |
普通方法:
普通方法 | 描述 |
void receive (DatagramPacket p) | 接收數據報,如果沒有接收到,該方法就會阻塞等待 |
void send(DatagramPacket p) | 發送數據報,不會阻塞等待,直接發送(無連接) |
void close( ) | 關閉此數據報套接字 |
當創建一個套接字時,系統會為其分配資源綁定端口號,如果用完不關閉則會導致資源持續被占用
2.2 DatagramPacket
表示UDP Socket發送和接收的數據報,一個DatagramPacket對象就相當于一個UDP數據報
構造方法 | 描述 |
DatagramPacket (byte[] buf, int length) | 構造?個DatagramPacket以用來接收數據報,接收的數據保存在字節數組(第?個參數buf)中,接收指定長度(第?個參數length) |
DatagramPacket(byte[] buf, int offset, int length,? SocketAddress address) | 構造?個DatagramPacket以用來發送數據報,發送的數據為字節數組(第?個參數buf)中,從0到指定? 度(第?個參數length)address指定?的主機的IP 和端?號 |
上述方法可以結合下述代碼理解
2.3 模擬回顯服務器
回顯服務器指客戶端發送一個請求給服務端,服務端將這個請求原封不動的作為相應返回給客戶端,這就叫回顯(請求啥相應就是啥),接下來先編寫服務器程序:
public class UdpEchoServer {public DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port); //創建 UDP Socket 并綁定一個端口號}}
服務器需要在程序啟動的時候,把在服務器程序的端口號確定下來,客戶端發送請求時需要知道服務器的IP地址(服務器所在主機的IP)、端口號port
服務器要能夠接收客戶端發送的數據,socket? receive( );需要向receive傳入一個UDP數據報
public class UdpEchoServer {public DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {//1) 接收請求DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//此時創建好的requestPacket 是一個空的數據包//requestPacket包含兩個部分1.報頭 2.載荷//字節數組用來存儲數據socket.receive(requestPacket); //客戶端會send一個數據包, 就會跳轉到這里//此時由requestPacket 是一個預留好空間的空數據包// 為了方便在 java 代碼中處理 (尤其是后面進行打印) 可以把上述數據報中的二進制數據, 拿出來, 構造成 StringString request = new String(requestPacket.getData(), 0, requestPacket.getLength());}}
接收來自客戶端的請求后,經過處理后將響應返回給客戶端,那么該如何知道應該給哪個客戶端返回響應,在我們receive接收到的數據包里就包含了這個數據包來自于哪個IP,來自于哪個端口號(客戶端)
// 2) 根據請求計算響應
String response = this.process(request);
// 3) 把響應寫回到客戶端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
requestPacket.getSocketAddress() 這個方法返回的對象里就包含了客戶端的IP地址和端口號
上述代碼干的事情就是將字符串類型的二進制數據再構造會UDP數據包并發送給客戶端
服務端完整代碼如下:
public class UdpEchoServer {private DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {Scanner scanner = new Scanner(System.in);System.out.println("服務器啟動!");while (true) { // 服務器需要7*24小時持續接收并處理請求// 1) 讀取請求并解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(requestPacket);//receive方法中的requestPacket是一個空的數據包,客戶端程序通過send方法發送有數據的數據包后//會直接跳轉到這里的receive方法,而這里的requestPacket是一個預留好空間的空數據包// 為了方便在 java 代碼中處理 (尤其是后面進行打印) 可以把上述數據報中的二進制數據, 拿出來, 構造成 StringString request = new String(requestPacket.getData(), 0, requestPacket.getLength());//將字節數組構造成String類的對象// 2) 根據請求計算響應String response = this.process(request);// 3) 把響應寫回到客戶端DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress(), requestPacket.getPort(),request, response); // 從左到右依次為: IP地址,端口號,請求,響應}}// 由于當前寫的是 "回顯服務器"public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}
接下來寫客戶端代碼:
首先客戶端需要知道服務器的IP和端口號,端口號是我們之前就設置的9090,IP用127.0.0.1,當服務器和客戶端在一個主機上,就用環回IP,這是系統提供的特殊的IP
public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;public UdpEchoClient(String serverIp, int serverPort) throws SocketException {socket = new DatagramSocket();// 這倆信息需要額外記錄下來, 以備后續使用.this.serverIp = serverIp;this.serverPort = serverPort;}
}
上述構造socket對象沒有指定端口號,這樣操作系統會分配一個空閑的端口號,這個端口號每次重新啟動程序都不一樣
// 1. 從控制臺讀取用戶輸入
String request = scanner.next();
// 2. 構造請求并發送
// 構造請求數據報的時候, 不光要有數據, 還要有 "目標 "
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket); //發送數據包
上述InetAddress.getByName(serverIp)是將字符串格式的IP地址轉成Java能識別的InetAddress對象
發送完數據包,服務器經過處理返回響應,客戶端就要接收響應
// 3. 讀取響應數據
//構造一個空的數據包負責接收服務器返回的響應
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);
// 4. 顯示響應到控制臺上.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
在第2步執行完send后,客戶端程序緊接著到第三步的receive,由于從發送請求到返回響應需要些時間,所以這里receive會阻塞,阻塞到接收到服務器返回響應
完整的客戶端代碼如下:
public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;public UdpEchoClient(String serverIp, int serverPort) throws SocketException {socket = new DatagramSocket();// 這倆信息需要額外記錄下來, 以備后續使用.this.serverIp = serverIp;this.serverPort = serverPort;}public void start() throws IOException {System.out.println("客戶端啟動!");Scanner scanner = new Scanner(System.in);while (true) {System.out.print("請輸入要發送的請求: ");// 1. 從控制臺讀取用戶輸入String request = scanner.next();// 2. 構造請求并發送// 構造請求數據報的時候, 不光要有數據, 還要有 "目標"DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length,InetAddress.getByName(serverIp), serverPort);socket.send(requestPacket);// 3. 讀取響應數據DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);// 4. 顯示響應到控制臺上.String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);// UdpEchoClient client = new UdpEchoClient("139.155.74.81", 9090);client.start();}
}
接下來啟動服務器程序和客戶端程序:
客戶端可以不斷發送請求并得到響應
服務端會不斷處理客戶端的請求