Java 提供了 java.net 包来支持网络编程,其中核心类包括:

ServerSocket: 用于服务器端,监听客户端的连接请求。Socket: 用于客户端,或者服务器端与客户端建立连接后,用于双向通信。
下面我将通过一个完整的、可运行的 “回显服务器”(Echo Server) 和 客户端 的例子,来逐步讲解 TCP 编程的流程。
核心概念
-
服务器端:
- 创建一个
ServerSocket并绑定到一个特定的端口(8888),开始监听客户端连接。 - 调用
accept()方法,这是一个阻塞方法,它会一直等待,直到有客户端连接上来,一旦有客户端连接,accept()方法会返回一个Socket对象,这个Socket代表了与那个客户端的连接通道。 - 通过这个
Socket对象的getInputStream()和getOutputStream()获取输入流和输出流,用于读取客户端发送的数据和向客户端发送数据。 - 通信结束后,关闭
Socket和ServerSocket。
- 创建一个
-
客户端:
- 创建一个
Socket对象,指定服务器的 IP 地址和端口号,这会尝试连接到服务器,Socket的构造方法也是阻塞的,直到连接成功或失败。 - 连接成功后,同样通过
getInputStream()和getOutputStream()获取输入流和输出流。 - 通过输出流向服务器发送数据,通过输入流读取服务器返回的数据。
- 通信结束后,关闭
Socket。
- 创建一个
服务器端代码
这个服务器会接收客户端发送的任何字符串,然后原封不动地发送回去。

// EchoServer.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer {
public static void main(String[] args) {
int port = 8888; // 服务器监听的端口号
// try-with-resources 语句,可以自动关闭资源
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,等待客户端连接...");
System.out.println("服务器地址: " + InetAddress.getLocalHost().getHostAddress() + ":" + port);
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 获取输入流,用于读取客户端发送的数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 获取输出流,用于向客户端发送数据
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // autoFlush=true
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("服务器已关闭。");
}
}
代码解释:
ServerSocket serverSocket = new ServerSocket(port): 在指定端口创建服务器套接字。serverSocket.accept(): 阻塞,等待客户端连接,返回一个Socket对象,代表与客户端的连接。new BufferedReader(new InputStreamReader(clientSocket.getInputStream())): 从Socket的输入流中读取数据。InputStreamReader将字节流转换为字符流,BufferedReader提供了高效的按行读取方法 (readLine())。new PrintWriter(clientSocket.getOutputStream(), true): 从Socket的输出流中写入数据。PrintWriter提供了方便的打印方法(如println()),第二个参数true表示自动刷新,即每次调用println()后都会自动调用flush(),确保数据被立即发送。while ((inputLine = in.readLine()) != null): 循环读取客户端发送的每一行文本,当客户端关闭连接时,readLine()会返回null,从而退出循环。
客户端代码
这个客户端可以连接到服务器,从控制台读取用户输入,发送给服务器,并打印服务器的回显。
// 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 hostname = "localhost"; // 服务器地址,本地运行用 "localhost"
int port = 8888; // 服务器端口
// try-with-resources 语句,可以自动关闭资源
try (
Socket socket = new Socket(hostname, port);
// 从标准输入读取用户输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
// 从Socket获取输入流,读取服务器返回的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 从Socket获取输出流,向服务器发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
System.out.println("已连接到服务器 " + hostname + ":" + port);
System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
String userInput;
// 循环读取用户输入
while ((userInput = stdIn.readLine()) != null) {
// 将用户输入发送给服务器
out.println(userInput);
// 从服务器读取回显
String response = in.readLine();
System.out.println("服务器回显: " + response);
// 如果用户输入 "bye",则退出循环
if ("bye".equalsIgnoreCase(userInput)) {
break;
}
System.out.println("请输入下一行消息 (输入 'bye' 退出):");
}
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O Error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
代码解释:
new Socket(hostname, port): 尝试连接到指定主机和端口,这是一个阻塞操作,直到连接建立或抛出异常。stdIn = new BufferedReader(new InputStreamReader(System.in)): 创建一个BufferedReader来读取用户在控制台的输入。in = new BufferedReader(new InputStreamReader(socket.getInputStream())): 读取服务器返回的数据。out = new PrintWriter(socket.getOutputStream(), true): 向服务器发送数据。while ((userInput = stdIn.readLine()) != null): 循环读取用户输入,并发送给服务器,然后读取并打印服务器的响应。
如何运行
-
编译代码:
(图片来源网络,侵删)javac EchoServer.java EchoClient.java
-
启动服务器: 打开一个终端窗口,运行服务器,它会进入阻塞状态,等待连接。
java EchoServer
你会看到输出:
服务器已启动,等待客户端连接... 服务器地址: 127.0.0.1:8888 -
启动客户端: 打开另一个终端窗口,运行客户端。
java EchoClient
你会看到输出:
已连接到服务器 localhost:8888 请输入要发送的消息 (输入 'bye' 退出): -
进行通信:
- 在客户端的终端输入
Hello Server,然后按回车。 - 客户端会显示:
服务器回显: Hello Server - 服务器端的终端会显示:
客户端已连接: 127.0.0.1 收到客户端消息: Hello Server - 继续输入更多消息进行测试。
- 在客户端的终端输入
-
关闭连接:
- 在客户端输入
bye,然后按回车。 - 客户端和服务器都会打印退出信息,并各自关闭程序。
- 在客户端输入
重要进阶概念
1 多线程处理并发连接
上面的服务器一次只能处理一个客户端连接,当 accept() 阻塞时,它无法为新的客户端服务,为了处理多个并发客户端,服务器需要为每个客户端连接创建一个独立的线程。
改进后的多线程服务器代码:
// MultiThreadEchoServer.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 MultiThreadEchoServer {
public static void main(String[] args) {
int port = 8888;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,等待客户端连接...");
while (true) { // 无限循环,持续接受新连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端连接创建一个新的线程来处理
ClientHandler clientHandler = new ClientHandler(clientSocket);
new Thread(clientHandler).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-with-resources 确保流被关闭
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);
if ("bye".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求断开连接。");
break;
}
}
} catch (IOException e) {
System.err.println("处理客户端时出错: " + e.getMessage());
} finally {
try {
clientSocket.close();
System.out.println("与客户端的连接已关闭。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这个版本的服务器主线程只负责 accept() 新连接,然后将每个连接交给一个 ClientHandler 线程去处理,从而实现了并发。
2 异步 I/O (NIO - New I/O)
对于需要处理成千上万个并发连接的高性能场景,传统的阻塞式 I/O 和多线程模型(一个线程连接)会消耗大量资源,Java NIO 提供了非阻塞的 I/O 模型,它使用选择器 和通道,可以用一个或几个线程来管理成千上万个连接,大大提高了系统的可伸缩性和性能,这是一个更高级的话题,建议在掌握了 BIO 之后进行学习。
| 特性 | 服务器端 | 客户端 |
|---|---|---|
| 创建套接字 | ServerSocket(port) |
Socket(host, port) |
| 等待/建立连接 | serverSocket.accept() (阻塞) |
new Socket() (阻塞) |
| 获取 I/O 流 | socket.getInputStream() / getOutputStream() |
socket.getInputStream() / getOutputStream() |
| 关闭资源 | 关闭 Socket 和 ServerSocket |
关闭 Socket |
这个例子涵盖了 Java TCP Socket 编程的核心知识点,从基础的单连接模型到实用的多线程模型,希望能帮助你快速上手。
