杰瑞科技汇

Java Socket如何高效传输字符串?

Socket 是基于字节流(InputStream / OutputStream)进行通信的,而不是直接传输字符串,我们需要将字符串转换为字节数组进行发送,并在接收端将接收到的字节数组重新组合成字符串。

Java Socket如何高效传输字符串?-图1
(图片来源网络,侵删)

下面我将分步讲解,并提供一个完整的、可运行的客户端/服务器示例。


核心概念

  1. java.net.Socket:代表客户端,它尝试连接到服务器的指定 IP 地址和端口。
  2. java.net.ServerSocket:代表服务器,它在指定的端口上监听客户端的连接请求。
  3. java.net.InetAddress:表示 IP 地址,可以通过 InetAddress.getByName("主机名") 来获取。
  4. InputStream / OutputStream:这是 Socket 进行数据传输的底层流,所有数据都必须以字节形式在这些流中传递。
  5. 字符编码:这是最关键也最容易出错的一点,在将字符串转换为字节数组(编码)和将字节数组转换回字符串(解码)时,必须使用相同的字符编码,否则会出现乱码。强烈推荐使用 UTF-8

完整示例:一个简单的 Echo Server(回声服务器)

这个例子包含两个部分:

  • 服务器端:在指定端口监听,接收客户端发来的字符串,然后将其原样发回给客户端。
  • 客户端:连接到服务器,发送一个字符串,并打印服务器返回的字符串。

服务器端代码 (Server.java)

