- 核心概念:了解 Socket 和 ServerSocket 的基本作用。
- 通信流程:一个典型的客户端-服务器通信步骤。
- 代码实现:分别提供客户端和服务器端的完整、可运行的 Java 代码。
- 进阶主题:多线程处理、IO 流的选择(BIO vs NIO)。
- 异常处理和资源关闭:非常重要的实践。
核心概念
在 Java 中,Socket 编程主要涉及两个核心类:

java.net.Socket:代表一个客户端,当你想连接到一个服务器时,你需要创建一个Socket对象,它封装了通信的端点,包含了 IP 地址(或域名)和端口号。java.net.ServerSocket:代表一个服务器端,服务器用它来“监听”某个特定的端口,等待客户端的连接请求,当一个客户端连接成功时,ServerSocket会返回一个新的Socket对象,专门用于与这个客户端进行通信。
一个简单的比喻:
ServerSocket就像一家餐厅的前台,它坐在一个固定的位置(端口),等待客人(客户端)到来。Socket就像一位客人,客人知道餐厅的地址(IP)和前台的位置(端口),于是过去敲门。- 当前台(
ServerSocket)听到敲门声,它会指派一位服务员(一个新的Socket)来专门为这位客人服务。 - 客人和服务员之间通过“说”和“听”(输入/输出流)来进行交流。
通信流程
一个典型的 TCP Socket 通信流程如下:
服务器端:
- 创建
ServerSocket实例,并绑定一个端口号。 - 调用
accept()方法,开始阻塞监听,等待客户端连接。 - 当客户端连接成功,
accept()返回一个新的Socket实例。 - 通过这个新的
Socket实例,获取输入流和输出流。 - 通过输入流读取客户端发送的数据,通过输出流向客户端发送数据。
- 通信结束后,关闭相关的流和
Socket。
客户端:

- 创建
Socket实例,指定服务器的 IP 地址和端口号。 - 连接建立后,通过
Socket实例获取输入流和输出流。 - 通过输出流向服务器发送数据,通过输入流读取服务器返回的数据。
- 通信结束后,关闭相关的流和
Socket。
代码实现
下面是一个最简单的 "Echo"(回显)服务器和客户端的例子,客户端发送一句话,服务器原样返回这句话。
服务器端代码 (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 = 12345;
// try-with-resources 语句,可以自动关闭资源
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + 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);
String inputLine;
// 循环读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 将收到的消息回显给客户端
out.println("服务器回显: " + inputLine);
// 如果客户端发送 "bye",则结束通信
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
System.out.println("客户端已断开连接。");
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
客户端代码 (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;
import java.util.Scanner;
public class EchoClient {
public static void main(String[] args) {
// 服务器的 IP 地址(如果是本地测试,可以用 "localhost" 或 "127.0.0.1")
String host = "localhost";
// 服务器监听的端口号,必须与服务器端一致
int port = 12345;
// try-with-resources 语句
try (Socket socket = new Socket(host, port);
// 用于从服务器读取数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 用于向服务器发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 从控制台读取用户输入
Scanner scanner = new Scanner(System.in)) {
System.out.println("已连接到服务器 " + host + ":" + port);
System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
// 循环读取用户输入并发送给服务器
while (true) {
String userInput = scanner.nextLine();
// 发送消息到服务器
out.println(userInput);
// 如果用户输入 "bye",则退出循环
if ("bye".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();
}
System.out.println("客户端已关闭。");
}
}
如何运行
-
启动服务器:
- 编译并运行
EchoServer.java。 - 控制台会显示:
服务器已启动,正在监听端口 12345...,然后它会停在那里等待连接。
- 编译并运行
-
启动客户端:
- 编译并运行
EchoClient.java。 - 客户端控制台会显示:
已连接到服务器 localhost:12345。 - 你可以在客户端输入任何文字,"Hello, Server!",然后按回车。
- 客户端会立即收到服务器的回显:
服务器响应: 服务器回显: Hello, Server! - 当你在客户端输入 "bye" 并回车后,客户端和服务器都会关闭连接,程序结束。
- 编译并运行
进阶主题
1 多线程处理服务器
上面的服务器一次只能处理一个客户端,如果多个客户端同时连接,后面的客户端必须等待前面的客户端断开,为了解决这个问题,服务器需要为每个客户端连接创建一个新的线程来处理。
改进后的服务器端 (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 = 12345;
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());
}
}
}
// 客户端处理任务,实现 Runnable 接口
class ClientHandler implements Runnable {
private 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);
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
// 如果客户端异常断开,这里会抛出异常,是正常现象
System.out.println("客户端 " + clientSocket.getInetAddress() + " 已断开或发生错误。");
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2 IO 流的选择:BIO vs NIO
- BIO (Blocking I/O - 阻塞式IO):上面我们使用的就是 BIO,当调用
read()或accept()时,线程会一直阻塞,直到有数据可读或有连接到来,这会导致线程资源浪费,在传统多线程模型下,高并发时性能会急剧下降。 - NIO (New I/O / Non-blocking I/O - 非阻塞式IO):Java NIO 引入了 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)。
- Channel 类似于 Stream,但可以双向读写。
- Buffer 是数据容器,所有读写都必须通过 Buffer。
- Selector 允许一个线程管理多个 Channel,它会把多个 Channel 注册到 Selector 上,然后在一个循环中查询哪些 Channel 有事件(如连接就绪、数据可读),从而实现非阻塞。
- NIO 适用于高并发场景,能以更少的线程处理更多的连接,性能远超 BIO,但对于初学者来说,理解和使用起来更复杂。
对于初学者和大多数中小型应用,BIO 已经足够,当需要处理成千上万的并发连接时,才需要转向 NIO 或 Netty 等高性能网络框架。
异常处理和资源关闭
这是 Java 编程中至关重要的一点。
-
为什么需要关闭?
- 文件描述符:每个 Socket 连接都会占用一个操作系统的文件描述符,如果不关闭,当连接数增多时,可能会导致文件描述符耗尽,程序无法再创建新的连接。
- 端口占用:服务器端的
ServerSocket会绑定一个端口,如果程序异常退出而没有关闭ServerSocket,这个端口可能会在一段时间内被占用,导致你无法立即重启服务器。
-
如何正确关闭?
try-with-resources语句:这是 Java 7 引入的最佳实践,任何实现了AutoCloseable接口的对象都可以放在try后面的括号里,当代码块执行完毕(无论是正常结束还是发生异常),这些资源都会被自动关闭。- 关闭顺序:应该先关闭高级别的流(
InputStream,OutputStream),再关闭底层的Socket,因为关闭流时,Socket通常也会被自动关闭,但显式地关闭Socket是一个好习惯。 - 多线程中的关闭:在服务器端,为每个客户端服务的线程结束时,应该关闭它所持有的
Socket。
通过遵循以上指南,你应该能够掌握 Java 中基本的 Socket 接口调用,并能够构建简单的网络应用程序。
