- 核心概念:简要介绍 Socket、IP 地址和端口号。
- 服务器端实现:如何创建服务器,监听客户端连接,并处理消息。
- 客户端实现:如何连接服务器,发送消息,并接收服务器转发的消息。
- 完整代码:提供服务器和客户端的完整、可运行的代码。
- 如何运行:指导你如何编译和运行这两个程序。
- 进阶与扩展:讨论如何改进这个基础版本,使其更健壮和功能更强大。
核心概念
- Socket (套接字):是网络通信的端点,你可以把它想象成一个电话机,要打电话,你需要一个电话机(Socket),一个对方的电话号码(IP 地址)和一个分机号(端口号),通过 Socket,程序可以在网络上发送和接收数据流。
- IP 地址:网络上设备的唯一地址,
0.0.1(代表本机)或168.1.100。 - 端口号:IP 地址唯一标识一台计算机,但一台计算机上可能同时运行多个网络程序(如浏览器、聊天软件等),端口号用于区分这些不同的程序,范围是 0 到 65535,0 到 1023 是系统保留端口。
- TCP vs UDP:Socket 编程主要基于两种协议。
- TCP (Transmission Control Protocol):面向连接、可靠的协议,数据在传输前会先建立连接,确保数据按顺序、无丢失地到达,我们的聊天室将使用 TCP。
- UDP (User Datagram Protocol):无连接、不可靠的协议,发送方直接把数据包发出去,不保证对方一定能收到,适用于视频、游戏等对实时性要求高但对少量丢包不敏感的场景。
服务器端实现
服务器的主要任务是:

- 在一个特定的端口上等待客户端的连接请求。
- 当一个客户端连接时,为该客户端创建一个新的
Socket和一个独立的线程,专门负责与这个客户端通信。 - 将所有连接的客户端都管理起来,当收到某个客户端的消息时,将消息广播给所有其他客户端。
服务器端代码 (Server.java)
import java.io.*;
import java.net.*;
import java.util.*;
// 每个客户端连接的处理线程
class ClientHandler extends Thread {
private Socket clientSocket;
private PrintWriter out;
private BufferedReader in;
private String username;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
public void run() {
try {
// 获取输出流,用于向客户端发送消息
out = new PrintWriter(clientSocket.getOutputStream(), true);
// 获取输入流,用于接收客户端的消息
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 1. 获取用户名
username = in.readLine();
System.out.println(username + " 已加入聊天室。");
Server.broadcastMessage(username + " 加入了聊天室。");
String inputLine;
// 2. 持续读取客户端发送的消息
while ((inputLine = in.readLine()) != null) {
if ("exit".equalsIgnoreCase(inputLine)) {
break; // 客户端退出
}
// 广播消息给所有客户端
Server.broadcastMessage(username + ": " + inputLine);
}
} catch (IOException e) {
System.out.println("与客户端 " + username + " 的连接出现错误: " + e.getMessage());
} finally {
// 3. 客户端断开连接后的清理工作
try {
if (username != null) {
System.out.println(username + " 已离开聊天室。");
Server.broadcastMessage(username + " 离开了聊天室。");
}
Server.removeClient(this);
in.close();
out.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 向当前客户端发送消息的方法
public void sendMessage(String message) {
out.println(message);
}
}
// 服务器主类
public class Server {
private static final int PORT = 12345;
private static Set<ClientHandler> clients = new HashSet<>();
public static void main(String[] args) {
System.out.println("聊天室服务器启动中...");
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
System.out.println("等待客户端连接...");
Socket clientSocket = serverSocket.accept(); // 阻塞,等待客户端连接
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个新创建一个线程来处理客户端
ClientHandler clientThread = new ClientHandler(clientSocket);
clients.add(clientThread);
clientThread.start();
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
// 广播消息给所有客户端
public static void broadcastMessage(String message) {
for (ClientHandler client : clients) {
client.sendMessage(message);
}
}
// 移除离线的客户端
public static void removeClient(ClientHandler client) {
clients.remove(client);
}
}
客户端实现
客户端的主要任务是:
- 知道服务器的 IP 地址和端口号。
- 尝试连接到服务器。
- 创建一个线程专门用于接收来自服务器的消息。
- 通过主线程(或另一个线程)从控制台读取用户输入,并发送给服务器。
客户端代码 (Client.java)
import java.io.*;
import java.net.*;
import java.util.Scanner;
// 客户端用于接收服务器消息的线程
class ReceiveMessageThread extends Thread {
private BufferedReader in;
public ReceiveMessageThread(BufferedReader in) {
this.in = in;
}
public void run() {
try {
String serverMessage;
// 持续读取服务器发来的消息并打印
while ((serverMessage = in.readLine()) != null) {
System.out.println(serverMessage);
}
} catch (IOException e) {
System.err.println("接收消息时出错: " + e.getMessage());
}
}
}
// 客户端主类
public class Client {
private static final String SERVER_IP = "127.0.0.1"; // 本机地址
private static final int PORT = 12345;
public static void main(String[] args) {
try (
// 创建 Socket 连接服务器
Socket socket = new Socket(SERVER_IP, 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("成功连接到服务器!");
System.out.print("请输入您的用户名: ");
String username = scanner.nextLine();
// 首先发送用户名给服务器
out.println(username);
// 启动一个专门的线程来接收服务器消息
ReceiveMessageThread receiveThread = new ReceiveMessageThread(in);
receiveThread.start();
// 主线程负责读取用户输入并发送给服务器
String userInput;
while (true) {
userInput = scanner.nextLine();
out.println(userInput);
if ("exit".equalsIgnoreCase(userInput)) {
break; // 用户输入 exit,则退出循环
}
}
} catch (UnknownHostException e) {
System.err.println("无法找到服务器: " + e.getMessage());
} catch (IOException e) {
System.err.println("I/O Error: " + e.getMessage());
}
System.out.println("客户端已关闭。");
}
}
完整代码
将上述两个代码块分别保存为 Server.java 和 Client.java 文件。
如何运行
-
编译代码: 打开你的终端或命令提示符,进入代码所在的目录,运行以下命令:
javac Server.java Client.java
这会生成
Server.class和Client.class文件。
(图片来源网络,侵删) -
启动服务器: 在同一个终端中,先运行服务器程序:
java Server
你会看到控制台输出:
聊天室服务器启动中... 等待客户端连接... -
启动客户端: 打开一个新的终端窗口(不要关闭服务器的终端),在新的终端中运行客户端程序:
java Client
你会看到:
(图片来源网络,侵删)成功连接到服务器! 请输入您的用户名: Alice在服务器终端,你会看到:
Alice 已加入聊天室。 -
开始聊天: 在
Alice的客户端输入消息,大家好!,然后按回车。 在Alice的客户端会看到:Alice: 大家好!在服务器终端会看到:
Alice: 大家好! -
启动第二个客户端: 再打开第三个终端窗口,再次运行
java Client。成功连接到服务器! 请输入您的用户名: Bob在
Alice和Bob的客户端,都会看到Bob 加入了聊天室。的消息。 -
测试聊天和退出:
Bob输入很高兴认识你,Alice和Bob都会看到Bob: 很高兴认识你。Alice输入我也是,Alice和Bob都会看到Alice: 我也是。Alice输入exit并回车,Alice的客户端会关闭。Bob的客户端会看到Alice 离开了聊天室。的消息。
进阶与扩展
这个基础版本已经实现了核心功能,但还有很大的改进空间:
-
使用
ExecutorService管理线程:直接创建新线程 (new Thread()) 对于大量客户端连接来说效率低下,可以使用ExecutorService线程池来管理客户端处理线程,避免频繁创建和销毁线程的开销。 -
处理异常和断开连接:当前的代码在客户端突然关闭(如直接关闭终端)时,服务器可能会抛出
IOException,但finally块中的清理逻辑可以保证资源被释放,可以进一步优化,让服务器能更优雅地处理客户端的意外断开。 -
使用
NIO (New I/O):传统的Socket编程是基于阻塞 I/O 的,一个线程只能处理一个连接,对于高并发的场景,可以使用 Java NIO 中的Selector、Channel和Buffer,实现一个线程管理多个连接,大幅提升服务器的性能和并发能力。 -
图形用户界面:目前的程序是基于命令行的,你可以使用 Java Swing 或 JavaFX 为客户端和服务器创建图形界面,使其更易用。
-
私聊功能:可以在消息格式上增加约定,
@Bob 你好,表示给Bob发送私聊消息,服务器在解析消息后,只将消息转发给指定的Bob客户端。 -
文件传输:可以扩展协议,增加文件传输的功能,客户端可以将文件通过 Socket 发送给服务器,服务器再转发给指定的其他客户端。
希望这个详细的教程能帮助你理解 Java Socket 编程的精髓!动手实践是最好的学习方式。
