核心概念:Socket 是什么?
在计算机网络中,Socket (套接字) 是通信的端点,你可以把它想象成一个电话,通过这个电话,你的程序(客户端)可以和另一个程序(服务器)进行双向的数据交换。

Java 使用 java.net 包中的类来处理网络通信,对于客户端,最核心的类是 Socket。
一个简单的比喻:
- 客户端 (Client):就像你拿起电话,拨打一个号码(服务器的 IP 地址和端口号)。
- 服务器 (Server):就像电话另一端的人,已经接通了电话,准备与你通话。
- IP 地址 (IP Address):电话号码,用来在网络中唯一标识一台服务器。
- 端口号 (Port Number):分机号,因为一台服务器上可能同时运行多个服务,端口号用来区分具体是哪个服务在等待连接。
- 输入/输出流 (Input/Output Stream):就像电话的听筒,一个用于接收(输入)对方说的话,一个用于发送(输出)你的话。
Java Socket 客户端编程步骤
创建一个 Java Socket 客户端通常遵循以下固定步骤:
- 创建 Socket 对象:指定服务器的 IP 地址和端口号,尝试连接服务器,如果服务器未开启或地址错误,这里会抛出异常。
- 获取输入/输出流:连接成功后,通过
Socket对象获取InputStream和OutputStream(或更方便的InputStreamReader/OutputStreamWriter、BufferedReader/PrintWriter)。 - 进行数据交换:
- 通过
OutputStream向服务器发送数据。 - 通过
InputStream从服务器接收数据。
- 通过
- 关闭资源:通信结束后,按照“后开先关”的原则,依次关闭流和
Socket对象,释放系统资源。
核心 API 介绍
Socket(String host, int port)- 创建一个流套接字并将其连接到指定主机上的指定端口号。
host:服务器的主机名或 IP 地址("127.0.0.1"或"localhost")。port:服务器正在监听的端口号(8080)。
InputStream getInputStream()返回此套接字的输入流,你可以从中读取服务器发送的数据。
(图片来源网络,侵删)OutputStream getOutputStream()返回此套接字的输出流,你可以通过它向服务器写入数据。
void close()关闭此套接字,一旦关闭,它就不能再使用。
为了方便操作字符流,我们通常会使用包装类:
InputStreamReader: 将字节输入流 (InputStream) 转换为字符输入流。OutputStreamWriter: 将字节输出流 (OutputStream) 转换为字符输出流。BufferedReader: 为字符输入流提供缓冲,可以方便地按行读取 (readLine())。PrintWriter: 为字符输出流提供便捷的打印方法 (println(),print()),并可以自动刷新缓冲区。
完整代码示例:一个简单的回显客户端
这个客户端会连接到一个回显服务器(服务器会将收到的任何消息原样返回给客户端),向服务器发送一行文本,然后接收并打印服务器的回复。

