杰瑞科技汇

Java Socket如何高效接收消息?

核心概念

  1. Socket (套接字):网络通信的端点,你可以把它想象成一个电话,通过它可以在网络上建立连接。
  2. ServerSocket (服务器套接字):服务器端用来监听客户端连接请求的“总机”,它会在一个指定的端口上等待,当有客户端尝试连接时,它会接受连接并返回一个 Socket 对象,用于与该客户端进行后续通信。
  3. InputStream / OutputStream:通过 Socket 获取的输入流和输出流,用于在连接上读取(接收)和写入(发送)数据。
  4. 字节流 vs. 字符流
    • 字节流SocketgetInputStream()getOutputStream() 默认提供的是字节流 (InputStream / OutputStream),它们直接处理字节数据,适合传输任何类型的数据(如图片、文件等)。
    • 字符流:为了方便处理文本,我们通常会将字节流包装成字符流,如 InputStreamReader (字节流转字符流) 和 BufferedReader (为字符流提供缓冲功能,提高效率)。

第一步:创建一个简单的 Echo 服务器

一个 "Echo" 服务器会将从客户端接收到的任何消息原封不动地发送回去,这是理解 Socket 通信最经典的例子。

Java Socket如何高效接收消息?-图1
(图片来源网络,侵删)

服务器端代码 (EchoServer.java)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
    public static void main(String[] args) {
        // 服务器监听的端口号
        int port = 12345;
        try (// 创建一个 ServerSocket,在指定的端口上监听客户端连接
             ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("服务器已启动,正在监听端口 " + port + "...");
            // serverSocket.accept() 是一个阻塞方法,它会一直等待直到有客户端连接
            // 当有客户端连接时,它会返回一个 Socket 对象,代表与该客户端的连接
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
            // 获取客户端的输入流,用于读取客户端发送的消息
            // 使用 BufferedReader 包装 InputStreamReader,以便按行读取文本
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // 读取客户端发送的消息
            // readLine() 也是一个阻塞方法,会一直等待直到客户端发送一行数据(以换行符结尾)
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                // 如果客户端发送 "exit",则关闭连接
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("客户端请求关闭连接。");
                    break;
                }
                System.out.println("收到客户端消息: " + inputLine);
                // Echo 回去
                // 注意:为了简单起见,这里我们只打印到服务器控制台。
                // 一个完整的 Echo 服务器会把消息写回客户端的输出流。
            }
        } catch (IOException e) {
            System.err.println("服务器发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

代码解析:

  1. ServerSocket serverSocket = new ServerSocket(12345);: 在 12345 端口上创建一个服务器套接字。
  2. serverSocket.accept();: 这是最关键的一步,它会阻塞程序的执行,直到有一个客户端连接到这个端口,一旦连接成功,它会返回一个 Socket 对象 clientSocket
  3. new BufferedReader(new InputStreamReader(clientSocket.getInputStream())): 获取从客户端流向服务器的数据流,我们使用 InputStreamReader 将字节流转为字符流,再用 BufferedReader 包装它,这样可以方便地使用 readLine() 方法按行读取消息。
  4. while ((inputLine = in.readLine()) != null): 循环读取客户端发送的每一行数据。readLine() 也会阻塞,直到流中有新的数据可读。
  5. if ("exit".equalsIgnoreCase(inputLine)): 一个简单的退出机制,当客户端发送 "exit" 时,服务器会退出循环并关闭连接。
  6. try-with-resources: ServerSocketSocket 都实现了 AutoCloseable 接口,使用 try-with-resources 语句可以确保它们在使用后被自动关闭,防止资源泄露。

第二步:创建客户端来连接服务器并发送消息

客户端代码 (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" 或 "127.0.0.1")
        String hostname = "localhost";
        // 服务器的端口号,必须与服务器设置的端口一致
        int port = 12345;
        try (// 创建一个 Socket 连接到指定主机和端口
             Socket socket = new Socket(hostname, 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("已连接到服务器。");
            System.out.println("请输入要发送的消息(输入 'exit' 退出):");
            String userInput;
            // 循环读取用户从键盘输入的消息
            while ((userInput = stdIn.readLine()) != null) {
                // 将用户输入的消息发送给服务器
                out.println(userInput);
                // 读取服务器返回的 Echo 消息
                String response = in.readLine();
                System.out.println("服务器回复: " + response);
                // 检查是否要退出
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                System.out.println("请输入下一条消息:");
            }
        } catch (UnknownHostException e) {
            System.err.println("无法找到主机: " + hostname);
            e.printStackTrace();
        } catch (IOException e) {
            System.err.println("I/O 发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

代码解析:

  1. new Socket("localhost", 12345);: 创建一个套接字,尝试连接到 localhost(本机)的 12345 端口,这同样是一个阻塞操作,直到连接成功或失败。
  2. new PrintWriter(socket.getOutputStream(), true): 获取流向服务器的数据流。PrintWriter 可以方便地使用 println() 方法发送字符串,并自动处理换行符。true 参数表示自动刷新(每次调用 println 后都会立即将数据发送出去)。
  3. new BufferedReader(new InputStreamReader(socket.getInputStream())): 获取从服务器返回的数据流,用于读取服务器的回复。
  4. BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)): 获取标准输入流(即键盘输入),以便让用户可以输入消息。
  5. while ((userInput = stdIn.readLine()) != null): 循环读取用户在控制台输入的每一行内容。
  6. out.println(userInput);: 将用户输入的内容发送给服务器。
  7. String response = in.readLine();: 读取服务器返回的回复。

如何运行

  1. 编译代码:

    javac EchoServer.java EchoClient.java
  2. 启动服务器: 在一个终端窗口中运行服务器:

    Java Socket如何高效接收消息?-图2
    (图片来源网络,侵删)
    java EchoServer

    你会看到输出:服务器已启动,正在监听端口 12345...

  3. 启动客户端: 在另一个终端窗口中运行客户端:

    java EchoClient

    你会看到输出:已连接到服务器。

  4. 测试通信:

    Java Socket如何高效接收消息?-图3
    (图片来源网络,侵删)
    • 在客户端的终端输入任意消息,然后按回车。

    • 输入 Hello Server,按回车。

    • 客户端会显示:服务器回复: Hello Server

    • 服务器终端会显示:收到客户端消息: Hello Server

    • 在客户端输入 exit,按回车。

    • 客户端和服务器都会关闭连接并退出程序。


重要注意事项和进阶

阻塞操作

serverSocket.accept(), socket.getInputStream().read(), bufferedReader.readLine() 都是阻塞方法,这意味着如果没有数据到达,线程会一直等待,无法执行后续代码,对于需要同时处理多个客户端的服务器,这是一个大问题。

处理多个客户端

一个简单的 while 循环只能处理一个客户端,要处理多个客户端,你需要使用多线程。

改进思路: 当 serverSocket.accept() 返回一个新的 Socket 后,立即创建一个新的线程来处理这个客户端的通信,而主线程则继续返回 accept() 状态,等待下一个客户端。

示例代码框架:

// 在 EchoServer 的 main 方法中
while (true) {
    Socket clientSocket = serverSocket.accept();
    // 为每个客户端连接创建一个新线程
    new Thread(new ClientHandler(clientSocket)).start();
}
// ClientHandler 类实现 Runnable 接口
class ClientHandler implements Runnable {
    private Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println("来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
                // ... 处理消息 ...
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

发送和接收复杂数据

上面的例子只适合发送文本行,如果要发送对象、图片等,你需要使用更高级的序列化技术。

  • 发送对象: 使用 ObjectOutputStreamObjectInputStream

    // 发送端
    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
    oos.writeObject(myObject); // myObject 必须实现 Serializable 接口
    // 接收端
    ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
    MyObject receivedObject = (MyObject) ois.readObject();
  • 发送文件: 使用 FileInputStream 读取文件,然后用 OutputStream 发送。

关闭资源

非常重要! 在通信结束后,必须关闭所有打开的资源(Socket, InputStream, OutputStream),以避免资源泄露,推荐使用 try-with-resources 语句,如上面的示例所示,它能确保资源被自动关闭。

异常处理

网络通信非常脆弱,可能会因为各种原因(如网络中断、客户端突然断开)抛出 IOException,代码中必须有健壮的异常处理机制。

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