核心概念
在开始编码之前,我们先理解几个基本概念:

- Socket (套接字):它是网络通信的端点,你可以把它想象成一个电话,应用程序通过这个“电话”来发送和接收数据,在 Java 中,
Socket类代表客户端的套接字。 - IP 地址:网络中设备的唯一标识,
0.0.1(代表本机) 或168.1.100。 - 端口号:在同一台机器上,不同的应用程序通过不同的端口号来区分,端口号是一个 16 位的整数(0-65535),0-1023 是系统保留端口,Web 服务通常使用 80 或 443 端口。
- 客户端/服务器模型:
- 服务器:在某个 IP 地址和端口号上监听客户端的连接请求,它被动地等待客户端发起连接。
- 客户端:主动发起连接请求到服务器的 IP 地址和端口号,如果服务器接受请求,连接就建立了,双方就可以进行双向通信。
客户端的核心步骤
一个典型的 Java Socket 客户端程序遵循以下步骤:
- 创建
Socket对象:使用服务器的 IP 地址和端口号来创建Socket对象,这一步会尝试连接到服务器,如果连接失败(服务器未启动或地址错误),会抛出IOException。 - 获取输入/输出流:连接成功后,通过
Socket对象获取InputStream和OutputStream。OutputStream:用于向服务器发送数据。InputStream:用于从服务器接收数据。
- 进行数据通信:
- 发送数据:将数据写入
OutputStream。 - 接收数据:从
InputStream读取数据。
- 发送数据:将数据写入
- 关闭资源:通信结束后,按照“后开先关”的原则,依次关闭
InputStream、OutputStream和Socket。
基础代码示例
下面是一个最简单的客户端代码示例,它连接到本地的服务器(IP: 0.0.1,端口: 8888),发送一条消息,并接收服务器的响应。
客户端代码 (SimpleClient.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 SimpleClient {
public static void main(String[] args) {
// 服务器的IP地址和端口号
String serverHost = "127.0.0.1";
int serverPort = 8888;
// 使用 try-with-resources 语句,可以自动关闭资源
try (Socket socket = new Socket(serverHost, serverPort);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
System.out.println("成功连接到服务器: " + serverHost + ":" + serverPort);
// 1. 发送数据到服务器
String messageToSend = "你好,服务器!";
out.println(messageToSend);
System.out.println("已发送消息: " + messageToSend);
// 2. 从服务器接收数据
String responseFromServer = in.readLine();
System.out.println("收到服务器响应: " + responseFromServer);
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + serverHost);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 发生错误: " + e.getMessage());
// 这通常意味着服务器没有运行,或者连接被拒绝
e.printStackTrace();
}
System.out.println("客户端通信结束。");
}
}
代码解释:

