杰瑞科技汇

Java Socket通信实例如何实现?

我们将使用 Java 最基础的 java.net.Socketjava.net.ServerSocket 来实现一个简单的“回显服务器”(Echo Server),客户端发送任何消息,服务器都会原样返回给客户端。

Java Socket通信实例如何实现?-图1
(图片来源网络,侵删)

核心概念

  1. Socket (套接字):网络通信的端点,它就像是电话机,负责建立连接、发送和接收数据。
  2. ServerSocket (服务器套接字):服务器端用来“监听”客户端连接请求的“总机”,当有客户端请求连接时,它会“接听”电话,并为每个客户端分配一个独立的 Socket 来进行后续通信。
  3. IP 地址:网络中设备的唯一标识,0.0.1 (本机地址)。
  4. 端口号:设备上应用程序的唯一标识,范围是 0-65535。8080,IP 地址 + 端口号可以唯一确定一个网络服务。

项目结构

我们创建两个独立的 Java 类:EchoServer.javaEchoClient.java

SocketDemo/
├── EchoServer.java
└── EchoClient.java

服务器端代码 (EchoServer.java)

服务器的工作流程是:

  1. 创建一个 ServerSocket 并在指定端口上开始监听。
  2. 调用 accept() 方法,阻塞等待客户端连接。
  3. 当客户端连接成功后,accept() 返回一个代表该客户端连接的 Socket 对象。
  4. 通过 Socket 获取输入流和输出流,与客户端进行双向通信。
  5. 通信结束后,关闭资源。
// EchoServer.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
    public static void main(String[] args) {
        // 定义服务器要监听的端口号
        int port = 8080;
        // 使用 try-with-resources 语句,确保 ServerSocket 被自动关闭
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // accept() 方法会阻塞,直到有客户端连接
            try (Socket clientSocket = serverSocket.accept();
                 // 从客户端获取输入流,用于读取客户端发送的数据
                 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                 // 向客户端获取输出流,用于发送数据给客户端
                 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
                System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
                String inputLine;
                // 循环读取客户端发送的数据
                while ((inputLine = in.readLine()) != null) {
                    System.out.println("收到客户端消息: " + inputLine);
                    // 将收到的消息原样回显给客户端
                    out.println("服务器回显: " + inputLine);
                    // 如果客户端发送 "bye",则结束通信
                    if ("bye".equalsIgnoreCase(inputLine)) {
                        System.out.println("客户端请求断开连接。");
                        break;
                    }
                }
            } catch (IOException e) {
                System.err.println("与客户端通信时发生错误: " + e.getMessage());
            }
            System.out.println("服务器已关闭。");
        } catch (IOException e) {
            System.err.println("服务器无法在端口 " + port + " 上启动或监听: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

客户端代码 (EchoClient.java)

客户端的工作流程是:

  1. 创建一个 Socket 对象,指定服务器的 IP 地址和端口号,尝试连接服务器。
  2. 连接成功后,通过 Socket 获取输入流和输出流,与服务器进行双向通信。
  3. 通信结束后,关闭资源。
// EchoClient.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class EchoClient {
    public static void main(String[] args) {
        // 服务器的 IP 地址 (localhost 代表本机) 和端口号
        String host = "127.0.0.1";
        int port = 8080;
        // 使用 try-with-resources 语句,确保 Socket, PrintWriter, BufferedReader 被自动关闭
        try (
            Socket socket = new Socket(host, port);
            // 获取输出流,用于发送数据给服务器
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // 获取输入流,用于读取服务器返回的数据
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 用于从控制台读取用户输入
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
        ) {
            System.out.println("已连接到服务器 " + host + ":" + port);
            System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
            String userInput;
            // 循环读取用户从控制台输入的消息
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入的消息发送给服务器
                out.println(userInput);
                // 读取并打印服务器返回的回显消息
                String response = in.readLine();
                System.out.println("服务器响应: " + response);
                // 如果用户输入 "bye",则退出循环
                if ("bye".equalsIgnoreCase(userInput)) {
                    break;
                }
                System.out.println("请输入下一条消息 (输入 'bye' 退出):");
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到主机: " + host);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("无法连接到主机 " + host + " 或发生 I/O 错误: " + e.getMessage());
            e.printStackTrace();
        }
        System.out.println("客户端已关闭。");
    }
}

如何运行

  1. 编译代码:打开终端或命令提示符,进入 SocketDemo 目录,运行 javac 命令。

    Java Socket通信实例如何实现?-图2
    (图片来源网络,侵删)
    javac EchoServer.java EchoClient.java
  2. 启动服务器:在一个终端窗口中,先运行服务器,服务器启动后会进入阻塞状态,等待客户端连接。

    java EchoServer

    你会看到输出:

    服务器已启动,正在监听端口 8080...
  3. 启动客户端:在另一个终端窗口中,运行客户端。

    java EchoClient

    你会看到客户端的输出:

    Java Socket通信实例如何实现?-图3
    (图片来源网络,侵删)
    已连接到服务器 127.0.0.1:8080
    请输入要发送消息 (输入 'bye' 退出):
  4. 进行通信

    • 在客户端的终端输入任意消息,Hello, Server!,然后按回车。
    • 客户端会立即收到服务器的回显:
      服务器响应: 服务器回显: Hello, Server!
    • 在服务器的终端,你会看到:
      客户端已连接: 127.0.0.1
      收到客户端消息: Hello, Server!
    • 继续输入更多消息进行测试。
  5. 关闭连接:在客户端输入 bye 并按回车。

    • 客户端会退出程序。
    • 服务器端也会检测到 bye 消息,打印 "客户端请求断开连接。",然后回到等待新连接的状态(accept() 再次阻塞)。

代码关键点解释

  • try-with-resources:这是 Java 7 引入的一个非常方便的特性,它确保了在 try 语句块结束时,所有实现了 AutoCloseable 接口(如 Socket, ServerSocket, PrintWriter, BufferedReader)的资源都会被自动关闭,无需手动调用 close() 方法,避免了资源泄漏。

  • new PrintWriter(socket.getOutputStream(), true):第二个参数 true 表示启用自动刷新,这样,每当调用 println()printf() 方法后,输出流都会自动刷新,确保数据能立即发送出去,而不需要手动调用 out.flush()

  • 阻塞方法

    • serverSocket.accept():这是服务器端的核心方法,它会一直“阻塞”(程序暂停执行),直到有一个客户端尝试连接,一旦有连接,它会返回一个新的 Socket 对象,代表与这个特定客户端的连接。
    • in.readLine():这个方法用于读取一行文本,它也会阻塞,直到输入流中有可读的数据行(以换行符 \n
  • 通信协议:我们使用简单的文本行作为协议,客户端发送一行,服务器读取一行,再发送一行,这种基于行的协议非常适合初学者理解,在实际应用中,协议会更复杂,可能会定义消息头、消息体、消息长度等。


扩展与进阶

这个实例是“一问一答”的同步模式,一个更高级的服务器通常需要能够同时处理多个客户端,这可以通过以下两种主要方式实现:

  1. 为每个客户端创建一个新线程

    • serverSocket.accept() 返回一个新的 Socket 后,立即创建一个新的 Thread,并将与该客户端的通信逻辑放在这个新线程中运行。
    • 这样,主线程可以继续回到 accept() 状态,等待下一个客户端,而其他客户端的通信在各自的线程中并发进行。
  2. 使用线程池

    为了避免为每个客户端都创建一个新线程(可能导致资源耗尽),可以使用一个固定大小的线程池来管理客户端连接任务。

  3. 使用 NIO (New I/O)

    • 对于更高性能、更高并发的场景,可以使用 Java NIO (基于 java.nio.channels 包),NIO 使用非阻塞 I/O 和选择器,可以用一个线程管理多个连接,极大地提高了系统的扩展性和效率。

这个基础实例是理解所有网络编程和高级网络框架(如 Netty、Vert.x)的基石,务必掌握其工作原理。

分享:
扫描分享到社交APP
上一篇
下一篇