Java 實現簡單的 Socket 通信


Java socket 封裝了傳輸層的實現細節,開發人員可以基於 socket 實現應用層。本文介紹了 Java socket 簡單用法。

1. 傳輸層協議

傳輸層包含了兩種協議,分別是 TCP (Transmission 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。

2. TCP 示例

TCP 是面向連接的,所以在進行通訊之前發送端(客戶端)需要先連接到接收端(服務端)。客戶端通過 new Socket("localhost", 9090) 來創建一個連接到服務端的套接字,這個套接字連接到主機 localhost 的 9090 端口。

ServerSocket 實現服務端套接字,通過 new ServerSocket(9090) 來創建一個監聽端口為 9090 實例;ServerSocket.accept() 方法會阻塞等待客戶端的連接,一旦有連接過來,會返回一個服務端的 Socket 實例。連接建立完成,客戶端 Socket 實例和服務端 Socket 實例就可以面向輸入輸出流發送數據了。

2.1 示例效果

客戶端程序接收控制台輸入的內容,客戶端控制台每輸入一行,就往服務端發送,服務端接收到消息之后,將消息打印到控制台。

客戶端輸入 "Bye" 時,客戶端斷開與服務端的連接,客戶端程序退出,服務端程序繼續等待連接。

客戶端控制台輸入輸出:

$ java Server.java
Bind Port 9090
New client connected.
Received Message --> Are you OK!

服務端控制台輸出:

$ java Client.java
Are you OK!
Send Msg --> Are you OK!
Bye
$

2.2 服務端程序代碼

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();
        }
    }
}

2.3 客戶端程序代碼

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();
        }
        
    }
    
}

3. UDP 示例

UDP 不需要連接,客戶端與服務端通過發送數據報來完成通信。Java 中使用 java.net.DatagramSocket 來表示 UDP 客戶端或服務端的套接字,使用 java.net.DatagramPacket 來表示 UDP 的數據報。客戶端和服務端可以直接向對方發送數據報,不需要進行連接。

下面代碼基於 UDP 實現了與上面程序同樣的功能。不過消息可能會出錯,某些消息可能也不能到達服務端。

3.1 服務端程序代碼

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();
        }
    }
    
}

3.2 客戶端程序代碼

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 套接字只能夠向指定的主機和端口發送數據報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM