- 核心概念:简要介绍 Socket 编程的基本原理。
- 项目结构:说明我们将创建哪些类。
- 服务器端代码:创建服务器,监听客户端连接。
- 客户端代码:创建客户端,连接服务器并进行聊天。
- 如何运行:分步指导你编译和运行程序。
- 代码分析与改进方向:解释关键代码,并指出可以如何扩展功能。
核心概念
Socket(套接字)是网络通信的端点,要实现两个程序之间的通信,每个程序都需要一个 Socket。

-
服务器端:
- 创建一个
ServerSocket并绑定到一个特定的端口号,开始监听客户端的连接请求。 - 当一个客户端请求连接时,
ServerSocket会接受这个请求,并返回一个新的Socket对象,专门用于与这个客户端通信。 - 服务器通常会为每个客户端连接创建一个新的线程,以便能够同时处理多个客户端的请求,而不会阻塞其他客户端。
- 创建一个
-
客户端:
- 创建一个
Socket对象,指定服务器的 IP 地址和端口号,向服务器发起连接请求。 - 如果连接成功,客户端就拥有了一个与服务器通信的
Socket。
- 创建一个
-
通信流程:
- 每个
Socket都有一个输入流和一个输出流。 - 发送数据:通过
Socket的输出流(OutputStream)将数据写入,发送给对方。 - 接收数据:通过
Socket的输入流(InputStream)读取对方发送过来的数据。 - 在 Java 中,我们通常会使用
InputStreamReader和OutputStreamWriter来处理字节流和字符流之间的转换,并用BufferedReader和PrintWriter来包装它们,以便进行高效的行读写。
- 每个
项目结构
为了清晰,我们将创建四个 Java 文件:

