主要添加了私聊功能
1服务器类定义与成员变量
public class ChatServer {
int port = 6666;// 定义服务器端口号为 6666
ServerSocket ss;// 定义一个 ServerSocket 对象用于监听客户端连接
//List<Socket> clientSockets = new ArrayList<>();// 定义一个列表用于存储已连接的客户端 Socket 对象
List<Socket> clientSockets = new CopyOnWriteArrayList<>();
List<String> clientNames = new ArrayList<>(10);
//迭代时会复制整个底层数组,因此在遍历过程中其他线程对集合的修改不会影响当前遍历,
// 有效避免了 ConcurrentModificationException 异常。
}
- 端口号:服务器的端口号被定义为
6666
,客户端需要知道这个端口号才能与服务器建立连接。 - ServerSocket 对象:
ServerSocket
是 Java 提供的一种用于服务器端编程的类,它负责监听特定端口上的客户端连接请求。一旦有客户端连接,就会创建一个新的Socket
对象来处理与该客户端之间的通信。 - 客户端套接字列表:
clientSockets
使用了CopyOnWriteArrayList
来存储与客户端建立连接的Socket
对象。这种数据结构在遍历时会复制整个底层数组,因此在遍历过程中其他线程对集合的修改不会影响当前遍历,有效避免了ConcurrentModificationException
异常。 - 客户端名称列表:
clientNames
用于存储每个连接客户端的名称,方便在消息中区分不同的客户端。
2初始化服务器
public void initServer() {// 初始化服务器的方法
try {
ss = new ServerSocket(port);// 创建 ServerSocket 对象并绑定到指定端口
System.out.println("服务器启动,等待客户端连接...");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
- 在服务器初始化方法
initServer
中,通过调用ServerSocket
的构造函数并传入端口号,创建了一个ServerSocket
对象,绑定了服务器到指定端口,使服务器能够监听该端口上的客户端连接请求。一旦初始化成功,就会打印出 "服务器启动,等待客户端连接..." 的提示信息,告知服务器已正常启动并处于监听状态。
3监听客户端连接
public void listenerConnection() {// 监听客户端连接的方法,返回连接的 Socket 对象
new Thread(()->{
while(true){
try {
Socket socket = ss.accept();// 调用 accept() 方法等待客户端连接
clientNames.add("Hello");
//clientSockets.add(socket);
synchronized (clientSockets) {// 同步操作确保线程安全
clientSockets.add(socket);// 将连接的客户端 Socket 对象添加到列表中
}
System.out.println("客户端已连接:" + socket.getInetAddress().getHostAddress());// 输出客户端连接成功提示信息及客户端 IP 地址
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
listenerConnection
方法创建了一个新线程,用于不断地监听客户端的连接请求。在循环中,调用ss.accept()
方法阻塞等待客户端的连接。一旦有客户端连接,就会创建一个新的Socket
对象,并将其添加到clientSockets
列表中。同时,向clientNames
列表中添加一个默认客户端名称 "Hello",后续可以根据实际情况更新该名称。每当有新的客户端连接成功,就会打印出客户端的 IP 地址,表明该客户端已成功连接到服务器。
4读取客户端消息
public void readMsg(List<Socket> clientSockets, JTextArea msgShow) {// 读取客户端消息的方法
//System.out.println("clientSockets size: " + clientSockets.size()); // 检查列表大小
synchronized (clientSockets) {// 对客户端列表进行同步操作
Thread tt = new Thread(() -> {// 创建一个线程用于读取并处理客户端消息
//System.out.println("开始读取客户端发送的消息");
while (true) {// 无限循环持续读取消息
InputStream is;// 定义输入流对象用于读取客户端消息
Socket socket = null;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (Socket cSocket : clientSockets) {// 遍历每个客户端 Socket
//System.out.println("循环每个socket");
socket = cSocket;
if(socket == null){
continue;
}
try {
is = socket.getInputStream();// 获取客户端 Socket 对象的输入流
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
int idLen = is.read();// 读取消息中发送方名称长度的字节
if(idLen == 0){
continue;
}
byte[] id = new byte[idLen];// 根据读取的长度创建字节数组存储发送方名称
is.read(id);// 读取发送方名称字节数组
int si = clientSockets.indexOf(socket);
clientNames.set(si,new String(id));
int msgLen = is.read();// 读取消息内容长度的字节
if(msgLen == 0){
continue;
}
byte[] msg = new byte[msgLen];// 根据读取的长度创建字节数组存储消息内容
is.read(msg);// 读取消息内容字节数组
String clientmsg = new String(msg);
//先判断@有没有
int start = clientmsg.indexOf('@');
if(start != -1){
int end = clientmsg.indexOf(' ');
String friendID = clientmsg.substring(start+1,end);
String message = clientmsg.substring(end);
System.out.println(new String(id) + "发送给" + friendID + " 的一条私聊消息:" + message);// 将字节数组转换为字符串并输出消息内容
msgShow.append(new String(id) + "发送给" + friendID + " 的一条私聊消息:" + message + "\n");
for (Socket clientSocket : clientSockets) {// 遍历所有已连接的客户端 Socket 对象
if (clientSocket == socket) {// 如果是当前发送消息的客户端
continue;
}
int s = clientSockets.indexOf(clientSocket);
String name = clientNames.get(s);
if(name.equals(friendID)){
OutputStream os = null;// 定义输出流对象用于向其他客户端发送消息
os = clientSocket.getOutputStream();// 获取客户端 Socket 对象的输出流
os.write(id.length);// 发送发送方名称长度
os.write(id);// 发送发送方名称字节数组
message += "(这是一条私聊消息)";
os.write(message.getBytes().length);// 发送消息内容长度
os.write(message.getBytes());// 发送消息内容字节数组
os.flush();// 刷新输出流确保数据发送完成
break;
}
}
}else {
System.out.println(new String(id) + "发送的消息:" + new String(msg));// 将字节数组转换为字符串并输出消息内容
msgShow.append(new String(id) + "说:" + new String(msg) + "\n");
// 转发信息给所有其他客户端
for (Socket clientSocket : clientSockets) {// 遍历所有已连接的客户端 Socket 对象
if (clientSocket == socket) {// 如果是当前发送消息的客户端
continue;
}
OutputStream os = null;// 定义输出流对象用于向其他客户端发送消息
os = clientSocket.getOutputStream();// 获取客户端 Socket 对象的输出流
os.write(id.length);// 发送发送方名称长度
os.write(id);// 发送发送方名称字节数组
os.write(msg.length);// 发送消息内容长度
os.write(msg);// 发送消息内容字节数组
os.flush();// 刷新输出流确保数据发送完成
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
});
tt.start();
}
}
readMsg
方法的核心功能是读取客户端发送的消息,并根据消息类型(普通消息或私聊消息)进行相应的处理和转发。- 首先,创建一个新线程
tt
,在无限循环中依次检查clientSockets
列表中的每个Socket
对象。对于每个客户端Socket
,通过socket.getInputStream()
获取输入流,从而读取客户端发送的数据。 - 客户端发送的数据格式预先约定为:首先是一个字节表示发送方名称的长度,然后是发送方名称对应的字节数组;接下来是一个字节表示消息内容长度,最后是消息内容对应的字节数组。按照这种格式,先读取发送方名称长度
idLen
,根据该长度创建字节数组id
读取发送方名称,再读取消息内容长度msgLen
,创建字节数组msg
读取消息内容。 - 随后,将读取到的发送方名称更新到对应的
clientNames
列表位置。接下来对消息内容进行判断,如果消息内容中包含 "@" 符号,则认为这是一条私聊消息。通过解析消息内容,获取私聊目标的名称friendID
和实际消息内容message
,然后在clientSockets
中查找对应的私聊目标客户端Socket
,并将私聊消息发送给该目标客户端。 - 如果消息内容中不包含 "@" 符号,则认为这是一条普通消息,直接将消息转发给除发送方外的所有其他客户端。
- 无论是普通消息还是私聊消息,都通过输出流
OutputStream
将消息发送给目标客户端。发送时,同样按照约定的数据格式,先发送发送方名称的长度和名称字节数组,再发送消息内容的长度和内容字节数组,并调用os.flush()
刷新输出流确保数据发送完成。
5服务器启动
public void start() {// 启动服务器的方法
initServer();// 调用初始化服务器的方法
//new Thread(()->{
//startSend();// 启动服务端从控制台向所有客户端发送消息的线程
//}).start();
ChatUI ui = new ChatUI("服务端", clientSockets);
ui.setVisible(true); // 确保 UI 可见
listenerConnection();// 调用监听客户端连接的方法
readMsg(clientSockets,ui.msgShow);// 调用读取消息的方法
}
- 在
start
方法中,首先调用initServer
方法初始化服务器,然后创建一个ChatUI
对象作为服务器端的用户界面(假设存在ChatUI
类,用于展示聊天内容等信息),使用户界面可见。接着调用listenerConnection
方法启动监听客户端连接的线程,最后调用readMsg
方法启动读取消息的线程,并将读取到的消息显示在用户界面中。
6主方法
public static void main(String[] args) {
ChatServer server = new ChatServer();// 创建 ChatServer 对象
server.start();// 调用启动服务器的方法
}
main
方法作为程序的入口,创建了一个ChatServer
对象,并调用其start
方法启动服务器。
7其他模块代码不变
public class Client {
Socket socket;// 定义 Socket 对象用于与服务器建立连接
String ip;// 定义服务器 IP 地址
int port;// 定义服务器端口号
InputStream in;// 定义输入流对象用于读取服务器发送的消息
OutputStream out;// 定义输出流对象用于向服务器发送消息
public Client(String ip, int port) {// 构造方法,初始化客户端 IP 地址和端口号
this.ip = ip;
this.port = port;
}
public void connectServer(String userName) {// 连接服务器的方法
try {
socket = new Socket(ip, port);// 创建 Socket 对象连接到指定 IP
in = socket.getInputStream();// 获取 Socket 对象的输入流用于读取消息
out = socket.getOutputStream();// 获取 Socket 对象的输出流用于发送消息
try {
out.write(userName.length());
out.write(userName.getBytes());
out.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("连接服务器成功");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void readMsg(JTextArea msgShow) {// 读取服务器发送的消息的方法
new Thread(() -> {// 创建一个线程用于读取并处理服务器消息
try {
System.out.println("开始读取消息");
while (true) { // 无限循环持续读取消息
int senderNameLength = in.read();// 读取发送方名称长度的字节
byte[] senderNameBytes = new byte[senderNameLength];// 根据读取的长度创建字节数组存储发送方名称
in.read(senderNameBytes);// 读取发送方名称字节数组
int msgLength = in.read();// 读取消息内容长度的字节
byte[] msgBytes = new byte[msgLength];// 根据读取的长度创建字节数组存储消息内容
in.read(msgBytes);// 读取消息内容字节数组
System.out.println(new String(senderNameBytes) + "发送的消息:" + new String(msgBytes));// 将字节数组转换为字符串并输出消息内容
msgShow.append(new String(senderNameBytes) +"说:" + new String(msgBytes) + "\n");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();
}
/*
public void startSend() {// 从控制台向服务器发送消息的方法
new Thread(() -> {// 创建一个线程用于读取控制台输入并发送消息
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));// 创建缓冲读取器读取控制台输入
System.out.println("请输入用户名:");
String userName = reader.readLine();// 读取一行控制台输入作为用户名
System.out.println("请输入消息(按回车发送):");
while (true) {// 无限循环持续读取并发送消息
String msg = reader.readLine();// 读取一行控制台输入作为消息内容
if (msg != null && !msg.isEmpty()) {// 如果输入的消息不为空
out.write(userName.getBytes().length);// 发送用户名长度
out.write(userName.getBytes());// 发送用户名字节数组
out.write(msg.getBytes().length);// 发送消息内容长度
out.write(msg.getBytes());// 发送消息内容字节数组
out.flush();// 刷新输出流确保数据发送完成
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();
}
*/
public void startClient() {// 启动客户端的方法
String userName = JOptionPane.showInputDialog("请输入用户名:");
connectServer(userName);// 调用连接服务器的方法
ChatUI ui = new ChatUI(userName, out);
readMsg(ui.msgShow);// 调用读取消息的方法
//startSend();// 调用发送消息的方法
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread() {
public void run() {
while (true) {
try {
out.write(0);
out.flush();
Thread.sleep(500);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}.start();
}
public static void main(String[] args) {
Client client = new Client("127.0.0.1", 6666);// 创建 Client 对象,连接到本地主机的 6666 端口
client.startClient();// 调用启动客户端的方法
}
}
public class ChatUI extends JFrame {
public JTextArea msgShow = new JTextArea();// 显示消息的文本区域
public ChatUI(String title, List<Socket> clientSockets) {// 服务器端构造方法
super(title);// 设置窗口标题
setSize(500, 500);// 设置窗口大小
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 设置关闭操作
JScrollPane scrollPane = new JScrollPane(msgShow);// 创建滚动面板包括消息显示区域
scrollPane.setPreferredSize(new Dimension(0, 350));
add(scrollPane, BorderLayout.NORTH);// 添加到窗口北部
// 创建消息输入面板及组件
JPanel msgInput = new JPanel();
JTextArea msg = new JTextArea();
JScrollPane scrollPane1 = new JScrollPane(msg);
scrollPane1.setPreferredSize(new Dimension(480, 80));
msgInput.add(scrollPane1);
JButton send = new JButton("发送");
msgInput.add(send);
msgInput.setPreferredSize(new Dimension(0, 120));
add(msgInput, BorderLayout.SOUTH);// 添加到窗口南部
setVisible(true);
ChatListener cl = new ChatListener();// 创建事件监听器
send.addActionListener(cl);// 为发送按钮添加监听器
cl.showMsg = msgShow;// 传递消息显示组件
cl.msgInput = msg;
cl.userName = title;
cl.clientSockets = clientSockets;
}
public ChatUI(String title, OutputStream out) {// 客户端构造方法
super(title);
setSize(500, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JScrollPane scrollPane = new JScrollPane(msgShow);
scrollPane.setPreferredSize(new Dimension(0, 350));
add(scrollPane, BorderLayout.NORTH);
JPanel msgInput = new JPanel();
JTextArea msg = new JTextArea();
JScrollPane scrollPane1 = new JScrollPane(msg);
scrollPane1.setPreferredSize(new Dimension(480, 80));
msgInput.add(scrollPane1);
JButton send = new JButton("发送");
msgInput.add(send);
msgInput.setPreferredSize(new Dimension(0, 120));
add(msgInput, BorderLayout.SOUTH);
setVisible(true);
clientListener cl = new clientListener();
send.addActionListener(cl);
cl.showMsg = msgShow;
cl.msgInput = msg;
cl.userName = title;
cl.out = out;
}
}
public class ChatListener implements ActionListener {
public List<Socket> clientSockets;// 客户端 Socket 列表
JTextArea showMsg;// 消息显示区域
JTextArea msgInput;// 消息输入区域
String userName;// 用户名
OutputStream out;// 输出流
public void actionPerformed(ActionEvent e) {// 处理发送按钮点击事件
String text = msgInput.getText();// 获取输入的消息文本
showMsg.append(userName + ": " + text + "\n");// 在显示区域追加消息
for (Socket cSocket : clientSockets) {// 遍历所有客户端
Socket socket = cSocket;
try {
out = socket.getOutputStream();// 获取客户端输出流
out.write(userName.getBytes().length);// 发送用户名长度
out.write(userName.getBytes());// 发送用户名
out.write(text.getBytes().length);// 发送消息内容长度
out.write(text.getBytes());// 发送消息内容
out.flush();// 刷新输出流
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}
public class clientListener implements ActionListener {
JTextArea showMsg;// 消息显示区域
JTextArea msgInput;// 消息输入区域
String userName;// 用户名
OutputStream out;// 输出流
public void actionPerformed(ActionEvent e) {// 处理发送按钮点击
String text = msgInput.getText();// 获取输入消息
showMsg.append(userName + ": " + text + "\n");// 显示消息
try {
out.write(userName.getBytes().length);// 发送用户名长度
out.write(userName.getBytes());// 发送用户名
out.write(text.getBytes().length);// 发送消息长度
out.write(text.getBytes());// 发送消息内容
out.flush();// 刷新输出流
//msgInput.setText(""); // 清空输入框
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}