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

下面我将分步讲解,并提供一个完整的、可运行的客户端/服务器示例。
核心概念
java.net.Socket:代表客户端,它尝试连接到服务器的指定 IP 地址和端口。java.net.ServerSocket:代表服务器,它在指定的端口上监听客户端的连接请求。java.net.InetAddress:表示 IP 地址,可以通过InetAddress.getByName("主机名")来获取。InputStream/OutputStream:这是 Socket 进行数据传输的底层流,所有数据都必须以字节形式在这些流中传递。- 字符编码:这是最关键也最容易出错的一点,在将字符串转换为字节数组(编码)和将字节数组转换回字符串(解码)时,必须使用相同的字符编码,否则会出现乱码。强烈推荐使用
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());
}
}
}
如何运行
- 编译:将
Server.java和Client.java放在同一个目录下,分别编译它们。javac Server.java javac Client.java
- 运行服务器:首先在终端或命令行中运行服务器。
java Server
你会看到输出:
服务器已启动,等待客户端连接... - 运行客户端:在另一个新的终端中运行客户端。
java Client
预期输出:

-
服务器端控制台:
服务器已启动,等待客户端连接... 收到客户端消息: 你好,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();
重要注意事项和最佳实践
-
编码问题:再次强调,发送方和接收方必须使用相同的字符编码,如果不指定,会使用平台默认编码(Windows 可能是
GBK,Linux/macOS 可能是UTF-8),这极易导致乱码,显式指定"UTF-8"是最稳妥的做法。 -
流的生命周期:
InputStream和OutputStream是从Socket中获取的,它们的生命周期与Socket绑定,一个Socket对应一个输入流和一个输出流。 -
阻塞行为:
serverSocket.accept():会一直阻塞,直到有新的客户端连接进来。reader.readLine():会一直阻塞,直到从输入流中读取到一行数据。inputStream.read():也会阻塞,直到读取到至少一个字节。
-
资源关闭:网络资源(
Socket,ServerSocket,InputStream,OutputStream)是有限且宝贵的,使用完毕后必须关闭,推荐使用try-with-resources语句(如示例所示),它可以自动关闭实现了AutoCloseable接口的对象,避免资源泄漏。 -
如何发送和接收多行文本或复杂对象?
- 定长消息:可以在消息前加上消息的长度,接收方先读取长度,再读取指定长度的数据。
- 分隔符:使用特殊字符(如
\0或自定义分隔符)来标记消息的结束。 - 对象序列化:对于复杂对象,可以使用
ObjectOutputStream和ObjectInputStream将整个对象序列化为字节流进行传输,但要注意,这种方式传输的数据量较大,且要求对象实现Serializable接口。
这个例子为你打下了坚实的基础,理解了字符串的发送和接收,你就可以进一步学习更复杂的网络通信场景。
