iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
自我挑戰組

我的Java自學之路:一個轉職者的30篇技術統整系列 第 29

Java 網路程式設計:Socket 程式設計基礎指南

  • 分享至 

  • xImage
  •  

什麼是 Socket?

Socket(插座)是網路通訊的端點,提供一種機制,使得兩個程式可以在網路上進行資料交換。
在 Java 中,Socket 是一個類別,封裝底層的網路通訊細節,讓開發者能夠專注於應用邏輯的實現。

Socket 在網路通訊中的角色

在網路通訊模型中,Socket 扮演著關鍵的角色:

  1. 抽象化網路通訊:Socket 將複雜的網路通訊過程抽象化,提供一個簡單的介面供應用程式使用。

  2. 建立點對點連接:Socket 允許兩個應用程式之間建立直接的連接,實現資料的雙向傳輸。

  3. 支援多種協定:Java 的 Socket API 支援多種網路協定,最常用的是 TCP(傳輸控制協定)和 UDP(使用者資料包協定)。

  4. 跨平台兼容:Java 的 Socket 程式設計提供跨平台的一致性,使得開發者可以編寫一次程式碼,在不同的作業系統上運行。

2. Socket 的基本概念

IP 位址和連接埠

  1. IP 位址

    • IP 位址是用來識別網路上每個裝置的唯一標識符。
    • IPv4 使用 32 位元表示(如 192.168.1.1),而 IPv6 使用 128 位元表示。
    • 在 Java 中,可以使用 InetAddress 類別來處理 IP 位址。
  2. 連接埠(Port)

    • 連接埠是一個 16 位元的數字,用於識別特定的網路服務或程序。
    • 連接埠範圍從 0 到 65535。
    • 常見的連接埠:HTTP(80),HTTPS(443),FTP(21)等。

Socket 類別簡介

Java 提供 java.net.Socket 類別來實現 TCP 通訊,以及 java.net.DatagramSocket 類別來實現 UDP 通訊。

  1. java.net.Socket
    • 用於建立客戶端 TCP 連接。
    • 主要方法包括:
      • connect(SocketAddress endpoint):連接到指定的伺服器。
      • getInputStream():獲取輸入串流。
      • getOutputStream():獲取輸出串流。
      • close():關閉 Socket 連接。
  2. java.net.ServerSocket
    • 用於建立伺服器端 TCP 監聽。
    • 主要方法包括:
      • accept():等待並接受客戶端連接。
      • close():關閉伺服器 Socket。
  3. java.net.DatagramSocket
    • 用於 UDP 通訊。
    • 主要方法包括:
      • send(DatagramPacket p):發送資料包。
      • receive(DatagramPacket p):接收資料包。

3. TCP Socket 程式設計

TCP(傳輸控制協定)是一種可靠的、面向連接的協定。在 Java 中,我們使用 java.net.Socketjava.net.ServerSocket 類別來實現 TCP 通訊。

建立 TCP 伺服器

建立一個 TCP 伺服器的基本步驟如下:

  1. 建立 ServerSocket 物件,指定監聽的連接埠。
  2. 呼叫 accept() 方法等待客戶端連接。
  3. 當客戶端連接時,獲取輸入和輸出串流。
  4. 處理客戶端請求。
  5. 關閉連接。

以下是簡單的 TCP 伺服器範例:

import java.io.*;
import java.net.*;

public class TCPServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(6789);
            System.out.println("Server is listening on port 6789");
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("Client connected: " + clientSocket.getInetAddress());
                
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("Received: " + inputLine);
                    out.println("Server received: " + inputLine);
                }
                
                clientSocket.close();
            }
        } finally {
            if (serverSocket != null) serverSocket.close();
        }
    }
}

建立 TCP 客戶端

建立一個 TCP 客戶端的基本步驟如下:

  1. 建立 Socket 物件,指定伺服器的 IP 位址和連接埠。
  2. 獲取輸入和輸出串流。
  3. 發送請求並接收回應。
  4. 關閉連接。

以下是簡單的 TCP 客戶端範例:

import java.io.*;
import java.net.*;

public class TCPClient {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        try {
            socket = new Socket("localhost", 6789);
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
            
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                System.out.println("Server response: " + in.readLine());
            }
        } finally {
            if (socket != null) socket.close();
        }
    }
}

