目录
案例要求:
实现思路:
itheima-chat-server包
src
com.itheima
Constant类:
package com.itheima;
public class Constant {
public static final int PORT = 6666;
}
Server类:
package com.itheima;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
public class Server {
// 定义一个集合容器存储所有登录进来的客户端管道,以便将来群发消息给他们.
// 定义一个Map集合,键是存储客户端的管道,值是这个管道的用户名称。
public static final Map<Socket, String> onLineSockets = new HashMap<>();
public static void main(String[] args) {
System.out.println("启动服务端系统.....");
try {
// 1、注册端口。
ServerSocket serverSocket = new ServerSocket(Constant.PORT);
// 2、主线程负责接受客户端的连接请求
while (true) {
// 3、调用accept方法,获取到客户端的Socket对象
System.out.println("等待客户端的连接.....");
Socket socket = serverSocket.accept();
// 把这个管道交给一个独立的线程来处理:以便支持很多客户端可以同时进来通信。
new ServerReaderThread(socket).start();
System.out.println("一个客户端连接成功.....");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
ServerReaderThread类:
package com.itheima;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 接收的消息可能有很多种类型:1、登录消息(包含昵称) 2、群聊消息 3、私聊消息
// 所以客户端必须声明协议发送消息
// 比如客户端先发1,代表接下来是登录消息。
// 比如客户端先发2,代表接下来是群聊消息。
// 先从socket管道中接收客户端发送来的消息类型编号
DataInputStream dis = new DataInputStream(socket.getInputStream());
while (true) {
int type = dis.readInt(); // 1、2、3
switch (type){
case 1:
// 客户端发来了登录消息,接下来要接收昵称数据,再更新全部在线客户端的在线人数列表。
String nickname = dis.readUTF();
// 把这个登录成功的客户端socket存入到在线集合。
Server.onLineSockets.put(socket, nickname);
// 更新全部客户端的在线人数列表
updateClientOnLineUserList();
break;
case 2:
// 客户端发来了群聊消息,接下来要接收群聊消息内容,再把群聊消息转发给全部在线客户端。
String msg = dis.readUTF();
sendMsgToAll(msg);
break;
case 3:
// 客户端发来了私聊消息,接下来要接收私聊消息内容,再把私聊消息转发给指定客户端。
break;
}
}
} catch (Exception e) {
System.out.println("客户端下线了:"+ socket.getInetAddress().getHostAddress());
Server.onLineSockets.remove(socket); // 把下线的客户端socket从在线集合中移除
updateClientOnLineUserList(); // 下线了用户也需要更新全部客户端的在线人数列表。
}
}
// 给全部在线socket推送当前客户端发来的消息
private void sendMsgToAll(String msg) {
// 一定要拼装好这个消息,再发给全部在线socket.
StringBuilder sb = new StringBuilder();
String name = Server.onLineSockets.get(socket);
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss EEE a");
String nowStr = dtf.format(now);
String msgResult = sb.append(name).append(" ").append(nowStr).append("\r\n")
.append(msg).append("\r\n").toString();
// 推送给全部客户端socket
for (Socket socket : Server.onLineSockets.keySet()) {
try {
// 3、把集合中的所有用户名称,通过socket管道发送给客户端
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(2); // 1代表告诉客户端接下来是在线人数列表信息 2 代表发的是群聊消息
dos.writeUTF(msgResult);
dos.flush(); // 刷新数据!
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void updateClientOnLineUserList() {
// 更新全部客户端的在线人数列表
// 拿到全部在线客户端的用户名称,把这些名称转发给全部在线socket管道。
// 1、拿到当前全部在线用户昵称
Collection<String> onLineUsers = Server.onLineSockets.values();
// 2、把这个集合中的所有用户都推送给全部客户端socket管道。
for (Socket socket : Server.onLineSockets.keySet()) {
try {
// 3、把集合中的所有用户名称,通过socket管道发送给客户端
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(1); // 1代表告诉客户端接下来是在线人数列表信息 2 代表发的是群聊消息
dos.writeInt(onLineUsers.size()); // 告诉客户端,接下来要发多少个用户名称
for (String onLineUser : onLineUsers) {
dos.writeUTF(onLineUser);
}
dos.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
itheima-chat-system包
src
com.itheima.ui
ChatEntryFrame类:
package com.itheima.ui;
import javax.swing.*;
import java.awt.*;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ChatEntryFrame extends JFrame {
private JTextField nicknameField;
private JButton enterButton;
private JButton cancelButton;
private Socket socket; // 记住当前客户端系统的通信管道
public ChatEntryFrame() {
setTitle("局域网聊天室");
setSize(350, 150);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(false); // 禁止调整大小
// 设置背景颜色
getContentPane().setBackground(Color.decode("#F0F0F0"));
// 创建主面板并设置布局
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.setBackground(Color.decode("#F0F0F0"));
add(mainPanel);
// 创建顶部面板
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
topPanel.setBackground(Color.decode("#F0F0F0"));
// 标签和文本框
JLabel nicknameLabel = new JLabel("昵称:");
nicknameLabel.setFont(new Font("楷体", Font.BOLD, 16));
nicknameField = new JTextField(10);
nicknameField.setFont(new Font("楷体", Font.PLAIN, 16));
nicknameField.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY),
BorderFactory.createEmptyBorder(5, 5, 5, 5)
));
topPanel.add(nicknameLabel);
topPanel.add(nicknameField);
mainPanel.add(topPanel, BorderLayout.NORTH);
// 按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
buttonPanel.setBackground(Color.decode("#F0F0F0"));
enterButton = new JButton("登录");
enterButton.setFont(new Font("楷体", Font.BOLD, 16));
enterButton.setBackground(Color.decode("#007BFF"));
enterButton.setForeground(Color.WHITE);
enterButton.setBorderPainted(false);
enterButton.setFocusPainted(false);
cancelButton = new JButton("取消");
cancelButton.setFont(new Font("楷体", Font.BOLD, 16));
cancelButton.setBackground(Color.decode("#DC3545"));
cancelButton.setForeground(Color.WHITE);
cancelButton.setBorderPainted(false);
cancelButton.setFocusPainted(false);
buttonPanel.add(enterButton);
buttonPanel.add(cancelButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
// 添加监听器
enterButton.addActionListener(e -> {
String nickname = nicknameField.getText(); // 获取昵称
nicknameField.setText("");
if (!nickname.isEmpty()) {
try {
login(nickname);
// 进入聊天室逻辑: 启动聊天界面。把昵称传给聊天界面。
new ClientChatFrame(nickname, socket);
this.dispose(); // 关闭登录窗口
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
JOptionPane.showMessageDialog(this, "请输入昵称!");
}
});
cancelButton.addActionListener(e -> System.exit(0));
this.setVisible(true); // 显示窗口
}
public void login(String nickname) throws Exception {
// 立即发送登录消息给服务端程序。
// 1、创建Socket管道请求与服务端的socket链接
socket = new Socket(Constant.SERVER_IP, Constant.SERVER_PORT );
// 2、立即发送消息类型1 和自己的昵称给服务端
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(1); // 消息类型 登录
dos.writeUTF(nickname);
dos.flush();
}
public static void main(String[] args) {
new ChatEntryFrame();
}
}
ClientChatFrame类:
package com.itheima.ui;
import javax.swing.*;
import java.awt.*;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ChatEntryFrame extends JFrame {
private JTextField nicknameField;
private JButton enterButton;
private JButton cancelButton;
private Socket socket; // 记住当前客户端系统的通信管道
public ChatEntryFrame() {
setTitle("局域网聊天室");
setSize(350, 150);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(false); // 禁止调整大小
// 设置背景颜色
getContentPane().setBackground(Color.decode("#F0F0F0"));
// 创建主面板并设置布局
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.setBackground(Color.decode("#F0F0F0"));
add(mainPanel);
// 创建顶部面板
JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
topPanel.setBackground(Color.decode("#F0F0F0"));
// 标签和文本框
JLabel nicknameLabel = new JLabel("昵称:");
nicknameLabel.setFont(new Font("楷体", Font.BOLD, 16));
nicknameField = new JTextField(10);
nicknameField.setFont(new Font("楷体", Font.PLAIN, 16));
nicknameField.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 1, 1, 1, Color.GRAY),
BorderFactory.createEmptyBorder(5, 5, 5, 5)
));
topPanel.add(nicknameLabel);
topPanel.add(nicknameField);
mainPanel.add(topPanel, BorderLayout.NORTH);
// 按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
buttonPanel.setBackground(Color.decode("#F0F0F0"));
enterButton = new JButton("登录");
enterButton.setFont(new Font("楷体", Font.BOLD, 16));
enterButton.setBackground(Color.decode("#007BFF"));
enterButton.setForeground(Color.WHITE);
enterButton.setBorderPainted(false);
enterButton.setFocusPainted(false);
cancelButton = new JButton("取消");
cancelButton.setFont(new Font("楷体", Font.BOLD, 16));
cancelButton.setBackground(Color.decode("#DC3545"));
cancelButton.setForeground(Color.WHITE);
cancelButton.setBorderPainted(false);
cancelButton.setFocusPainted(false);
buttonPanel.add(enterButton);
buttonPanel.add(cancelButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
// 添加监听器
enterButton.addActionListener(e -> {
String nickname = nicknameField.getText(); // 获取昵称
nicknameField.setText("");
if (!nickname.isEmpty()) {
try {
login(nickname);
// 进入聊天室逻辑: 启动聊天界面。把昵称传给聊天界面。
new ClientChatFrame(nickname, socket);
this.dispose(); // 关闭登录窗口
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
JOptionPane.showMessageDialog(this, "请输入昵称!");
}
});
cancelButton.addActionListener(e -> System.exit(0));
this.setVisible(true); // 显示窗口
}
public void login(String nickname) throws Exception {
// 立即发送登录消息给服务端程序。
// 1、创建Socket管道请求与服务端的socket链接
socket = new Socket(Constant.SERVER_IP, Constant.SERVER_PORT );
// 2、立即发送消息类型1 和自己的昵称给服务端
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeInt(1); // 消息类型 登录
dos.writeUTF(nickname);
dos.flush();
}
public static void main(String[] args) {
new ChatEntryFrame();
}
}
ClientReaderThread类:
package com.itheima.ui;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class ClientReaderThread extends Thread{
private Socket socket;
private DataInputStream dis;
private ClientChatFrame win;
public ClientReaderThread( Socket socket, ClientChatFrame win) {
this.win = win;
this.socket = socket;
}
@Override
public void run() {
try {
// 接收的消息可能有很多种类型:1、在线人数更新的数据 2、群聊消息
dis = new DataInputStream(socket.getInputStream());
while (true) {
int type = dis.readInt(); // 1、2、3
switch (type){
case 1:
// 服务端发来的在线人数更新消息
updateClientOnLineUserList();
break;
case 2:
// 服务端发送来的群聊消息
getMsgToWin();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void getMsgToWin() throws Exception {
// 获取群聊消息
String msg = dis.readUTF();
win.setMsgToWin(msg);
}
// 更新客户端的在线用户列表
private void updateClientOnLineUserList() throws Exception {
// 1、读取有多少个在线用户
int count = dis.readInt();
// 2、循环控制读取多少个用户信息。
String[] names = new String[count];
for (int i = 0; i < count; i++) {
// 3、读取每个用户的信息
String nickname = dis.readUTF();
// 4、将每个用户的信息添加到数组
names[i] = nickname;
}
// 5、将集合中的数据展示到窗口上
win.updateOnlineUsers(names);
}
}
Constant类:
package com.itheima.ui;
public class Constant {
public static final String SERVER_IP = "127.0.0.1";
public static final int SERVER_PORT = 6666;
}
APP启动类:
import com.itheima.ui.ChatEntryFrame;
public class App {
public static void main(String[] args) {
new ChatEntryFrame(); // 启动登录界面
}
}