项目:聊天室小项目
该项目是一个基于 Java 开发的网络聊天系统,包含客户端和服务器端,支持用户登录、私聊、群聊以及显示在线用户列表等功能。以下是对项目的详细总结:
主要文件及功能
1. ChatServer.java
- 功能 :作为聊天系统的服务器端,负责处理客户端的连接、消息转发和用户状态管理。
- 关键方法 :
- handleRead(SelectionKey key) :处理客户端的读取事件,根据消息类型(如登录、私聊、群聊等)进行相应处理。
- broadcast(String message) 和 broadcast(String message, SocketChannel sex) :用于向所有客户端或除指定客户端外的其他客户端广播消息。
2. ChatClientUI.java
- 功能 :图形用户界面(GUI)版本的聊天客户端,使用 Swing 库实现。用户可以通过界面进行登录、选择聊天对象(私聊或群聊)并发送消息。
- 关键组件 :
- JFrame 、 JPanel 、 JTextField 、 JButton 等:用于构建用户界面。
- JTextArea :显示聊天消息。
- JList :显示在线用户列表。
- 关键方法 :
- connectToServer() :与服务器建立连接,并启动一个线程处理服务器消息。
- sendMessage(String message) :将消息发送给服务器。
- ServerMessageHandler 类:处理服务器发送的消息,根据消息类型更新在线用户列表和聊天区域。
消息协议
项目定义了一套消息协议,消息头为 5 个字符,用于区分不同类型的消息:
- LOGIN :用户登录。
- OFFUR :用户下线。
- ONUSR :用户上线。
- ONALL :在线用户列表。
- 4USER :发送给指定用户的私聊消息。
- 4ALLS :发送给所有用户的群聊消息。
总结
该项目实现了一个基本的网络聊天系统,结合了命令行和图形用户界面两种客户端。通过 NIO 实现了非阻塞的网络通信,提高了系统的性能。同时,使用自定义的消息协议确保了消息的正确处理和转发。
缺点
没有处理TCP黏连问题。在快速连续两次发送时,会出现TCP黏连问题,导致不能按指定消息处理。
服务端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class ChatServer {private static final int PORT = 8888;private Selector selector;private ServerSocketChannel serverSocketChannel;private Map<String, SocketChannel> clients = new HashMap<>();// 新增一个映射来记录客户端的连接时间private Map<SocketChannel, Long> connectionTimes = new HashMap<>();public ChatServer() {try {selector = Selector.open();serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(PORT));serverSocketChannel.configureBlocking(false);serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务器已启动,监听端口:" + PORT);} catch (IOException e) {e.printStackTrace();System.exit(1);}}public void start() {try {while (true) {int count = selector.select();if (count > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();if (key.isAcceptable()) {handleAccept(key);} else if (key.isReadable()) {handleRead(key);}iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}private void handleAccept(SelectionKey key) throws IOException {ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel client = server.accept();client.configureBlocking(false);client.register(selector, SelectionKey.OP_READ);System.out.println("新客户端连接:" + client.getRemoteAddress());// 记录客户端的连接时间connectionTimes.put(client, new Date().getTime()); }/** 服务端消息定义:* 消息头:发送到那个客户端#那个客户端发来的@* 消息头 : 5个字符,例如:LOGIN 登录 ,OFFUR 下线,ONUSR 上线,ONALL 在线列表,4USER 发送给指定客户* 4ALLS 发送给所有客户端* * 客户端消息定义:消息头:那个客户端发来的@* * */private void handleRead(SelectionKey key) {SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);try {int read = client.read(buffer);if (read > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);String message = new String(bytes).trim();System.out.println(message);if (message.startsWith("LOGIN:")) {String username = message.substring(6);// 检查用户是否已经登录if (clients.containsKey(username)) {// 向客户端发送提示信息client.write(ByteBuffer.wrap(("用户 " + username + " 已登录,请勿重复登录").getBytes()));// 关闭连接client.close();key.cancel();connectionTimes.remove(client);} else {clients.put(username, client);// 登录成功后,移除连接时间记录connectionTimes.remove(client);//发送当前在线用户列表给新登录的用户StringBuilder onlineUsers = new StringBuilder("ONALL:");for (String onlineUser : clients.keySet()) {onlineUsers.append(onlineUser).append(",");}onlineUsers.delete(onlineUsers.length() - 1, onlineUsers.length()); // 删除最后一个逗号和空格client.write(ByteBuffer.wrap(onlineUsers.toString().getBytes()));broadcast("ONUSR:" + username, client );}}else if (connectionTimes.containsKey(client)){// 还在登录阶段,不处理消息return;}else if(message.startsWith("4USER:")) {int userSplite = message.indexOf("#");String username = message.substring(6,userSplite);String messageContent ="4USER:" + message.substring(userSplite+1);SocketChannel toClient = clients.get(username);if (toClient!= null) {toClient.write(ByteBuffer.wrap(messageContent.getBytes()));}} else if(message.startsWith("4ALLS:")) {int userSplite = message.indexOf("#");String messageContent ="4ALLS:" + message.substring(userSplite+1);broadcast( messageContent, client);}} else if (read == -1) {String username = getUsernameByChannel(client);if (username != null) {clients.remove(username);broadcast("OFFUR:" + username ,client);}client.close();connectionTimes.remove(client);}} catch (IOException e) {// 处理客户端异常断开连接的情况String username = getUsernameByChannel(client);if (username != null) {clients.remove(username);broadcast("OFFUR:" + username ,client);}try {client.close();} catch (IOException ex) {ex.printStackTrace();}connectionTimes.remove(client);key.cancel();}}private void broadcast(String message) {for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {SocketChannel client = entry.getValue();try {client.write(ByteBuffer.wrap(message.getBytes()));} catch (IOException e) {e.printStackTrace();}}}private void broadcast( String message, SocketChannel sex) {for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {SocketChannel client = entry.getValue();try {if (client.equals(sex)) {continue;}client.write(ByteBuffer.wrap(message.getBytes()));} catch (IOException e) {e.printStackTrace();}}}private String getUsernameByChannel(SocketChannel channel) {for (Map.Entry<String, SocketChannel> entry : clients.entrySet()) {if (entry.getValue().equals(channel)) {return entry.getKey();}}return null;}public static void main(String[] args) {ChatServer server = new ChatServer();ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);// 固定延迟执行(上一次结束后延迟5秒执行下一次)// 但如果用户连接超过5秒,未登录闭连接,防止恶意攻击。executor.scheduleWithFixedDelay(() -> {// System.out.println("任务执行:" + System.currentTimeMillis());try { // 检查是否超时for (Map.Entry<SocketChannel, Long> entry : server.connectionTimes.entrySet()) {SocketChannel client = entry.getKey();Long connectTime = entry.getValue();if (connectTime != null && (new Date().getTime() - connectTime) > 5000) {// 由于在静态上下文中无法直接访问非静态字段,通过 server 实例来访问 connectionTimesserver.connectionTimes.remove(client);try {client.close();} catch (IOException e) {e.printStackTrace();}}}} catch (Exception e) {}}, 0,5, TimeUnit.SECONDS);// 固定频率执行(每3秒执行一次,无论任务耗时)// executor.scheduleAtFixedRate(() -> {// System.out.println("固定频率任务:" + System.currentTimeMillis());// }, 0, 3, TimeUnit.SECONDS);server.start();}
}
客户端代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;public class ChatClientUI {private static final String SERVER_HOST = "127.0.0.1";private static final int SERVER_PORT = 8888;private Selector selector;private SocketChannel socketChannel;private String username;private JFrame frame;private JTextField usernameField;private JTextField messageField;private JTextArea chatArea;private JButton loginButton;private JButton sendButton;private DefaultListModel<String> userListModel;private JList<String> userList;public ChatClientUI() {// 初始化界面组件frame = new JFrame("聊天客户端");frame.setSize(600, 600);frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);frame.setLayout(new BorderLayout());// 登录面板JPanel loginPanel = new JPanel();usernameField = new JTextField(15);loginButton = new JButton("登录");loginPanel.add(new JLabel("用户名:"));loginPanel.add(usernameField);loginPanel.add(loginButton);// 聊天区域chatArea = new JTextArea();chatArea.setEditable(false);JScrollPane scrollPane = new JScrollPane(chatArea);// 消息输入面板JPanel messagePanel = new JPanel(new FlowLayout()); // 显式设置 FlowLayoutmessageField = new JTextField(25);sendButton = new JButton("发送");sendButton.setEnabled(false);// 添加 JLabel 显示当前要发送给谁JLabel recipientLabel = new JLabel("当前发送给: 所有人");messagePanel.add(recipientLabel);messagePanel.add(messageField);messagePanel.add(sendButton);// 初始化在线用户列表userListModel = new DefaultListModel<>();userList = new JList<>(userListModel);JScrollPane userListScrollPane = new JScrollPane(userList);userListScrollPane.setPreferredSize(new Dimension(150, 0));// 使用 JSplitPane 分割聊天区域和在线用户列表JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, scrollPane, userListScrollPane);splitPane.setDividerLocation(450);frame.add(loginPanel, BorderLayout.NORTH);frame.add(splitPane, BorderLayout.CENTER);frame.add(messagePanel, BorderLayout.SOUTH);// 在线用户列表添加选择监听器userList.addListSelectionListener(e -> {if (!e.getValueIsAdjusting()) {String selectedUser = userList.getSelectedValue();if (selectedUser != null) {recipientLabel.setText("当前发送给: " + selectedUser);} else {recipientLabel.setText("当前发送给: 所有人");}}});// 登录按钮事件监听器loginButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {username = usernameField.getText().trim();// 验证用户名是否只包含字母和数字if (!username.matches("[a-zA-Z0-9]+") || username.length()>8) {// 使用弹出框警告JOptionPane.showMessageDialog(frame, "用户名只能包含字母和数字,请重新输入。", "警告", JOptionPane.WARNING_MESSAGE);return;}if (!username.isEmpty()) {try {connectToServer();loginButton.setEnabled(false);usernameField.setEditable(false);sendButton.setEnabled(true);} catch (IOException ex) {chatArea.append("连接服务器失败,请重试。\n");}} else {chatArea.append("请输入用户名。\n");}}});// 发送按钮事件监听器sendButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {String message = messageField.getText().trim();if (!message.isEmpty()) {try {// 获取选中的用户String selectedUser = userList.getSelectedValue();if (selectedUser != null && !selectedUser.equals("所有人")) {recipientLabel.setText("当前发送给: " + selectedUser);// 构造私聊消息格式chatArea.append("我@"+ selectedUser + "#:" +message+"\n"); message = "4USER:" + selectedUser + "#" + username + "@"+message;} else {recipientLabel.setText("当前发送给: 所有人");chatArea.append("我@所有人#:" +message+"\n"); message = "4ALLS:" + selectedUser + "#" + username + "@"+message;}sendMessage(message);messageField.setText("");} catch (Exception ex) {chatArea.append("发送消息失败,请重试。\n");}}}});frame.setVisible(true);}private void connectToServer() throws IOException {selector = Selector.open();socketChannel = SocketChannel.open();socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));socketChannel.register(selector, SelectionKey.OP_CONNECT);// 启动一个线程处理服务器消息new Thread(new ServerMessageHandler()).start();}private void sendMessage(String message) throws IOException {if (username != null) {if (message.startsWith("LOGIN:") == false) {socketChannel.write(ByteBuffer.wrap(message.getBytes()));}}}private class ServerMessageHandler implements Runnable {@Overridepublic void run() {try {while (true) {int count = selector.select();if (count > 0) {Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();if (key.isConnectable()) {SocketChannel client = (SocketChannel) key.channel();if (client.isConnectionPending()) {client.finishConnect();client.register(selector, SelectionKey.OP_READ);// 发送登录信息client.write(ByteBuffer.wrap(("LOGIN:" + username).getBytes()));}} else if (key.isReadable()) {/* 服务端消息定义:* 消息头:发送到那个客户端#那个客户端发来的@* 消息头 : 5个字符,例如:LOGIN 登录 ,OFFUR 下线,ONUSR 上线,ONALL 在线列表,4USER 发送给指定客户* 4ALLS 发送给所有客户端* * 客户端消息定义:消息头:那个客户端发来的@* 消息头有:OFFUR 下线,ONUSR 上线,ONALL 在线列表,4USER 发送给指定客户* 4ALLS 发送给所有客户端. 如果不在定义的消息头中,说明是错误信息,打印,并且让发送按钮不可用*/SocketChannel client = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int read = client.read(buffer);if (read > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);String message = new String(bytes).trim();System.out.println(message);// 处理消息if (message.startsWith("ONALL:")) { // 更新在线列表SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {// 提取消息内容String privateMessage = message.substring(6);userListModel.clear();userListModel.addElement("所有人");String[] usernames = privateMessage.split(",");for (String username : usernames) {userListModel.addElement(username);}}});} else if (message.startsWith("4USER:")) { // 私聊消息// 提取消息内容int spliteIndex = message.indexOf("@");if (spliteIndex != -1) { // String sendUser = message.substring(6, spliteIndex);String privateMessage = message.substring(spliteIndex+1);chatArea.append("私聊:#" + sendUser + "#: " + privateMessage + "\n");}} else if (message.startsWith("4ALLS")){ // 群聊消息// 提取消息内容int spliteIndex = message.indexOf("@");if (spliteIndex != -1) { // String sendUser = message.substring(6, spliteIndex);String privateMessage = message.substring(spliteIndex+1);chatArea.append("群发:#" + sendUser + "#: " + privateMessage + "\n");}}else if (message.startsWith("ONUSR")) { // 有人登录,更新在线列表 chatArea.append("登录成功。\n");String usrName = message.substring(6);if (!userListModel.contains(usrName)) {SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {userListModel.addElement(usrName);}});}} else if (message.startsWith("OFFUR")) { // 有人退出,更新在线列表SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {String usrName = message.substring(6);userListModel.removeElement(usrName);}});}else{chatArea.append("错误:#" +message + "\n");sendButton.setEnabled(false);}}}iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}}public static void main(String[] args) {SwingUtilities.invokeLater(new Runnable() {@Overridepublic void run() {new ChatClientUI();}});}
}