客户端代码 (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) {
// 服务器地址和端口
String host = "localhost"; // 或 "127.0.0.1"
int port = 8080;
// try-with-resources 语句,可以自动关闭资源
try (
// 1. 创建 Socket 对象,连接服务器
Socket socket = new Socket(host, port);
// 2. 获取输入输出流,并包装成更易用的字符流
// PrintWriter 用于向服务器发送数据,autoFlush=true 自动刷新缓冲区
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// BufferedReader 用于从服务器接收数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// BufferedReader 用于从控制台读取用户输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器 " + host + ":" + port);
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
String userInput;
// 3. 进行数据交换
while ((userInput = stdIn.readLine()) != null) {
// 发送消息到服务器
out.println(userInput);
// 如果用户输入 'exit',则退出循环
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
// 从服务器接收回复
String response = in.readLine();
System.out.println("服务器回复: " + response);
}
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + host);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 发生错误: " + e.getMessage());
e.printStackTrace();
}
// 4. try-with-resources 会自动关闭 socket 和所有流
System.out.println("客户端已断开连接。");
}
}
如何运行这个示例?
-
你需要一个配套的服务器,下面是一个非常简单的回显服务器代码,你可以将它保存为
EchoServer.java并在另一个终端或 IDE 中运行。// 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) throws IOException { int port = 8080; try (ServerSocket serverSocket = new ServerSocket(port)) { System.out.println("服务器正在监听端口 " + port + "..."); // 等待客户端连接 Socket clientSocket = serverSocket.accept(); System.out.println("客户端已连接: " + clientSocket.getInetAddress()); try ( PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())) ) { String inputLine; // 读取客户端发送的每一行数据 while ((inputLine = in.readLine()) != null) { System.out.println("收到客户端消息: " + inputLine); // 将消息回显给客户端 out.println(inputLine); } } } System.out.println("服务器已关闭。"); } } -
运行步骤:
- 先编译并运行
EchoServer。 - 然后编译并运行
EchoClient。 - 在客户端的控制台输入任何文本,按回车,你会在客户端看到服务器的回显,输入
exit退出。
- 先编译并运行
进阶话题
1 多线程客户端
上面的示例是同步的,客户端在等待服务器回复时会阻塞,在实际应用中,一个客户端通常需要同时处理发送和接收,这时,我们可以使用多线程。
- 一个线程(主线程):负责从
System.in读取用户输入,并发送给服务器。 - 另一个线程(新线程):负责从
Socket.getInputStream()读取服务器发送来的消息,并打印到控制台。
多线程客户端示例 (MultiThreadEchoClient.java)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class MultiThreadEchoClient {
public static void main(String[] args) {
String host = "localhost";
int port = 8080;
try (Socket socket = new Socket(host, port)) {
System.out.println("已连接到服务器 " + host + ":" + port);
// 发送线程
Thread sendThread = new Thread(new SendTask(socket));
// 接收线程
Thread receiveThread = new Thread(new ReceiveTask(socket));
sendThread.start();
receiveThread.start();
// 等待发送线程结束(即用户输入 'exit')
sendThread.join();
// 然后关闭 socket,这会导致接收线程的 readLine() 抛出异常并结束
socket.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
// 负责发送数据的任务
class SendTask implements Runnable {
private final Socket socket;
public SendTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
String userInput;
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
}
} catch (IOException e) {
// socket 被关闭,这里会抛出异常,是正常的
// System.err.println("发送线程出错: " + e.getMessage());
}
}
}
// 负责接收数据的任务
class ReceiveTask implements Runnable {
private final Socket socket;
public ReceiveTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
String response;
while ((response = in.readLine()) != null) {
System.out.println("服务器回复: " + response);
}
} catch (IOException e) {
// socket 被关闭,这里会抛出异常,是正常的
// System.err.println("接收线程出错: " + e.getMessage());
}
}
}
2 异步 I/O (NIO - New I/O)
对于高性能、高并发的网络应用,传统的阻塞式 I/O (BIO) 效率很低,Java NIO 提供了非阻塞 I/O 的能力,它使用通道 (Channel)、缓冲区 (Buffer) 和选择器 (Selector) 来实现。
- Channel:类似流,但可以双向读写。
- Buffer:数据被写入缓冲区,然后从缓冲区读取。
- Selector:允许一个线程监视多个通道的事件(如连接、数据到达等),当某个通道有事件发生时,Selector 会通知线程进行处理。
NIO 的学习曲线比 BIO 更陡峭,但性能优势巨大,是实现高性能网络服务器(如聊天室、游戏服务器)的基础。
| 特性 | 同步阻塞式 (BIO) | 多线程 BIO | 异步非阻塞式 (NIO) |
|---|---|---|---|
| 模型 | 一连接一线程 | 一连接一线程,但分离了收发 | 一个线程管理多个连接 |
| 优点 | 编程简单,逻辑清晰 | 能同时处理收发,响应性好 | 高并发,高吞吐,资源占用低 |
| 缺点 | 性能差,无法处理并发 | 线程创建和切换开销大 | 编程模型复杂,学习成本高 |
| 适用场景 | 简单工具、学习示例 | 中小型应用,对并发要求不高 | 大型、高性能网络应用 |
对于初学者,掌握同步阻塞式 (BIO) 客户端是第一步,理解其原理后,再学习多线程模型,最后根据需要探索 NIO,希望这份详细的指南能帮助你理解 Java Socket 客户端编程!
