構建一個 **局域網廣播聊天/通知系統**:
+----------------+ TCP/Socket +-----------------+
| Client A | ◄──────────────────────────► | |
| Client B | ◄──────────────────────────► | ChatServer | (維護 List)
| Client C | ◄──────────────────────────► | |
+----------------+ +-----------------+
▲ │
│ 廣播循環: for(h: handlers) h.send(msg) │
└────────────────────────────────────────────────────┘
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
public class ChatServer extends JFrame {
private final JTextArea logArea = new JTextArea(20, 50);
private final List clients = Collections.synchronizedList(new ArrayList<>());
private ServerSocket serverSocket;
public ChatServer(int port) throws IOException {
super("廣播信使 - 服務端 [" + port + "]");
setDefaultCloseOperation(EXIT_ON_CLOSE);
logArea.setEditable(false);
logArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
add(new JScrollPane(logArea), BorderLayout.CENTER);
pack(); setLocationRelativeTo(null); setVisible(true);
serverSocket = new ServerSocket(port);
log("服務器啟動,監聽端口 " + port);
new Thread(this::acceptLoop).start();
}
private void acceptLoop() {
try {
while (!serverSocket.isClosed()) {
Socket client = serverSocket.accept();
ClientHandler handler = new ClientHandler(client);
clients.add(handler);
handler.start();
log("新連接: " + client.getInetAddress() + " 當前在線: " + clients.size());
}
} catch (IOException e) {
if (!serverSocket.isClosed()) log("Accept 錯誤: " + e);
}
}
// 廣播給所有客戶端
void broadcast(String msg, ClientHandler sender) {
String full = "[" + sender.getUserName() + "] " + msg;
log(full);
synchronized (clients) {
for (ClientHandler c : clients) c.send(full);
}
}
// 移除斷開連接的客戶端
void removeClient(ClientHandler h) {
clients.remove(h);
log(h.getUserName() + " 斷開,剩餘: " + clients.size());
}
private void log(String s) {
SwingUtilities.invokeLater(() -> logArea.append(s + "\n"));
}
public static void main(String[] args) throws IOException {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 8888;
new ChatServer(port);
}
// ===== 內部類:處理單個客戶端 =====
class ClientHandler extends Thread {
private final Socket socket;
private DataInputStream in;
private DataOutputStream out;
private String userName;
ClientHandler(Socket s) { this.socket = s; }
@Override public void run() {
try {
in = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
// 首個數據包為用戶名
userName = in.readUTF();
broadcast(userName + " 上線了", this);
String msg;
while ((msg = in.readUTF()) != null) {
broadcast(msg, this);
}
} catch (IOException e) {
// 正常斷開或異常
} finally {
close();
removeClient(this);
}
}
void send(String msg) {
try { out.writeUTF(msg); out.flush(); } catch (IOException ignored) {}
}
String getUserName() { return userName != null ? userName : socket.getInetAddress().toString(); }
void close() {
try { socket.close(); } catch (IOException ignored) {}
}
}
}
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
public class ChatClient extends JFrame implements Runnable {
private final JTextArea chatArea = new JTextArea(15, 40);
private final JTextField inputField = new JTextField(30);
private final DefaultListModel userListModel = new DefaultListModel<>();
private final JList userList = new JList<>(userListModel);
private DataInputStream in;
private DataOutputStream out;
private Socket socket;
private String userName;
public ChatClient(String host, int port) throws IOException {
super("廣播信使 - 客戶端");
setDefaultCloseOperation(EXIT_ON_CLOSE);
// 登錄對話框
userName = JOptionPane.showInputDialog(this, "輸入用戶名:", "登錄", JOptionPane.PLAIN_MESSAGE);
if (userName == null || userName.trim().isEmpty()) System.exit(0);
userName = userName.trim();
// 佈局
chatArea.setEditable(false);
chatArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
JPanel center = new JPanel(new BorderLayout());
center.add(new JScrollPane(chatArea), BorderLayout.CENTER);
center.add(new JScrollPane(userList), BorderLayout.EAST);
userList.setPreferredSize(new Dimension(120, 0));
userList.setBorder(BorderFactory.createTitledBorder("在線用戶"));
JPanel bottom = new JPanel(new BorderLayout());
bottom.add(new JLabel(userName + ": "), BorderLayout.WEST);
bottom.add(inputField, BorderLayout.CENTER);
JButton sendBtn = new JButton("發送");
bottom.add(sendBtn, BorderLayout.EAST);
add(center, BorderLayout.CENTER);
add(bottom, BorderLayout.SOUTH);
pack(); setLocationRelativeTo(null); setVisible(true);
// 連接服務器
socket = new Socket(host, port);
in = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
// 發送用戶名
out.writeUTF(userName);
out.flush();
// 發送按鈕事件
ActionListener sendAction = e -> sendMessage();
inputField.addActionListener(sendAction);
sendBtn.addActionListener(sendAction);
// 啟動接收線程
new Thread(this).start();
}
private void sendMessage() {
String text = inputField.getText().trim();
if (text.isEmpty()) return;
try { out.writeUTF(text); out.flush(); inputField.setText(""); }
catch (IOException ex) { append("發送失敗: " + ex); }
}
@Override public void run() {
try {
String msg;
while ((msg = in.readUTF()) != null) {
// 簡單協議: "用戶列表:|user1|user2|" 或 普通消息
if (msg.startsWith("用戶列表:")) {
updateUserList(msg.substring(5));
} else {
append(msg);
}
}
} catch (IOException e) {
append("連接斷開: " + e);
} finally { close(); }
}
private void append(String s) {
SwingUtilities.invokeLater(() -> chatArea.append(s + "\n"));
}
private void updateUserList(String data) {
SwingUtilities.invokeLater(() -> {
userListModel.clear();
for (String u : data.split("\\|")) if (!u.isEmpty()) userListModel.addElement(u);
});
}
void close() {
try { socket.close(); } catch (IOException ignored) {}
System.exit(0);
}
public static void main(String[] args) throws IOException {
String host = args.length > 0 ? args[0] : "127.0.0.1";
int port = args.length > 1 ? Integer.parseInt(args[1]) : 8888;
SwingUtilities.invokeLater(() -> {
try { new ChatClient(host, port); } catch (IOException ex) { ex.printStackTrace(); }
});
}
}
| 模塊 | 關鍵類/方法 | 作用 |
|---|---|---|
| 服務端監聽 | `ServerSocket.accept()` | 阻塞等待客戶端連接,返回 `Socket` |
| 多線程隔離 | `ClientHandler extends Thread` | 每連接一線程,互不阻塞 |
| 數據流 | `DataInputStream`/`DataOutputStream` + `readUTF`/`writeUTF` | 處理字符串編碼/長度前綴,避免粘包 |
| 線程安全集合 | `Collections.synchronizedList` | 多線程並發增刪客戶端列表 |
| UI 線程隔離 | `SwingUtilities.invokeLater` | 網絡線程安全更新 Swing 組件 |
| 廣播模式 | 遍歷 handlers 逐個 `send()` | 單點發送 → 服務端扇出 → 所有客戶端 |