實作範例

讓我們來看一個完整的 TCP 伺服器和客戶端通訊的範例,實現簡單的回音(Echo)服務,客戶端發送訊息,伺服器將接收到的訊息回傳給客戶端。

伺服器端程式碼:

import java.io.*;
import java.net.*;

public class EchoServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(6789);
            System.out.println("Echo Server is listening on port 6789");
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("Client connected: " + clientSocket.getInetAddress());
                
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("Echoing: " + inputLine);
                    out.println(inputLine);
                }
                
                clientSocket.close();
            }
        } finally {
            if (serverSocket != null) serverSocket.close();
        }
    }
}

客戶端程式碼:

import java.io.*;
import java.net.*;

public class EchoClient {
    public static void main(String[] args) throws IOException {
        Socket socket = null;
        try {
            socket = new Socket("localhost", 6789);
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
            
            String userInput;
            System.out.println("Enter messages to echo (type 'exit' to quit):");
            while ((userInput = stdIn.readLine()) != null) {
                if ("exit".equalsIgnoreCase(userInput)) break;
                out.println(userInput);
                System.out.println("Echo: " + in.readLine());
            }
        } finally {
            if (socket != null) socket.close();
        }
    }
}

4. UDP Socket 程式設計

UDP(使用者資料包協定)是一種無連接的傳輸協定,相較於 TCP,更輕量但不保證資料的可靠傳輸。
在 Java 中,我們使用 java.net.DatagramSocketjava.net.DatagramPacket 類別來實現 UDP 通訊。

建立 UDP 伺服器

建立一個 UDP 伺服器的基本步驟如下:

  1. 建立 DatagramSocket 物件,指定監聽的連接埠。
  2. 建立 DatagramPacket 物件來接收資料。
  3. 使用 receive() 方法接收資料包。
  4. 處理接收到的資料。
  5. 如果需要回應,建立新的 DatagramPacket 並使用 send() 方法發送。

以下是簡單的 UDP 伺服器範例:

import java.net.*;

public class UDPServer {
    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket(9876);
        byte[] receiveData = new byte[1024];
        
        while(true) {
            DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
            socket.receive(receivePacket);
            String sentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
            System.out.println("RECEIVED: " + sentence);
            
            InetAddress IPAddress = receivePacket.getAddress();
            int port = receivePacket.getPort();
            String capitalizedSentence = sentence.toUpperCase();
            byte[] sendData = capitalizedSentence.getBytes();
            DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
            socket.send(sendPacket);
        }
    }
}

建立 UDP 客戶端

建立一個 UDP 客戶端的基本步驟如下:

  1. 建立 DatagramSocket 物件。
  2. 建立 DatagramPacket 物件,包含要發送的資料、目標 IP 和連接埠。
  3. 使用 send() 方法發送資料包。
  4. 如果需要接收回應,使用 receive() 方法。

以下是簡單的 UDP 客戶端範例:

import java.io.*;
import java.net.*;

public class UDPClient {
    public static void main(String args[]) throws Exception {
        BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
        DatagramSocket clientSocket = new DatagramSocket();
        InetAddress IPAddress = InetAddress.getByName("localhost");
        byte[] sendData = new byte[1024];
        byte[] receiveData = new byte[1024];
        
        System.out.println("Enter a message:");
        String sentence = inFromUser.readLine();
        sendData = sentence.getBytes();
        DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, 9876);
        clientSocket.send(sendPacket);
        
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
        clientSocket.receive(receivePacket);
        String modifiedSentence = new String(receivePacket.getData(), 0, receivePacket.getLength());
        System.out.println("FROM SERVER:" + modifiedSentence);
        clientSocket.close();
    }
}

實作範例

將實現一個簡單的時間查詢服務,客戶端發送請求,伺服器回傳當前時間。

伺服器端程式碼:

import java.net.*;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeServer {
    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket(9876);
        byte[] receiveData = new byte[1024];
        
        while(true) {
            DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
            socket.receive(receivePacket);
            
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String currentTime = formatter.format(new Date());
            
            InetAddress IPAddress = receivePacket.getAddress();
            int port = receivePacket.getPort();
            byte[] sendData = currentTime.getBytes();
            DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
            socket.send(sendPacket);
        }
    }
}

客戶端程式碼:

import java.net.*;

