用JavaSocket编程开发聊天室

用JavaSocket编程开发聊天室

实验要求:

  1. 用Java图形用户界面编写聊天室服务器端和客户端, 支持多个客户端连接到一个服务器。每个客户端能够输入账号。
  2. 可以实现群聊(聊天记录显示在所有客户端界面)。
  3. 完成好友列表在各个客户端上显示。
  4. 可以实现私人聊天,用户可以选择某个其他用户,单独发送信息。
  5. 服务器能够群发系统消息,能够强行让某些用户下线。
  6. 客户端的上线下线要求能够在其他客户端上面实时刷新。
    本人目前大二,这是期末课程设计做得一个完整的玩具项目,但是由于水平和时间等问题,这个项目的设计和架构还是有些问题,比如说发送消息和指令都是使用Socket发送的,仅仅使用一些特殊字符来区别消息和指令,如果用户端直接发送和指令相同的字符串,则会导致bug。因此本项目的健壮性和可拓展性都较差,仅能作为课设使用。
    分为客户端和服务器端。
    服务器端功能:
  • 可以实现查看所有在线用户
  • 可以强制下线在线用户
  • 可以发送系统消息
  • 用户正常登录和退出会通知所有在线用户

客户端功能:

  • 输入服务器、端口和用户名即可登录
  • 可以发生群聊消息
  • 右侧显示所有在线用户
  • 双击右侧在线用户可发送私信

项目地址:
javaSocket聊天室
客户端Client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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,关于同时启动多个实例比较简单,大家可以自行搜索。

其他

个人主页:
张明宇的个人主页

作者

Zhangmingyu

发布于

2024-06-05

更新于

2025-06-04

许可协议

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

评论

You forgot to set the shortname for Disqus. Please set it in _config.yml.