Java 實現簡單的 Socket 通訊
Java socket 封裝了傳輸層的實現細節,開發人員可以基於 socket 實現應用層。本文介紹了 Java socket 簡單用法。
傳輸層協議
傳輸層包含了兩種協議,分別是 TCP (Transfer Control Protocol,傳輸控制協議) 和 UDP (User Datagram Protocol,使用者資料報協議)。
TCP 是一種面向連線,可靠的傳輸協議。通訊雙方在“傳送-接收”資料之前需要先建立 TCP 連線。所謂連線,指的是各種裝置、線路,或網路中進行通訊的應用程式為了相互傳遞訊息而建立的專有、虛擬的通訊線路。連線一旦建立,進行通訊的應用程式只使用該虛擬的通訊線路傳送和接收資料。TCP 還需要處理端到端之間的流量控制。
UDP 是一種無連線的,不可靠的傳輸協議。傳送方不需要與接收方建立連線,可以直接傳送資料。
TCP 通過序列號、確認應答、資料校驗等機制確保了傳輸的可靠性,適用於需要可靠資料傳輸的場景,應用層協議 HTTP,FTP 基於 TCP。UDP 沒有複雜的控制機制,不糾錯,不重發,不保證資料的準確性,不確保資料到達目的地;不過 UDP 傳送等量資料花費更小的流量,適用於對時延要求高但對準確性要求不高的場景,如視訊、音訊通訊。
Java 中有 3 種套接字類,java.net.Socket 和 java.net.ServerSocket 基於 TCP,java.net.DatagramSocket 基於 UDP。
TCP 示例
TCP 是面向連線的,所以在進行通訊之前傳送端(客戶端)需要先連線到接收端(服務端)。Java 中使用 java.net.Socket 來表示套接字,客戶端與服務端通過套接字來互發資料。使用 java.net.ServerSocket 來表示服務端套接字。
客戶端通過 1 個 java.net.Socket 來與服務端進行連線,服務端在接收到連線請求之後會建立 1 個 java.net.Socket 來與客戶端進行連線。之後,客戶端和服務端就可以通過兩個 Socket 來收發資料了。
下面分別建立一個服務端程式和客戶端程式,客戶端程式接收控制檯輸入的內容,客戶端控制檯每輸入一行,就往服務端傳送,服務端接收到訊息之後,將訊息列印到控制檯。當客戶端輸入 "Bye" 時,客戶端斷開與服務端的連線,客戶端程式退出,服務端程式繼續等待連線。
服務端程式程式碼
import java.net.*;
import java.io.*;
class Server {
public static void main(String[] args) {
// ServerSocket 實現了 AutoCloseable 介面,所以支援 try-with-resource 語句
// 建立一個 ServerSocket,監聽 9090 埠
try(ServerSocket serv = new ServerSocket(9090)){
System.out.printf("Bind Port %d\n", serv.getLocalPort());
Socket socket = null;
while(true){
// 接收連線,如果沒有連線,accept() 方法會阻塞
socket = serv.accept();
// 獲取輸入流,並使用 BufferedInputStream 和 InputStreamReader 裝飾,方便以字元流的形式處理,方便一行行讀取內容
try(BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream()) )){
String msg = null;
char[] cbuf = new char[1024];
int len = 0;
while( (len = in.read(cbuf, 0, 1024)) != -1 ){ // 迴圈讀取輸入流中的內容
msg = new String(cbuf, 0, len);
if("Bye".equals(msg)) { // 如果檢測到 "Bye" ,則跳出迴圈,不再讀取輸入流中內容。
break;
}
System.out.printf("Received Message --> %s \n", msg);
}
}catch (IOException e){
e.printStackTrace();
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
客戶端程式程式碼
import java.net.*;
import java.io.*;
import java.util.*;
class Client{
public static void main(String[] args){
try(Socket socket = new Socket("localhost", 9090)){
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
Scanner scanner = new Scanner(System.in);
scanner.useDelimiter("\r\n");
String msg = null;
while( !(msg = scanner.next()).equals("Bye") ){
System.out.printf("Send Msg --> %s \n", msg);
out.write(msg);
out.flush(); // 立即傳送,否則需要積累到一定大小才一次性發送
}
}catch (IOException e){
e.printStackTrace();
}
}
}
UDP 示例
UDP 不需要連線,客戶端與服務端通過傳送資料報來完成通訊。Java 中使用 java.net.DatagramSocket 來表示 UDP 客戶端和服務端的套接字,使用 java.net.DatagramPacket 來表示 UDP 的資料報。
下面基於 UDP 來實現與上面程式同樣的功能,不過訊息可能會出錯,某些訊息可能也不能到達服務端。
服務端程式程式碼
import java.net.*;
import java.io.*;
class Server {
public static void main(String[] args){
// 建立一個 DatagramPacket 例項,用來接收客戶端傳送過來的 UDP 資料報,這個例項可以重複利用。
byte[] buf = new byte[8192]; // 快取區
int len = buf.length; // 要利用的快取區的大小
DatagramPacket pac = new DatagramPacket(buf, len);
// 建立服務端的套接字,需要指定繫結的埠號
try(DatagramSocket serv = new DatagramSocket(9191)){
while(true){
serv.receive(pac); // 接收資料報。如果沒有資料報傳送過去,會阻塞
System.out.println("Message --> " + new String(pac.getData(), 0, pac.getLength()));
}
}catch (IOException e){
e.printStackTrace();
}
}
}
客戶端程式程式碼
import java.io.*;
import java.net.*;
import java.util.*;
class Client {
public static void main(String[] args){
// 建立一個客戶端的 UDP 套接字,不需要指定任何資訊
try(DatagramSocket client = new DatagramSocket()){
// 建立一個數據報例項,資料和長度在傳送之前都會重新設定,所以這裡直接置為 0 即可。
// 由於是傳送端,所以需要設定服務端的地址和埠
DatagramPacket pac = new DatagramPacket(new byte[0], 0, InetAddress.getByName("localhost"), 9191);
// 掃描控制檯輸入
Scanner scanner = new Scanner(System.in);
scanner.useDelimiter("\r\n");
String msg = null;
while( !(msg = scanner.next()).equals("Bye") ){
// 設定要傳送的資料
pac.setData(msg.getBytes());
// 傳送資料報
client.send(pac);
System.out.println("Sent Message --> " + msg);
}
}catch (IOException e){
e.printStackTrace();
}
}
}
需要注意的是,UDP 是面向無連線的,但 DatagramSocket 的 API 中提供了 connect 相關的方法,這裡的 connect 並非 TCP 中連線的意思。而是指定了當前的 UDP 套接字只能夠向指定的主機和埠傳送資料報。