用JavaSocket编程开发聊天室
实验要求:
- 用Java图形用户界面编写聊天室服务器端和客户端, 支持多个客户端连接到一个服务器。每个客户端能够输入账号。
- 可以实现群聊(聊天记录显示在所有客户端界面)。
- 完成好友列表在各个客户端上显示。
- 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息。
- 服务器能够群发系统消息,能够强行让某些用户下线。
- 客户端的上线下线要求能够在其他客户端上面实时刷新。
本人目前大二,这是期末课程设计做得一个完整的玩具项目,但是由于水平和时间等问题,这个项目的设计和架构还是有些问题,比如说发送消息和指令都是使用Socket发送的,仅仅使用一些特殊字符来区别消息和指令,如果用户端直接发送和指令相同的字符串,则会导致bug。因此本项目的健壮性和可拓展性都较差,仅能作为课设使用。
分为客户端和服务器端。
服务器端功能:
- 可以实现查看所有在线用户
- 可以强制下线在线用户
- 可以发送系统消息
- 用户正常登录和退出会通知所有在线用户
客户端功能:
- 输入服务器、端口和用户名即可登录
- 可以发生群聊消息
- 右侧显示所有在线用户
- 双击右侧在线用户可发送私信
项目地址:
javaSocket聊天室
客户端Client:
package client;
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
public class Client {
private Socket socket;
private DataOutputStream outputStream;
private PrintWriter out;
private BufferedReader in;
private String serverAddress;
private int port;
private String username;
private OnMessageReceivedListener listener;
public Client(String serverAddress, int port, String username, OnMessageReceivedListener listener) throws IOException {
this.serverAddress = serverAddress;
this.port = port;
this.username = username;
this.listener = listener;
initConnection();
}
private void initConnection() throws IOException {
if (socket != null && !socket.isClosed()) {
return;
}
socket = new Socket(serverAddress, port);
outputStream = new DataOutputStream(socket.getOutputStream());
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
}
public void sendMessage(String message) {
if (outputStream != null && !socket.isClosed()) {
try {
out.println(message);
out.flush();
} catch (Exception e) {
handleSendError(e);
}
} else {
System.err.println("Socket is not properly initialized or is closed.");
}
}
public void sendPrivateMessage(String recipient, String message) {
sendMessage("/pm " + recipient + " " + message);
}
private void handleSendError(Exception e) {
e.printStackTrace();
try {
socket.close();
} catch (IOException closeException) {
closeException.printStackTrace();
}
listener.onConnectionLost();
}
public void connect() {
try {
initConnection();
sendMessage(username); // 发送用户名以登录
Thread readerThread = new Thread(() -> {
String message;
try {
while ((message = in.readLine()) != null) {
listener.onMessageReceived(message);
}
} catch (IOException e) {
listener.onConnectionLost();
e.printStackTrace();
} finally {
try {
socket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
readerThread.start();
} catch (UnknownHostException e) {
System.err.println("Server not found: " + e.getMessage());
listener.onConnectionLost();
} catch (IOException e) {
System.err.println("Error connecting to server: " + e.getMessage());
listener.onConnectionLost();
}
}
public void disconnect() {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getUsername() {
return username;
}
public interface OnMessageReceivedListener {
void onMessageReceived(String message);
void onConnectionLost();
void onUpdateOnlineUsers(String userListData);
void onForceLogout();
}
}
客户端界面ClientGUI:
package client;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class ClientGUI extends Component {
private JTextField serverAddressField, portField, usernameField, messageField;
private JButton connectButton, sendButton;
private JTextArea chatArea;
private JList<String> onlineList;
private DefaultListModel<String> onlineListModel;
private Client client;
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new ClientGUI().initializeUI());
}
private void initializeUI() {
JFrame frame = new JFrame("Chat Client");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
chatArea = new JTextArea();
chatArea.setEditable(false);
JScrollPane scrollPane = new JScrollPane(chatArea);
frame.add(scrollPane, BorderLayout.CENTER);
JPanel southPanel = new JPanel();
southPanel.setLayout(new BorderLayout());
JPanel inputPanel = new JPanel();
inputPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
messageField = new JTextField(30);
sendButton = new JButton("Send");
inputPanel.add(messageField);
inputPanel.add(sendButton);
southPanel.add(inputPanel, BorderLayout.CENTER);
JPanel connectPanel = new JPanel();
connectPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
serverAddressField = new JTextField("localhost", 10);
portField = new JTextField("12345", 5);
usernameField = new JTextField("User", 10);
connectButton = new JButton("Connect");
connectPanel.add(new JLabel("Server: "));
connectPanel.add(serverAddressField);
connectPanel.add(new JLabel(" Port: "));
connectPanel.add(portField);
connectPanel.add(new JLabel(" Username: "));
connectPanel.add(usernameField);
connectPanel.add(connectButton);
southPanel.add(connectPanel, BorderLayout.SOUTH);
frame.add(southPanel, BorderLayout.SOUTH);
onlineListModel = new DefaultListModel<>();
onlineList = new JList<>(onlineListModel);
JScrollPane onlineScrollPane = new JScrollPane(onlineList);
onlineScrollPane.setPreferredSize(new Dimension(150, 0));
frame.add(onlineScrollPane, BorderLayout.EAST);
createEvents();
frame.setVisible(true);
}
private void createEvents() {
connectButton.addActionListener(e -> {
String serverAddress = serverAddressField.getText();
int port = Integer.parseInt(portField.getText());
String username = usernameField.getText();
connectButton.setEnabled(false);
try {
client = new Client(serverAddress, port, username, new GUIListener());
client.connect();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "连接失败,请检查输入信息。", "连接错误", JOptionPane.ERROR_MESSAGE);
connectButton.setEnabled(true);
}
});
sendButton.addActionListener(e -> {
String message = messageField.getText();
if (!message.isEmpty()) {
client.sendMessage(message);
displaySentMessage(message);
messageField.setText("");
}
});
onlineList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
String selectedUser = onlineList.getSelectedValue();
if (selectedUser != null && client != null) {
String privateMessage = JOptionPane.showInputDialog(ClientGUI.this,
"输入要发送给 " + selectedUser + " 的消息:", "发送私信", JOptionPane.PLAIN_MESSAGE);
if (privateMessage != null && !privateMessage.trim().isEmpty()) {
client.sendPrivateMessage(selectedUser, privateMessage);
displaySentMessage("[私信给 " + selectedUser + "]: " + privateMessage); // 显示发送的私聊消息
}
}
}
}
});
}
private void displaySentMessage(String message) {
SwingUtilities.invokeLater(() -> {
chatArea.append(client.getUsername() + ": " + message + "\n");
chatArea.setCaretPosition(chatArea.getDocument().getLength());
});
}
private class GUIListener implements Client.OnMessageReceivedListener {
@Override
public void onMessageReceived(String message) {
SwingUtilities.invokeLater(() -> {
if (message.startsWith("/users ")) { // 检查消息是否以/users开头
onUpdateOnlineUsers(message.substring(7)); // 去掉"/users "前缀,然后更新在线用户列表
}else if(message.equals("/forceLogout")){
onForceLogout();
}else if(message.equals("/server/ERROR: 用户名已被占用,请选择其他用户名。")){
JOptionPane.showMessageDialog(ClientGUI.this, "用户名已被占用,请选择其他用户名。", "连接错误", JOptionPane.ERROR_MESSAGE);
connectButton.setEnabled(true);
client.disconnect();
}else if(message.equals("/server/SUCCESS: 连接成功")){
JOptionPane.showMessageDialog(ClientGUI.this, "连接成功", "连接状态", JOptionPane.INFORMATION_MESSAGE);
}
else {
chatArea.append(message + "\n");
chatArea.setCaretPosition(chatArea.getDocument().getLength());
}
});
}
@Override
public void onConnectionLost() {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(ClientGUI.this, "连接丢失,请检查网络或重新登录。", "连接错误", JOptionPane.ERROR_MESSAGE);
connectButton.setEnabled(true);
});
}
@Override
public void onUpdateOnlineUsers(String userListData) {
SwingUtilities.invokeLater(() -> {
String[] usernames = userListData.split(","); // 假设用户列表是以逗号分隔的用户名
onlineListModel.clear(); // 清空现有在线用户列表
for (String username : usernames) {
onlineListModel.addElement(username.trim()); // 添加每个用户名到在线用户列表模型中
}
});
}
@Override
public void onForceLogout(){
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(null, "抱歉!您已被服务器强制下线!", "强制下线", JOptionPane.WARNING_MESSAGE);
System.exit(0); // 关闭客户端程序
});
}
}
}
Common包下Message类:
package common;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Message {
private String sender;
private String recipient;
private String content;
private LocalDateTime timestamp;
// 构造函数
public Message(String sender, String recipient, String content) {
this.sender = sender;
this.recipient = recipient;
this.content = content;
this.timestamp = LocalDateTime.now(); // 当前时间作为发送时间
}
public Message(String sender, String content) {
this(sender, "Everyone", content);
}
// Getter 和 Setter 方法
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
public String getRecipient() {
return recipient;
}
public void setRecipient(String recipient) {
this.recipient = recipient;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public LocalDateTime getTimestamp() {
return timestamp;
}
// 格式化时间戳的字符串表示
public String getFormattedTimestamp() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return timestamp.format(formatter);
}
// 重写toString方法,便于打印或显示消息详情
@Override
public String toString() {
return String.format("[%s] %s -> %s: %s",
getFormattedTimestamp(),
sender,
recipient,
content);
}
}
服务器端Server:
package server;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import common.Message;
public class Server {
private static final int PORT = 12345; // 服务器端口
private final List<ServerThread> clients = new ArrayList<>(); // 存储所有连接的客户端线程
private ServerGUI gui;
public Server(ServerGUI gui) {
this.gui = gui;
}
public static void main(String[] args) {
ServerGUI gui = new ServerGUI();
Server server = new Server(gui);
gui.setServer(server);
server.startServer();
}
public void startServer() {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器启动,正在监听端口: " + PORT);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待客户端连接
ServerThread serverThread = new ServerThread(socket, this);
serverThread.start(); // 启动线程处理客户端请求
System.out.println("新客户端连接: " + socket.getInetAddress());
}
} catch (IOException e) {
e.printStackTrace();
System.err.println("服务器启动失败");
}
}
public synchronized void addClient(ServerThread client) {
clients.add(client);
updateOnlineUsers();
gui.updateUserList(clients);
}
public synchronized boolean removeClient(ServerThread client) {
boolean removed = clients.remove(client);
if (removed) {
updateOnlineUsers();
gui.updateUserList(clients);
}
return removed;
}
public synchronized void broadcast(Message message, ServerThread excludeClient) {
for (ServerThread client : clients) {
if (client != excludeClient) {
client.send(message);
}
}
}
public synchronized void updateOnlineUsers() {
StringBuilder userList = new StringBuilder("/users ");
for (ServerThread client : clients) {
userList.append(client.getUsername()).append(",");
}
String userListMessage = userList.toString();
if (userListMessage.endsWith(",")) {
userListMessage = userListMessage.substring(0, userListMessage.length() - 1);
}
for (ServerThread client : clients) {
client.sendRawMessage(userListMessage);
}
}
public List<ServerThread> getClients() {
return clients;
}
public void forceLogout(String username) {
for (ServerThread client : clients) {
if (client.getUsername().equals(username)) {
client.interrupt(); // 中断客户端线程以强制下线
removeClient(client); // 从列表中移除客户端
client.forceLogout();
gui.updateUserList(clients);
break;
}
}
}
public synchronized boolean isUsernameTaken(String username) {
for (ServerThread client : clients) {
if (client.getUsername().equals(username)) {
return true;
}
}
return false;
}
public synchronized void sendSystemMessage(String content) {
Message systemMessage = new Message("Server", "Everyone", content);
broadcast(systemMessage, null);
}
}
客户端界面ServerGUI:
package server;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
public class ServerGUI extends JFrame {
private JList<String> userList;
private DefaultListModel<String> userListModel;
private Server server;
public ServerGUI() {
setTitle("Chat Server");
setSize(400, 300);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
userListModel = new DefaultListModel<>();
userList = new JList<>(userListModel);
JScrollPane scrollPane = new JScrollPane(userList);
JButton forceLogoutButton = new JButton("Force Logout");
forceLogoutButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String selectedUser = userList.getSelectedValue();
if (selectedUser != null) {
server.forceLogout(selectedUser);
}
}
});
// 系统消息输入框和发送按钮
JTextField systemMessageField = new JTextField();
JButton sendSystemMessageButton = new JButton("发送系统消息");
sendSystemMessageButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String message = systemMessageField.getText();
if (message != null && !message.trim().isEmpty()) {
server.sendSystemMessage(message);
systemMessageField.setText(""); // 清空输入框
}
}
});
JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(forceLogoutButton, BorderLayout.SOUTH);
// 系统消息面板
JPanel systemMessagePanel = new JPanel(new BorderLayout());
systemMessagePanel.add(systemMessageField, BorderLayout.CENTER);
systemMessagePanel.add(sendSystemMessageButton, BorderLayout.EAST);
// 添加到主窗口
add(panel, BorderLayout.CENTER);
add(systemMessagePanel, BorderLayout.SOUTH);
setVisible(true);
}
public void updateUserList(List<ServerThread> clients) {
SwingUtilities.invokeLater(() -> {
userListModel.clear();
for (ServerThread client : clients) {
userListModel.addElement(client.getUsername());
}
});
}
public void setServer(Server server) {
this.server = server;
}
}
客户端ServerThread:
package server;
import common.Message;
import java.io.*;
import java.net.Socket;
public class ServerThread extends Thread {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String username;
private Server server;
private volatile boolean running = true;
public ServerThread(Socket socket, Server server) {
this.socket = socket;
this.server = server;
try {
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error initializing streams for client: " + e.getMessage());
}
}
@Override
public void run() {
try {
this.username = in.readLine();
if (server.isUsernameTaken(this.username)) {
sendRawMessage("/server/ERROR: 用户名已被占用,请选择其他用户名。");
socket.close();
return;
}
sendRawMessage("/server/SUCCESS: 连接成功");
System.out.println("账号" + this.username + "已经登录");
server.addClient(this); // 注意不要在构造函数中重复调用 addClient
server.broadcast(new Message(username, "Server", username + " has joined the chat!"), this);
String inputLine;
while (running && (inputLine = in.readLine()) != null) {
if (inputLine.startsWith("/pm ")) {
handlePrivateMessage(inputLine);
}
else {
server.broadcast(new Message(username, "Everyone", inputLine), this);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
boolean removed = server.removeClient(this);
if (removed) {
server.broadcast(new Message(username, "Server", username + " has left the chat!"), null);
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void send(Message message) {
if (out != null && !out.checkError()) {
out.println(message.toString());
out.flush();
}
}
public void sendRawMessage(String message) {
try {
if (out != null && !out.checkError()) {
out.println(message);
out.flush();
} else {
System.err.println("Output stream is closed, cannot send message.");
}
} catch (Exception e) {
e.printStackTrace();
System.err.println("Error sending message: " + e.getMessage());
}
}
private void handlePrivateMessage(String inputLine) {
int firstSpace = inputLine.indexOf(" ");
int secondSpace = inputLine.indexOf(" ", firstSpace + 1);
if (secondSpace != -1) {
String recipient = inputLine.substring(firstSpace + 1, secondSpace);
String message = inputLine.substring(secondSpace + 1);
for (ServerThread client : server.getClients()) {
if (client.getUsername().equals(recipient)) {
client.send(new Message(username, recipient, message));
break;
}
}
}
}
public String getUsername() {
return username;
}
public void forceLogout() {
running = false; // 停止主循环
sendRawMessage("/forceLogout");
try {
socket.close(); // 关闭Socket以触发IOException并停止线程
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用方法
首先启动服务器端Server,再启动客户端界面ClientGUI,关于同时启动多个实例比较简单,大家可以自行搜索。
其他
个人主页:
张明宇的个人主页