核心概念与流程
TCP 通信就像打电话,必须先建立连接,然后才能互相通话,最后挂断连接。

两个核心角色
- 客户端:主动发起连接请求的一方,它需要知道服务器的 IP 地址和端口号。
- 服务器端:被动等待连接请求的一方,它需要绑定一个特定的端口号,并在此端口上监听客户端的连接。
通信流程
一个完整的 TCP 通信过程如下:
-
服务器端启动:
- 创建一个
ServerSocket实例,并绑定到一个具体的端口号。 - 调用
ServerSocket的accept()方法,该方法会阻塞线程,等待客户端的连接请求。 - 当一个客户端连接成功时,
accept()方法返回一个新的Socket实例,这个Socket代表了与该客户端的专用连接通道。
- 创建一个
-
客户端启动:
- 创建一个
Socket实例,同时指定服务器的 IP 地址和端口号。 - Java 会尝试通过三次握手与服务器建立连接,如果成功,
Socket构造函数会返回;如果失败(如服务器未启动或端口错误),则会抛出异常。
- 创建一个
-
数据传输:
(图片来源网络,侵删)- 一旦连接建立,客户端和服务器端的
Socket对象就可以通过 输入流 和 输出流 进行双向数据传输。 - 客户端通过
Socket.getOutputStream()获取输出流,向服务器发送数据。 - 客户端通过
Socket.getInputStream()获取输入流,读取服务器发来的数据。 - 服务器端通过
Socket.getOutputStream()获取输出流,向客户端发送数据。 - 服务器端通过
Socket.getInputStream()获取输入流,读取客户端发来的数据。
- 一旦连接建立,客户端和服务器端的
-
关闭连接:
通信结束后,客户端和服务器端都需要关闭各自的 Socket 和相关的输入/输出流,关闭的顺序通常是:先关闭流,再关闭 Socket。
代码示例
下面是一个经典的“Echo Server”(回显服务器)的完整实现,客户端发送任何消息,服务器都会原样返回。
服务器端代码
// EchoServer.java
import java.io.*;
import java.net.*;
public class EchoServer {
public static void main(String[] args) {
// 定义服务器监听的端口号
int port = 8888;
try ( // 使用 try-with-resources 语句,确保资源自动关闭
// 1. 创建 ServerSocket,并绑定到指定端口
ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 2. 调用 accept() 方法,阻塞等待客户端连接
// 当有客户端连接时,accept() 返回一个 Socket 对象,代表与该客户端的连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
try (
// 3. 获取客户端的输入流,用于读取客户端发送的数据
InputStream inputStream = clientSocket.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
// 4. 获取客户端的输出流,用于向客户端发送数据
OutputStream outputStream = clientSocket.getOutputStream();
PrintWriter out = new PrintWriter(outputStream, true)) { // autoFlush=true
String inputLine;
// 5. 循环读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 6. 将收到的消息回显给客户端
out.println("服务器回显: " + inputLine);
}
System.out.println("客户端已断开连接。");
} // 内部的 try-with-resources 会自动关闭 in 和 out
// clientSocket 会在外层 try-with-resources 结束时关闭
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
客户端代码
// EchoClient.java
import java.io.*;
import java.net.*;
public class EchoClient {
public static void main(String[] args) {
// 服务器的 IP 地址(本地回环地址)和端口号
String host = "127.0.0.1";
int port = 8888;
try ( // 使用 try-with-resources 语句,确保资源自动关闭
// 1. 创建 Socket 对象,尝试连接到服务器
Socket socket = new Socket(host, port);
// 2. 获取服务器的输出流,用于向服务器发送数据
OutputStream outputStream = socket.getOutputStream();
PrintWriter out = new PrintWriter(outputStream, true); // autoFlush=true
// 3. 获取服务器的输入流,用于读取服务器返回的数据
InputStream inputStream = socket.getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) {
System.out.println("已连接到服务器 " + host + ":" + port);
// 创建一个控制台读取器,用于从键盘读取用户输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
String userInput;
// 4. 循环读取用户输入并发送给服务器
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
while ((userInput = stdIn.readLine()) != null) {
if ("exit".equalsIgnoreCase(userInput)) {
break; // 输入 'exit' 则退出循环
}
// 发送消息到服务器
out.println(userInput);
// 读取并打印服务器回显的消息
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();
}
}
}
关键点解析
try-with-resources 语句
在上述代码中,我们使用了 try-with-resources (Java 7+ 特性),只要一个类实现了 AutoCloseable 接口(Socket, ServerSocket, InputStream, OutputStream, Reader, Writer 等都实现了),就可以放在 try 后面的括号里,这样,无论 try 块是正常结束还是抛出异常,JVM 都会自动调用这些资源的 close() 方法,避免了资源泄漏。

InputStream / OutputStream vs. Reader / Writer
- 字节流 (
InputStream/OutputStream):处理原始的二进制数据(图片、视频、文件等),它们是面向字节的。 - 字符流 (
Reader/Writer):处理字符数据(文本),它们是面向字符的,并且可以处理字符编码。
在 TCP Socket 编程中,底层传输的是字节流,当我们想传输文本时,通常需要进行“桥接”:
- 用
InputStreamReader将InputStream(字节流) 包装成Reader(字符流)。 - 用
OutputStreamWriter将OutputStream(字节流) 包装成Writer(字符流)。
为了方便操作文本行,我们通常会进一步使用 BufferedReader 和 PrintWriter。
阻塞方法
ServerSocket.accept():服务器端阻塞,它会一直等待,直到有客户端连接。Socket.getInputStream().read():客户端和服务器端都阻塞,它会一直等待,直到从网络中读取到数据。BufferedReader.readLine():阻塞,它会一直等待,直到读取到一行完整的文本(以\n,\r, 或\r\n。
理解这些阻塞行为对于编写多线程网络程序至关重要。
半关闭
TCP 连接是全双工的,意味着可以双向独立关闭。Socket.close() 会同时关闭输入流和输出流,但有时我们可能只想关闭一个方向,
- 客户端发送完所有数据后,想告诉服务器“我没有数据要发了,但你还可以继续发给我”,这时可以调用
socket.shutdownOutput()。 - 服务器收到这个信号后,在
read()方法时会读到-1,从而知道客户端已经发送完毕。
进阶与最佳实践
处理多个客户端
上面的服务器一次只能处理一个客户端,因为它在 accept() 之后是同步等待数据收发的,在实际应用中,服务器需要能够同时为多个客户端服务,这通常通过多线程实现。
改进的服务器端(多线程版):
// MultiThreadEchoServer.java
import java.io.*;
import java.net.*;
public class MultiThreadEchoServer {
public static void main(String[] args) {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,正在监听端口 " + port + "...");
while (true) { // 循环接受所有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新的线程来处理
ClientHandler handler = new ClientHandler(clientSocket);
new Thread(handler).start();
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
}
}
}
// 客户端处理任务
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("[" + Thread.currentThread().getName() + "] 收到客户端消息: " + inputLine);
out.println("服务器回显: " + inputLine);
}
} catch (IOException e) {
// 如果客户端断开连接,会抛出 SocketException,这是正常行为
System.out.println("客户端 " + clientSocket.getInetAddress() + " 已断开连接。");
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用 NIO (New I/O) 提高性能
对于高并发的场景,为每个连接创建一个线程会消耗大量系统资源,Java NIO (Non-blocking I/O) 提供了一种更高效的方式,它使用 通道 和 选择器,允许一个线程管理多个连接,通过事件驱动模型来处理 I/O 操作,从而大大提高了系统的吞吐量。
NIO 是一个更复杂但更强大的主题,适合构建高性能的网络框架。
序列化对象
如果不想只传输字符串,而是想直接传输 Java 对象,可以使用 ObjectInputStream 和 ObjectOutputStream,被传输的类必须实现 java.io.Serializable 接口。
// 发送对象 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); oos.writeObject(myObject); // 接收对象 ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); MyObject myObject = (MyObject) ois.readObject();
使用现成框架
在实际项目中,直接使用原生 Socket 编写复杂的网络应用是比较繁琐的,通常会使用成熟的网络框架,它们封装了底层的细节,提供了更高级的功能,如:
- Netty: 异步事件驱动的网络应用框架,性能极高,是目前最流行的 Java NIO 框架。
- Mina: 另一个流行的 NIO 框架。
- Apache Commons Net: 提供了一些客户端和服务器端的实现,相对简单。
希望这份详细的指南能帮助你理解 Java TCP Socket 编程!