public class TimeClient {
    public static void main(String args[]) throws Exception {
        DatagramSocket clientSocket = new DatagramSocket();
        InetAddress IPAddress = InetAddress.getByName("localhost");
        byte[] sendData = new byte[1024];
        byte[] receiveData = new byte[1024];
        
        String sentence = "GET_TIME";
        sendData = sentence.getBytes();
        DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, 9876);
        clientSocket.send(sendPacket);
        
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
        clientSocket.receive(receivePacket);
        String time = new String(receivePacket.getData(), 0, receivePacket.getLength());
        System.out.println("Current time from server: " + time);
        clientSocket.close();
    }
}

5. Socket 程式設計的實踐

錯誤處理

  1. 使用 try-with-resources:自動關閉資源,避免資源洩漏。
  2. 適當的例外處理:捕捉並處理可能發生的網路相關例外。
  3. 優雅地處理連接中斷:實現重連機制或適當的錯誤回饋。

資源管理

  1. 及時關閉 Socket:不再使用時立即關閉 Socket 連接。
  2. 使用連接池:對於高併發的應用,考慮使用連接池來管理 Socket 連接。
  3. 設置超時:為 Socket 操作設置適當的超時時間,避免無限等待。

效能考量

  1. 使用緩衝區:適當使用緩衝區可以提高 I/O 效率。
  2. 非阻塞 I/O:考慮使用 NIO(New I/O)來實現非阻塞操作,提高並發處理能力。
  3. 資料序列化:選擇高效的資料序列化方式,如 Protocol Buffers 或 JSON。

安全性

  1. 加密通訊:使用 SSL/TLS 加密敏感資料的傳輸。
  2. 驗證連接:實現適當的身份驗證機制。
  3. 防止 DoS 攻擊:實現連接限制和超時機制。

程式碼範例

以下是結合上述的實踐建議所撰寫的簡單 TCP 伺服器範例:

import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import javax.net.ssl.*;
import java.security.*;

public class BestPracticeTCPServer {
    private static final int PORT = 8888;
    private static final int TIMEOUT = 30000; // 30 seconds
    private static final int MAX_CONNECTIONS = 100;

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(MAX_CONNECTIONS);
        
        try (ServerSocket serverSocket = createSSLServerSocket()) {
            serverSocket.setSoTimeout(TIMEOUT);
            System.out.println("Server is listening on port " + PORT);
            
            while (true) {
                try {
                    Socket clientSocket = serverSocket.accept();
                    executor.submit(new ClientHandler(clientSocket));
                } catch (SocketTimeoutException e) {
                    System.out.println("Timeout waiting for connection, continuing...");
                }
            }
        } catch (IOException e) {
            System.err.println("Could not listen on port " + PORT);
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }

    private static SSLServerSocket createSSLServerSocket() throws IOException {
        try {
            // 加載金鑰庫
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(new FileInputStream("keystore.jks"), "keystorepassword".toCharArray());

            // 創建並初始化金鑰管理器
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            kmf.init(keyStore, "keystorepassword".toCharArray());

            // 創建並初始化 SSL 上下文
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(kmf.getKeyManagers(), null, null);

            // 創建 SSL 伺服器 socket
            SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
            return (SSLServerSocket) sslServerSocketFactory.createServerSocket(PORT);
        } catch (Exception e) {
            throw new IOException("Failed to create SSL server socket", e);
        }
    }

    private static class ClientHandler implements Runnable {
        private final Socket clientSocket;

        public ClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        @Override
        public void run() {
            try (
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
            ) {
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("Received: " + inputLine);
                    out.println("Server received: " + inputLine);
                    if ("bye".equalsIgnoreCase(inputLine)) {
                        break;
                    }
                }
            } catch (IOException e) {
                System.err.println("Error handling client: " + e.getMessage());
            } finally {
                try {
                    clientSocket.close();
                } catch (IOException e) {
                    System.err.println("Error closing client socket: " + e.getMessage());
                }
            }
        }
    }
}

本篇文章同步刊載: JYI.TW
筆者個人的網站: JUNYI


上一篇
Java IO和NIO:非阻塞 IO 的實際應用場景及範例解析
下一篇
Java 網路程式設計:理解 TCP 和 UDP 的區別及應用
系列文
我的Java自學之路:一個轉職者的30篇技術統整30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言