ChatApp/
├── Server.java // 服务器主程序
├── Client.java // 客户端主程序
├── ClientHandler.java // 服务器为每个客户端创建的处理线程
└── MessageListener.java // 客户端用于接收服务器消息的线程
服务器端代码
服务器负责监听、接受连接,并为每个连接启动一个独立的处理线程。
Server.java
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
private static final int PORT = 12345;
// 使用一个静态列表来存储所有连接的客户端处理器,以便广播消息
private static List<ClientHandler> clients = new ArrayList<>();
// 使用线程池来管理客户端连接线程
private static ExecutorService pool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器已启动,等待客户端连接...");
while (true) {
// accept() 是一个阻塞方法,等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个新客户端创建一个处理器
ClientHandler clientHandler = new ClientHandler(clientSocket, clients);
// 将新客户端添加到列表中
clients.add(clientHandler);
// 将客户端处理任务提交到线程池
pool.execute(clientHandler);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭线程池
pool.shutdown();
}
}
}
ClientHandler.java
这个类代表服务器与单个客户端的通信逻辑,它运行在一个独立的线程中。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.List;
public class ClientHandler implements Runnable {
private Socket clientSocket;
private List<ClientHandler> clients;
private PrintWriter out;
private BufferedReader in;
private String clientUsername;
public ClientHandler(Socket socket, List<ClientHandler> clients) {
this.clientSocket = socket;
this.clients = clients;
}
@Override
public void run() {
try {
// 获取输入输出流
out = new PrintWriter(clientSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 读取客户端发送的用户名
clientUsername = in.readLine();
System.out.println(clientUsername + " 已加入聊天室。");
// 广播新用户加入的消息
broadcastMessage("[系统] " + clientUsername + " 已加入聊天室。");
String inputLine;
// 循环读取客户端发送的消息
while ((inputLine = in.readLine()) != null) {
System.out.println("收到来自 " + clientUsername + " 的消息: " + inputLine);
// 广播消息给所有客户端
broadcastMessage(clientUsername + ": " + inputLine);
}
} catch (IOException e) {
// 如果客户端断开连接,会抛出异常
System.out.println(clientUsername + " 已断开连接。");
} finally {
// 清理资源
try {
if (in != null) in.close();
if (out != null) out.close();
if (clientSocket != null) clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
// 从客户端列表中移除
clients.remove(this);
// 广播用户离开的消息
broadcastMessage("[系统] " + clientUsername + " 已离开聊天室。");
}
}
// 广播消息给所有客户端
public void broadcastMessage(String message) {
for (ClientHandler client : clients) {
client.out.println(message);
}
}
}
客户端代码
客户端需要两个线程:一个用于发送消息(主线程),另一个用于接收消息(MessageListener 线程)。
Client.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 Client {
private static final String SERVER_ADDRESS = "127.0.0.1"; // 本地地址
private static final int SERVER_PORT = 12345;
public static void main(String[] args) {
try (
// 创建 Socket 连接服务器
Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
// 用于向服务器发送消息
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 用于从服务器读取消息
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 用于从控制台读取用户输入
Scanner scanner = new Scanner(System.in);
) {
System.out.println("成功连接到服务器!");
System.out.print("请输入您的用户名: ");
String username = scanner.nextLine();
// 首先发送用户名给服务器
out.println(username);
// 启动一个线程来专门监听服务器发来的消息
MessageListener listener = new MessageListener(in);
new Thread(listener).start();
// 主线程用于读取用户输入并发送
System.out.println("您现在可以开始聊天了,输入 'exit' 退出。");
String userInput;
while (true) {
userInput = scanner.nextLine();
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
out.println(userInput);
}
} catch (UnknownHostException e) {
System.err.println("不知道服务器: " + SERVER_ADDRESS);
e.printStackTrace();
} catch (IOException e) {
System.err.println("无法连接到服务器 " + SERVER_ADDRESS + " 端口 " + SERVER_PORT);
e.printStackTrace();
}
}
}
MessageListener.java
这个线程负责持续监听并打印从服务器接收到的消息,避免阻塞主线程(发送消息的线程)。

import java.io.BufferedReader;
public class MessageListener implements Runnable {
private BufferedReader in;
public MessageListener(BufferedReader in) {
this.in = in;
}
@Override
public void run() {
try {
String serverMessage;
// 循环读取服务器发来的消息
while ((serverMessage = in.readLine()) != null) {
// 直接在控制台打印服务器消息
System.out.println(serverMessage);
}
} catch (IOException e) {
// 如果连接断开,会抛出异常
System.out.println("与服务器连接已断开。");
}
}
}
如何运行
-
保存文件:将上面的四个 Java 文件(
Server.java,ClientHandler.java,Client.java,MessageListener.java)保存在同一个目录下。 -
编译代码:打开终端或命令提示符,进入到该目录,然后运行
javac命令编译所有.java文件。javac *.java
-
启动服务器:在同一个终端中,首先运行服务器程序。
java Server
你会看到输出:
服务器已启动,等待客户端连接... -
启动客户端:打开另一个新的终端(非常重要,不要关闭服务器的终端),然后运行客户端程序。
java Client
你会看到:
成功连接到服务器!,然后提示你输入用户名。 -
开始聊天:
- 在第一个客户端输入用户名("Alice")并回车。
- 打开第三个终端,再启动一个客户端
java Client,输入另一个用户名("Bob")并回车。 - 你可以在任意一个客户端的终端中输入消息,消息会广播给所有其他客户端(包括服务器控制台)。
-
退出:在任一客户端输入
exit并回车,该客户端会正常退出,服务器会收到该用户离开的通知。
代码分析与改进方向
关键点分析
- 多线程:服务器端为每个客户端创建一个
ClientHandler线程,这是实现“多人同时聊天”的核心,客户端使用一个独立的MessageListener线程来接收消息,使得发送和接收可以同时进行,互不干扰。 - 阻塞式 I/O:
in.readLine()是一个阻塞方法,在ClientHandler和MessageListener中,它们都在循环中调用此方法,等待数据,这简化了代码,但如果一个客户端长时间不发送消息,它会一直占用一个线程。 - 广播机制:服务器维护一个
List<ClientHandler>,当收到一个消息时,遍历这个列表,将消息通过每个ClientHandler的PrintWriter发送给对应的客户端。 - 流的使用:
PrintWriter: 用于方便地发送字符串,并带有自动刷新功能(true参数)。BufferedReader: 用于高效地按行读取字符串。Scanner: 用于方便地从控制台读取用户输入。
改进与扩展方向
这是一个非常基础的聊天程序,你可以尝试以下改进来增加功能:
- 图形用户界面:使用 Java Swing 或 JavaFX 为客户端和服务器创建图形界面,使其更易用。
- 私聊功能:在消息前加上目标用户名,
@Bob 你好,服务器解析后只将消息发送给 Bob。 - 处理异常用户:增加一个简单的机制来处理恶意发送数据的用户,例如踢出聊天室。
- 使用 NIO:对于更高性能的场景,可以使用 Java NIO (New I/O) 的
Selector和Channel来实现非阻塞 I/O,用更少的线程处理成千上万的连接。 - 增加文件传输:扩展协议,允许客户端发送文件。
- 加密通信:使用 SSL/TLS 为 Socket 连接加密,防止消息被窃听,这可以通过
SSLSocket和SSLServerSocket来实现。 - 更优雅的退出:改进
exit命令,使其能通知服务器,服务器再通知所有其他用户。
希望这个详细的教程能帮助你理解 Java Socket 编程并成功实现一个聊天程序!