import java.io.*;
import java.net.*;
public class Server {
    public static void main(String[] args) {
        // 定义服务器监听的端口号
        int port = 12345;
        try ( // 使用 try-with-resources 语句,确保资源被自动关闭
            // 创建一个 ServerSocket 并在指定端口上监听
            ServerSocket serverSocket = new ServerSocket(port);
            // 调用 accept() 方法,阻塞等待客户端连接
            // 当有客户端连接时,accept() 返回一个代表该客户端连接的 Socket 对象
            Socket clientSocket = serverSocket.accept();
            // 从客户端连接中获取输入流,用于读取客户端发送的数据
            InputStream inputStream = clientSocket.getInputStream();
            // 使用 InputStreamReader 将字节流转换为字符流,并指定编码为 UTF-8
            // 使用 BufferedReader 来高效地读取一行文本
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            // 从客户端连接中获取输出流,用于向客户端发送数据
            OutputStream outputStream = clientSocket.getOutputStream();
            // 使用 OutputStreamWriter 将字节流转换为字符流,并指定编码为 UTF-8
            // 使用 PrintWriter 来方便地写入文本(如 println)
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true);
            // true: 启用自动刷新,每次调用 println() 或 printf() 后都会自动刷新输出缓冲区
        ) {
            System.out.println("服务器已启动,等待客户端连接...");
            // 1. 接收客户端发来的字符串
            // readLine() 会阻塞,直到读取到一行文本(以换行符结尾)
            String receivedMessage = reader.readLine();
            System.out.println("收到客户端消息: " + receivedMessage);
            // 2. 将收到的消息回写给客户端
            writer.println("服务器已收到你的消息: " + receivedMessage);
            System.out.println("已向客户端发送回声消息。");
        } catch (IOException e) {
            System.err.println("服务器发生错误: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

客户端代码 (Client.java)

import java.io.*;
import java.net.*;
public class Client {
    public static void main(String[] args) {
        // 服务器的 IP 地址(本地回环地址)和端口号
        String serverHost = "127.0.0.1";
        int port = 12345;
        try ( // 使用 try-with-resources 语句
            // 创建一个 Socket 对象,尝试连接到指定服务器的端口
            Socket socket = new Socket(serverHost, port);
            // 从 Socket 获取输出流,用于向服务器发送数据
            OutputStream outputStream = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true);
            // 从 Socket 获取输入流,用于读取服务器返回的数据
            InputStream inputStream = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        ) {
            System.out.println("已成功连接到服务器。");
            // 1. 向服务器发送一个字符串
            String messageToSend = "你好,Java Socket!";
            System.out.println("向服务器发送消息: " + messageToSend);
            writer.println(messageToSend); // 使用 println 发送,服务器端用 readLine() 接收
            // 2. 读取服务器返回的字符串
            String serverResponse = reader.readLine();
            System.out.println("收到服务器回声: " + serverResponse);
        } catch (UnknownHostException e) {
            System.err.println("找不到服务器: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("客户端发生 I/O 错误: " + e.getMessage());
        }
    }
}

如何运行

  1. 编译:将 Server.javaClient.java 放在同一个目录下,分别编译它们。
    javac Server.java
    javac Client.java
  2. 运行服务器:首先在终端或命令行中运行服务器。
    java Server

    你会看到输出:服务器已启动,等待客户端连接...

  3. 运行客户端在另一个新的终端中运行客户端。
    java Client

预期输出:

Java Socket如何高效传输字符串?-图2
(图片来源网络,侵删)
  • 服务器端控制台:

    服务器已启动,等待客户端连接...
    收到客户端消息: 你好,Java Socket!
    已向客户端发送回声消息。
  • 客户端控制台:

    已成功连接到服务器。
    向服务器发送消息: 你好,Java Socket!
    收到服务器回声: 服务器已收到你的消息: 你好,Java Socket!

关键代码解析

字符串的发送(客户端)

// 1. 准备要发送的字符串
String messageToSend = "你好,Java Socket!";
// 2. 获取输出流
OutputStream outputStream = socket.getOutputStream();
// 3. 创建 PrintWriter,这是关键步骤!
//    - OutputStreamWriter: 将字节流转换为字符流,并指定编码(UTF-8)
//    - PrintWriter: 提供方便的 print() 和 println() 方法
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"), true);
// 4. 发送字符串
//    println() 会在字符串末尾自动添加一个换行符(\n),这对于服务器端的 readLine() 至关重要
writer.println(messageToSend);

字符串的接收(服务器端)

// 1. 获取输入流
InputStream inputStream = socket.getInputStream();
// 2. 创建 BufferedReader,这是关键步骤!
//    - InputStreamReader: 将字节流转换为字符流,并指定编码(必须与发送端一致!)
//    - BufferedReader: 提供方便的 readLine() 方法,可以一次性读取一行文本
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
// 3. 接收字符串
//    readLine() 会阻塞,直到读取到一行文本(遇到换行符 \n 或回车符 \r)或流结束
String receivedMessage = reader.readLine();

重要注意事项和最佳实践

  1. 编码问题:再次强调,发送方和接收方必须使用相同的字符编码,如果不指定,会使用平台默认编码(Windows 可能是 GBK,Linux/macOS 可能是 UTF-8),这极易导致乱码,显式指定 "UTF-8" 是最稳妥的做法。

  2. 流的生命周期InputStreamOutputStream 是从 Socket 中获取的,它们的生命周期与 Socket 绑定,一个 Socket 对应一个输入流和一个输出流。

  3. 阻塞行为

    • serverSocket.accept():会一直阻塞,直到有新的客户端连接进来。
    • reader.readLine():会一直阻塞,直到从输入流中读取到一行数据。
    • inputStream.read():也会阻塞,直到读取到至少一个字节。
  4. 资源关闭:网络资源(Socket, ServerSocket, InputStream, OutputStream)是有限且宝贵的,使用完毕后必须关闭,推荐使用 try-with-resources 语句(如示例所示),它可以自动关闭实现了 AutoCloseable 接口的对象,避免资源泄漏。

  5. 如何发送和接收多行文本或复杂对象?

    • 定长消息:可以在消息前加上消息的长度,接收方先读取长度,再读取指定长度的数据。
    • 分隔符:使用特殊字符(如 \0 或自定义分隔符)来标记消息的结束。
    • 对象序列化:对于复杂对象,可以使用 ObjectOutputStreamObjectInputStream 将整个对象序列化为字节流进行传输,但要注意,这种方式传输的数据量较大,且要求对象实现 Serializable 接口。

这个例子为你打下了坚实的基础,理解了字符串的发送和接收,你就可以进一步学习更复杂的网络通信场景。

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