new Socket(serverHost, serverPort):这是连接服务器的核心,它会阻塞当前线程,直到连接成功或失败。new PrintWriter(socket.getOutputStream(), true):socket.getOutputStream()获取字节输出流。PrintWriter是一个字符流包装器,方便我们使用println()方法发送字符串。- 第二个参数
true表示自动刷新,每当调用println()或printf()时,缓冲区的内容会立即发送出去,这对于实时通信非常重要。
new BufferedReader(new InputStreamReader(socket.getInputStream())):socket.getInputStream()获取字节输入流。InputStreamReader将字节流转换为字符流。BufferedReader提供了readLine()方法,可以方便地按行读取服务器发来的字符串。
try-with-resources:这是 Java 7 引入的语法,可以确保Socket、PrintWriter和BufferedReader在代码块执行完毕后自动关闭,即使发生异常也是如此,这是一种非常好的实践,可以避免资源泄漏。
一个完整的可运行示例(客户端 + 服务器)
为了让上面的客户端代码能运行起来,你需要一个对应的服务器,下面是一个简单的回显服务器,它会接收客户端的消息并原样返回。
服务器代码 (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 = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已在端口 " + port + " 上启动,等待客户端连接...");
// accept() 方法会阻塞,直到有客户端连接
try (Socket clientSocket = serverSocket.accept();
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
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());
e.printStackTrace();
}
System.out.println("服务器已关闭。");
}
}
如何运行:
- 先运行服务器:
java EchoServer- 控制台会输出:
服务器已在端口 8888 上启动,等待客户端连接...
- 控制台会输出:
- 再运行客户端:
java SimpleClient- 客户端控制台会输出连接和通信过程。
- 服务器控制台会显示连接信息和收到的消息。
进阶要点与最佳实践
1 多线程处理
上面的 EchoServer 一次只能处理一个客户端,如果需要同时处理多个客户端,就必须使用多线程。
改进后的多线程服务器 (MultiThreadEchoServer.java)
import java.io.*;
import java.net.*;
public class MultiThreadEchoServer implements Runnable {
private final Socket clientSocket;
public MultiThreadEchoServer(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
System.out.println("处理新客户端连接: " + clientSocket.getInetAddress());
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
out.println("服务器回响: " + inputLine);
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
System.err.println("处理客户端时出错: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客户端 " + clientSocket.getInetAddress() + " 已断开。");
}
}
public static void main(String[] args) throws IOException {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已在端口 " + port + " 上启动。");
while (true) {
Socket clientSocket = serverSocket.accept();
// 为每个新客户端连接创建一个新线程
new Thread(new MultiThreadEchoServer(clientSocket)).start();
}
}
}
}
2 处理二进制数据(如图片、文件)
如果需要传输非文本数据,应该使用字节流。
- 发送方:使用
socket.getOutputStream()直接写入字节数组。 - 接收方:使用
socket.getInputStream()直接读取字节数组。
示例:发送文件
// 客户端发送文件
File file = new File("example.jpg");
try (FileInputStream fis = new FileInputStream(file);
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream os = socket.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead); // 写入Socket输出流
}
System.out.println("文件发送完成。");
} catch (IOException e) {
e.printStackTrace();
}
注意:直接传输字节流有一个问题,接收方不知道文件何时结束,更健壮的做法是先发送文件名和文件长度,然后再发送文件内容。
3 使用 try-with-resources
再次强调,始终使用 try-with-resources 来管理 Socket、InputStream 和 OutputStream,这是防止资源泄漏最简单、最可靠的方法。
4 设置超时
如果服务器长时间没有响应,客户端可能会无限期地阻塞,可以使用 socket.setSoTimeout() 来设置一个超时时间。
Socket socket = new Socket("127.0.0.1", 8888);
socket.setSoTimeout(5000); // 设置5秒超时
// 如果接下来的 read() 操作在5秒内没有数据返回,会抛出 SocketTimeoutException
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String response = in.readLine(); // 可能会阻塞
常见问题与错误排查
-
Connection refused: connect- 原因:最常见的错误,表示客户端无法连接到指定的 IP 和端口。
- 排查:
- 服务器程序是否已经启动?
- 服务器的 IP 地址和端口号是否正确?
- 防火墙是否阻止了该端口的连接?
-
UnknownHostException- 原因:无法将主机名(如
www.google.com)解析为 IP 地址。 - 排查:检查主机名是否拼写错误,或者网络是否可以正常解析域名。
- 原因:无法将主机名(如
-
客户端发送了数据,但服务器没有收到
- 原因:通常是
PrintWriter没有开启自动刷新(new PrintWriter(..., false)),并且没有手动调用flush()方法。 - 排查:确保创建
PrintWriter时第二个参数为true,或者在发送关键数据后调用out.flush()。
- 原因:通常是
-
readLine()返回null- 原因:对于
BufferedReader.readLine(),当流结束时(即客户端或服务器关闭了连接),它会返回null。 - 排查:在
while ((line = in.readLine()) != null)循环中,当收到null时,就应该认为连接已断开,并关闭相应的资源。
- 原因:对于
Java Socket 客户端通信的核心流程非常清晰:创建 Socket -> 获取流 -> 收发数据 -> 关闭资源。
- 对于初学者,从简单的文本通信开始,理解
Socket、InputStream和OutputStream的基本用法。 - 对于实际应用,务必采用多线程服务器来处理并发请求,并使用
try-with-resources来确保资源安全释放。 - 根据数据类型(文本/二进制)选择合适的流(
Reader/Writer或Stream)。 - 善用
try-catch块来处理网络中可能出现的各种异常。
