Implementation of Multi-user Instant Messaging System in Java

Articles and codes have been archived in [Github warehouse: https://github.com/timerring/java-tutorial] or the public account [AIShareLab] can also be obtained by replying to java .

project involves

  • Project Framework Design
  • java object oriented programming
  • network programming
  • Multithreading
  • I流
  • Mysql/Using collections as in-memory databases

Project development process

image-20230508194843670

demand analysis

  1. User login
  2. Pull the list of online users
  3. Exit without exception (client, server)
  4. private chat
  5. group chat
  6. send file
  7. server push news

Function realization - user login

Function Description

Because we haven’t learned the database yet, we artificially specify username/id = 100 and password 123456 to log in, and other users cannot log in.
Later, we use HashMap to simulate the database, and multiple users can log in.

A client will also have multiple threads to contact the server.

Function realization - pull online user list

Function implementation - exit without exception

slightly

Function realization - private chat

Function realization - group chat

slightly

Function Description - Send Documents

Function realization - server push news

client

`-- com
    `-- hspedu
        |-- qqclient
        |   |-- service
        |   |   |-- ClientConnectServerThread.java
        |   |   |-- FileClientService.java
        |   |   |-- ManageClientConnectServerThread.java
        |   |   |-- MessageClientService.java
        |   |   `-- UserClientService.java
        |   |-- utils
        |   |   `-- Utility.java
        |   `-- view
        |       `-- QQView.java
        `-- qqcommon
            |-- Message.java
            |-- MessageType.java
            `-- User.java

QQView

package com.hspedu.qqclient.view;

import com.hspedu.qqclient.service.FileClientService;
import com.hspedu.qqclient.service.MessageClientService;
import com.hspedu.qqclient.service.UserClientService;
import com.hspedu.qqclient.utils.Utility;

/**
 * 客户端的菜单界面
 */
@SuppressWarnings("all")
public class QQView {
    
    

    private boolean loop = true; //控制是否显示菜单
    private String key = ""; // 接收用户的键盘输入
    private UserClientService userClientService = new UserClientService();//对象是用于登录服务/注册用户
    private MessageClientService messageClientService = new MessageClientService();//对象用户私聊/群聊.
    private FileClientService fileClientService = new FileClientService();//该对象用户传输文件
    public static void main(String[] args) {
    
    
        new QQView().mainMenu();
        System.out.println("客户端退出系统.....");
    }

    //显示主菜单
    private void mainMenu() {
    
    

        while (loop) {
    
    

            System.out.println("===========欢迎登录网络通信系统===========");
            System.out.println("\t\t 1 登录系统");
            System.out.println("\t\t 9 退出系统");
            System.out.print("请输入你的选择: ");
            key = Utility.readString(1);

            //根据用户的输入,来处理不同的逻辑
            switch (key) {
    
    
                case "1":
                    System.out.print("请输入用户号: ");
                    String userId = Utility.readString(50);
                    System.out.print("请输入密  码: ");
                    String pd = Utility.readString(50);
                    //这里就比较麻烦了, 需要到服务端去验证该用户是否合法
                    //这里有很多代码, 我们这里编写一个类 UserClientService[用户登录/注册]
                    if (userClientService.checkUser(userId, pd)) {
    
     //还没有写完, 先把整个逻辑打通....
                        System.out.println("===========欢迎 (用户 " + userId + " 登录成功) ===========");
                        //进入到二级菜单
                        while (loop) {
    
    
                            System.out.println("\n=========网络通信系统二级菜单(用户 " + userId + " )=======");
                            System.out.println("\t\t 1 显示在线用户列表");
                            System.out.println("\t\t 2 群发消息");
                            System.out.println("\t\t 3 私聊消息");
                            System.out.println("\t\t 4 发送文件");
                            System.out.println("\t\t 9 退出系统");
                            System.out.print("请输入你的选择: ");
                            key = Utility.readString(1);
                            switch (key) {
    
    
                                case "1":
                                    //这里老师准备写一个方法,来获取在线用户列表
                                    userClientService.onlineFriendList();
                                    break;
                                case "2":
                                    System.out.println("请输入想对大家说的话: ");
                                    String s = Utility.readString(100);
                                    messageClientService.sendMessageToAll(s, userId);
                                    break;
                                case "3":
                                    System.out.print("请输入想聊天的用户号(在线): ");
                                    String getterId = Utility.readString(50);
                                    System.out.print("请输入想说的话: ");
                                    String content = Utility.readString(100);
                                    //编写一个方法,将消息发送给服务器端
                                    messageClientService.sendMessageToOne(content, userId, getterId);
                                    break;
                                case "4":
                                    System.out.print("请输入你想把文件发送给的用户(在线用户): ");
                                    getterId = Utility.readString(50);
                                    System.out.print("请输入发送文件的路径(形式 d:\\xx.jpg)");
                                    String src = Utility.readString(100);
                                    System.out.print("请输入把文件发送到对应的路径(形式 d:\\yy.jpg)");
                                    String dest = Utility.readString(100);
                                    fileClientService.sendFileToOne(src,dest,userId,getterId);
                                    break;
                                case "9":
                                    //调用方法,给服务器发送一个退出系统的message
                                    userClientService.logout();
                                    loop = false;
                                    break;
                            }

                        }
                    } else {
    
     //登录服务器失败
                        System.out.println("=========登录失败=========");
                    }
                    break;
                case "9":
                    loop = false;
                    break;
            }

        }

    }
}

com/hspedu/qqcommon/Message.java

package com.hspedu.qqcommon;

import java.io.Serializable;

/**
 * 表示客户端和服务端通信时的消息对象
 */
public class Message implements Serializable {
    
    
    private static final long serialVersionUID = 1L;
    private String sender;//发送者
    private String getter;//接收者
    private String content;//消息内容
    private String sendTime;//发送时间
    private String mesType;//消息类型[可以在接口定义消息类型]

    //进行扩展 和文件相关的成员
    private byte[] fileBytes;
    private int fileLen = 0;
    private String dest; //将文件传输到哪里
    private String src; //源文件路径

    public byte[] getFileBytes() {
    
    
        return fileBytes;
    }

    public void setFileBytes(byte[] fileBytes) {
    
    
        this.fileBytes = fileBytes;
    }

    public int getFileLen() {
    
    
        return fileLen;
    }

    public void setFileLen(int fileLen) {
    
    
        this.fileLen = fileLen;
    }

    public String getDest() {
    
    
        return dest;
    }

    public void setDest(String dest) {
    
    
        this.dest = dest;
    }

    public String getSrc() {
    
    
        return src;
    }

    public void setSrc(String src) {
    
    
        this.src = src;
    }

    public String getMesType() {
    
    
        return mesType;
    }

    public void setMesType(String mesType) {
    
    
        this.mesType = mesType;
    }

    public String getSender() {
    
    
        return sender;
    }

    public void setSender(String sender) {
    
    
        this.sender = sender;
    }

    public String getGetter() {
    
    
        return getter;
    }

    public void setGetter(String getter) {
    
    
        this.getter = getter;
    }

    public String getContent() {
    
    
        return content;
    }

    public void setContent(String content) {
    
    
        this.content = content;
    }

    public String getSendTime() {
    
    
        return sendTime;
    }

    public void setSendTime(String sendTime) {
    
    
        this.sendTime = sendTime;
    }
}

com/hspedu/qqcommon/MessageType.java

package com.hspedu.qqcommon;

/**
 * 表示消息类型
 */
public interface MessageType {
    
    
    //老师解读
    //1. 在接口中定义了一些常量
    //2. 不同的常量的值,表示不同的消息类型.
    String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功
    String MESSAGE_LOGIN_FAIL = "2"; // 表示登录失败
    String MESSAGE_COMM_MES = "3"; //普通信息包
    String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表
    String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表
    String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出
    String MESSAGE_TO_ALL_MES = "7"; //群发消息报
    String MESSAGE_FILE_MES = "8"; //文件消息(发送文件)
}

com/hspedu/qqcommon/User.java

package com.hspedu.qqcommon;

import java.io.Serializable;

/**
 * 表示一个用户/客户信息
 */
public class User implements Serializable {
    
    

    private static final long serialVersionUID = 1L;
    private String userId;//用户Id/用户名
    private String passwd;//用户密码

    public User() {
    
    }
    public User(String userId, String passwd) {
    
    
        this.userId = userId;
        this.passwd = passwd;
    }

    public String getUserId() {
    
    
        return userId;
    }

    public void setUserId(String userId) {
    
    
        this.userId = userId;
    }

    public String getPd() {
    
    
        return passwd;
    }

    public void setPasswd(String passwd) {
    
    
        this.passwd = passwd;
    }
}

com/hspedu/qqclient/utils/Utility.java

package com.hspedu.qqcommon;

import java.io.Serializable;

/**
 * 表示一个用户/客户信息
 */
public class User implements Serializable {
    
    

    private static final long serialVersionUID = 1L;
    private String userId;//用户Id/用户名
    private String passwd;//用户密码

    public User() {
    
    }
    public User(String userId, String passwd) {
    
    
        this.userId = userId;
        this.passwd = passwd;
    }

    public String getUserId() {
    
    
        return userId;
    }

    public void setUserId(String userId) {
    
    
        this.userId = userId;
    }

    public String getPd() {
    
    
        return passwd;
    }

    public void setPasswd(String passwd) {
    
    
        this.passwd = passwd;
    }
}

com/hspedu/qqclient/service/ClientConnectServerThread.java

package com.hspedu.qqclient.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;

public class ClientConnectServerThread extends Thread {
    
    
    //该线程需要持有Socket
    private Socket socket;

    //构造器可以接受一个Socket对象
    public ClientConnectServerThread(Socket socket) {
    
    
        this.socket = socket;
    }

    //
    @Override
    public void run() {
    
    
        //因为Thread需要在后台和服务器通信,因此我们while循环
        while (true) {
    
    

            try {
    
    
                System.out.println("客户端线程,等待从读取从服务器端发送的消息");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                //如果服务器没有发送Message对象,线程会阻塞在这里
                Message message = (Message) ois.readObject();
                //注意,后面我们需要去使用message
                //判断这个message类型,然后做相应的业务处理
                //如果是读取到的是 服务端返回的在线用户列表
                if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {
    
    
                    //取出在线列表信息,并显示
                    //规定
                    String[] onlineUsers = message.getContent().split(" ");
                    System.out.println("\n=======当前在线用户列表========");
                    for (int i = 0; i < onlineUsers.length; i++) {
    
    
                        System.out.println("用户: " + onlineUsers[i]);
                    }

                } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
    
    //普通的聊天消息
                    //把从服务器转发的消息,显示到控制台即可
                    System.out.println("\n" + message.getSender()
                            + " 对 " + message.getGetter() + " 说: " + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
    
    
                    //显示在客户端的控制台
                    System.out.println("\n" + message.getSender() + " 对大家说: " + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
    
    //如果是文件消息
                    //让用户指定保存路径。。。
                    System.out.println("\n" + message.getSender() + " 给 " + message.getGetter()
                            + " 发文件: " + message.getSrc() + " 到我的电脑的目录 " + message.getDest());

                    //取出message的文件字节数组,通过文件输出流写出到磁盘
                    FileOutputStream fileOutputStream = new FileOutputStream(message.getDest(), true);
                    fileOutputStream.write(message.getFileBytes());
                    fileOutputStream.close();
                    System.out.println("\n 保存文件成功~");

                } else {
    
    
                    System.out.println("是其他类型的message, 暂时不处理....");
                }

            } catch (Exception e) {
    
    
                e.printStackTrace();
            }

        }

    }

    //为了更方便的得到Socket
    public Socket getSocket() {
    
    
        return socket;
    }
}

com/hspedu/qqclient/service/FileClientService.java

package com.hspedu.qqclient.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;

import java.io.*;

/**
 * 该类/对象完成 文件传输服务
 */
public class FileClientService {
    
    
    /**
     *
     * @param src 源文件
     * @param dest 把该文件传输到对方的哪个目录
     * @param senderId 发送用户id
     * @param getterId 接收用户id
     */
    public void sendFileToOne(String src, String dest, String senderId, String getterId) {
    
    

        //读取src文件  -->  message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_FILE_MES);
        message.setSender(senderId);
        message.setGetter(getterId);
        message.setSrc(src);
        message.setDest(dest);

        //需要将文件读取
        FileInputStream fileInputStream = null;
        byte[] fileBytes = new byte[(int)new File(src).length()];

        try {
    
    
            fileInputStream = new FileInputStream(src);
            fileInputStream.read(fileBytes);//将src文件读入到程序的字节数组
            //将文件对应的字节数组设置message
            message.setFileBytes(fileBytes);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            //关闭
            if(fileInputStream != null) {
    
    
                try {
    
    
                    fileInputStream.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
        //提示信息
        System.out.println("\n" + senderId + " 给 " + getterId + " 发送文件: " + src
                + " 到对方的电脑的目录 " + dest);
        //发送
        try {
    
    
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }


    }
}

com/hspedu/qqclient/service/ManageClientConnectServerThread.java

package com.hspedu.qqclient.service;

import java.util.HashMap;

/**
 * 该类管理客户端连接到服务器端的线程的类
 */
public class ManageClientConnectServerThread {
    
    
    //我们把多个线程放入一个HashMap集合,key 就是用户id, value 就是线程
    private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();

    //将某个线程加入到集合
    public static void addClientConnectServerThread(String userId, ClientConnectServerThread clientConnectServerThread) {
    
    
        hm.put(userId, clientConnectServerThread);
    }
    //通过userId 可以得到对应线程
    public static ClientConnectServerThread getClientConnectServerThread(String userId) {
    
    
        return hm.get(userId);
    }

}

com/hspedu/qqclient/service/MessageClientService.java

package com.hspedu.qqclient.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;

/**
 * 该类/对象,提供和消息相关的服务方法
 */
public class MessageClientService {
    
    


    /**
     * @param content  内容
     * @param senderId 发送者
     */
    public void sendMessageToAll(String content, String senderId) {
    
    
        //构建message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_TO_ALL_MES);//群发消息这种类型
        message.setSender(senderId);
        message.setContent(content);
        message.setSendTime(new Date().toString());//发送时间设置到message对象
        System.out.println(senderId + " 对大家说 " + content);
        //发送给服务端

        try {
    
    
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * @param content  内容
     * @param senderId 发送用户id
     * @param getterId 接收用户id
     */
    public void sendMessageToOne(String content, String senderId, String getterId) {
    
    
        //构建message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_COMM_MES);//普通的聊天消息这种类型
        message.setSender(senderId);
        message.setGetter(getterId);
        message.setContent(content);
        message.setSendTime(new Date().toString());//发送时间设置到message对象
        System.out.println(senderId + " 对 " + getterId + " 说 " + content);
        //发送给服务端

        try {
    
    
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

    }
}

com/hspedu/qqclient/service/UserClientService.java

package com.hspedu.qqclient.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.qqcommon.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
 * 该类完成用户登录验证和用户注册等功能.
 */
public class UserClientService {
    
    

    // 因为我们可能在其他地方用使用user信息, 因此作出成员属性
    private User u = new User();
    // 因为Socket在其它地方也可能使用,因此作出属性
    private Socket socket;

    // 根据userId 和 pd 到服务器验证该用户是否合法
    public boolean checkUser(String userId, String pd) {
    
    
        boolean b = false;
        //创建User对象
        u.setUserId(userId);
        u.setPasswd(pd);


        try {
    
    
            // 连接到服务端,发送u对象
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            // 得到ObjectOutputStream对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(u);//发送User对象

            // 读取从服务器回复的 Message 对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message ms = (Message) ois.readObject();

            if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {
    
    // 登录OK

                // 创建一个和服务器端保持通信的线程-> 创建一个类 ClientConnectServerThread
                ClientConnectServerThread clientConnectServerThread =
                        new ClientConnectServerThread(socket);
                // 启动客户端的线程
                clientConnectServerThread.start();
                // 这里为了后面客户端的扩展,我们将线程放入到集合管理
                ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);
                b = true;
            } else {
    
    
                // 如果登录失败, 我们就不能启动和服务器通信的线程, 关闭 socket
                socket.close();
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return b;
    }

    // 向服务器端请求在线用户列表
    public void onlineFriendList() {
    
    

        // 发送一个Message , 类型MESSAGE_GET_ONLINE_FRIEND
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
        message.setSender(u.getUserId());

        // 发送给服务器

        try {
    
    
            // 从管理线程的集合中,通过userId, 得到这个线程对象
            ClientConnectServerThread clientConnectServerThread =
                    ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
            // 通过这个线程得到关联的socket
            Socket socket = clientConnectServerThread.getSocket();
            // 得到当前线程的Socket 对应的 ObjectOutputStream对象
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message); //发送一个Message对象,向服务端要求在线用户列表
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

    }

    // 编写方法,退出客户端,并给服务端发送一个退出系统的message对象
    public void logout() {
    
    
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(u.getUserId()); // 一定要指定我是哪个客户端id

        // 发送message
        try {
    
    
            // ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            ObjectOutputStream oos =
                    new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());
            oos.writeObject(message);
            System.out.println(u.getUserId() + " 退出系统 ");
            System.exit(0); // 结束进程
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

Service-Terminal

`-- com
    `-- hspedu
        |-- qqcommon
        |   |-- Message.java
        |   |-- MessageType.java
        |   `-- User.java
        |-- qqframe
        |   `-- QQFrame.java
        |-- qqserver
        |   `-- service
        |       |-- ManageClientThreads.java
        |       |-- QQServer.java
        |       |-- SendNewsToAllService.java
        |       `-- ServerConnectClientThread.java
        `-- utils
            `-- Utility.java

com/hspedu/qqcommon/Message.java

package com.hspedu.qqcommon;

import java.io.Serializable;

/**
 * 表示客户端和服务端通信时的消息对象
 */
public class Message implements Serializable {
    
    
    private static final long serialVersionUID = 1L;
    private String sender;//发送者
    private String getter;//接收者
    private String content;//消息内容
    private String sendTime;//发送时间
    private String mesType;//消息类型[可以在接口定义消息类型]

    //进行扩展 和文件相关的成员
    private byte[] fileBytes;
    private int fileLen = 0;
    private String dest; //将文件传输到哪里
    private String src; //源文件路径

    public byte[] getFileBytes() {
    
    
        return fileBytes;
    }

    public void setFileBytes(byte[] fileBytes) {
    
    
        this.fileBytes = fileBytes;
    }

    public int getFileLen() {
    
    
        return fileLen;
    }

    public void setFileLen(int fileLen) {
    
    
        this.fileLen = fileLen;
    }

    public String getDest() {
    
    
        return dest;
    }

    public void setDest(String dest) {
    
    
        this.dest = dest;
    }

    public String getSrc() {
    
    
        return src;
    }

    public void setSrc(String src) {
    
    
        this.src = src;
    }

    public String getMesType() {
    
    
        return mesType;
    }

    public void setMesType(String mesType) {
    
    
        this.mesType = mesType;
    }

    public String getSender() {
    
    
        return sender;
    }

    public void setSender(String sender) {
    
    
        this.sender = sender;
    }

    public String getGetter() {
    
    
        return getter;
    }

    public void setGetter(String getter) {
    
    
        this.getter = getter;
    }

    public String getContent() {
    
    
        return content;
    }

    public void setContent(String content) {
    
    
        this.content = content;
    }

    public String getSendTime() {
    
    
        return sendTime;
    }

    public void setSendTime(String sendTime) {
    
    
        this.sendTime = sendTime;
    }
}

com/hspedu/qqcommon/MessageType.java

package com.hspedu.qqcommon;

/**
 * 表示消息类型
 */
public interface MessageType {
    
    
    //1. 在接口中定义了一些常量
    //2. 不同的常量的值,表示不同的消息类型.
    String MESSAGE_LOGIN_SUCCEED = "1"; //表示登录成功
    String MESSAGE_LOGIN_FAIL = "2"; // 表示登录失败
    String MESSAGE_COMM_MES = "3"; //普通信息包
    String MESSAGE_GET_ONLINE_FRIEND = "4"; //要求返回在线用户列表
    String MESSAGE_RET_ONLINE_FRIEND = "5"; //返回在线用户列表
    String MESSAGE_CLIENT_EXIT = "6"; //客户端请求退出
    String MESSAGE_TO_ALL_MES = "7"; //群发消息报
    String MESSAGE_FILE_MES = "8"; //文件消息(发送文件)

}

com/hspedu/qqcommon/User.java

package com.hspedu.qqcommon;

import java.io.Serializable;

/**
 * 表示一个用户/客户信息
 */
public class User implements Serializable {
    
    
    // 如果一个对象需要通过对象流的方式读取,则该对象对应的类需要序列化。(IO讲的)!!!!

    // 保证兼容性
    private static final long serialVersionUID = 1L;
    private String userId;//用户Id/用户名
    private String passwd;//用户密码

    public User(String userId, String passwd) {
    
    
        this.userId = userId;
        this.passwd = passwd;
    }

    public String getUserId() {
    
    
        return userId;
    }

    public void setUserId(String userId) {
    
    
        this.userId = userId;
    }

    public String getPd() {
    
    
        return passwd;
    }

    public void setPasswd(String passwd) {
    
    
        this.passwd = passwd;
    }
}

com/hspedu/qqframe/QQFrame.java

package com.hspedu.qqframe;

import com.hspedu.qqserver.service.QQServer;

/**
 * 该类创建QQServer ,启动后台的服务
 */
public class QQFrame {
    
    
    public static void main(String[] args)  {
    
    
        new QQServer();
    }
}

com/hspedu/qqserver/service/ManageClientThreads.java

package com.hspedu.qqserver.service;

import java.util.HashMap;
import java.util.Iterator;

/**
 * 该类用于管理和客户端通信的线程
 */
public class ManageClientThreads {
    
    
    private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();

    // 返回 hm
    public static HashMap<String, ServerConnectClientThread> getHm() {
    
    
        return hm;
    }

    // 添加线程对象到 hm 集合
    public static void addClientThread(String userId, ServerConnectClientThread serverConnectClientThread) {
    
    
        hm.put(userId, serverConnectClientThread);
    }

    // 根据 userId 返回 ServerConnectClientThread 线程
    public static ServerConnectClientThread getServerConnectClientThread(String userId) {
    
    
        return hm.get(userId);
    }

    // 增加一个方法,从集合中,移除某个线程对象
    public static void removeServerConnectClientThread(String userId) {
    
    
        hm.remove(userId);
    }

    // 这里编写方法,可以返回在线用户列表
    public static String getOnlineUser() {
    
    
        //集合遍历 ,遍历 hashmap 的 key
        Iterator<String> iterator = hm.keySet().iterator();
        String onlineUserList = "";
        while (iterator.hasNext()) {
    
    
            onlineUserList += iterator.next().toString() + " ";
        }
        return  onlineUserList;
    }
}

com/hspedu/qqserver/service/QQServer.java

package com.hspedu.qqserver.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.qqcommon.User;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 这是服务器, 在监听9999,等待客户端的连接,并保持通信
 */
public class QQServer {
    
    

    private ServerSocket ss = null;
    //创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法
    //这里我们也可以使用 ConcurrentHashMap, 可以处理并发的集合,没有线程安全
    //HashMap 没有处理线程安全,因此在多线程情况下是不安全
    //ConcurrentHashMap 处理的线程安全,即线程同步处理, 在多线程情况下是安全
    private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
    //private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();

    static {
    
     //在静态代码块,初始化 validUsers

        validUsers.put("100", new User("100", "123456"));
        validUsers.put("200", new User("200", "123456"));
        validUsers.put("300", new User("300", "123456"));
        validUsers.put("至尊宝", new User("至尊宝", "123456"));
        validUsers.put("紫霞仙子", new User("紫霞仙子", "123456"));
        validUsers.put("菩提老祖", new User("菩提老祖", "123456"));

    }

    //验证用户是否有效的方法
    private boolean checkUser(String userId, String passwd) {
    
    

        User user = validUsers.get(userId);
        //过关的验证方式
        if(user == null) {
    
    //说明userId没有存在validUsers 的key中
            return  false;
        }
        if(!user.getPd().equals(passwd)) {
    
    //userId正确,但是密码错误
            return false;
        }
        return  true;
    }

    public QQServer() {
    
    
        //注意:端口可以写在配置文件.
        try {
    
    
            System.out.println("服务端在9999端口监听...");
            //启动推送新闻的线程
            new Thread(new SendNewsToAllService()).start();
            ss = new ServerSocket(9999);

            while (true) {
    
     //当和某个客户端连接后,会继续监听, 因此while
                Socket socket = ss.accept(); // 如果没有客户端连接,就会阻塞在这里
                //得到socket关联的对象输入流
                ObjectInputStream ois =
                        new ObjectInputStream(socket.getInputStream());

                //得到socket关联的对象输出流
                ObjectOutputStream oos =
                        new ObjectOutputStream(socket.getOutputStream());
                User u = (User) ois.readObject();//读取客户端发送的User对象
                //创建一个Message对象,准备回复客户端
                Message message = new Message();
                //验证用户 方法
                if (checkUser(u.getUserId(), u.getPd())) {
    
    //登录通过
                   message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                   //将message对象回复客户端
                    oos.writeObject(message);
                    //创建一个线程,和客户端保持通信, 该线程需要持有socket对象
                    ServerConnectClientThread serverConnectClientThread =
                            new ServerConnectClientThread(socket, u.getUserId());
                    //启动该线程
                    serverConnectClientThread.start();
                    //把该线程对象,放入到一个集合中,进行管理.
                    ManageClientThreads.addClientThread(u.getUserId(), serverConnectClientThread);

                } else {
    
     // 登录失败
                    System.out.println("用户 id=" + u.getUserId() + " pd=" + u.getPd() + " 验证失败");
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                    oos.writeObject(message);
                    //关闭socket
                    socket.close();
                }
            }

        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    

            //如果服务器退出了while,说明服务器端不在监听,因此关闭ServerSocket
            try {
    
    
                ss.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

com/hspedu/qqserver/service/SendNewsToAllService.java

package com.hspedu.qqserver.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.utils.Utility;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.*;

public class SendNewsToAllService implements Runnable {
    
    


    @Override
    public void run() {
    
    

        //为了可以推送多次新闻,使用while
        while (true) {
    
    
            System.out.println("请输入服务器要推送的新闻/消息[输入exit表示退出推送服务线程]");
            String news = Utility.readString(100);
            if("exit".equals(news)) {
    
    
                break;
            }
            //构建一个消息 , 群发消息
            Message message = new Message();
            message.setSender("服务器");
            message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
            message.setContent(news);
            message.setSendTime(new Date().toString());
            System.out.println("服务器推送消息给所有人 说: " + news);

            //遍历当前所有的通信线程,得到socket,并发送message

            HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();

            Iterator<String> iterator = hm.keySet().iterator();
            while (iterator.hasNext()) {
    
    
                String onLineUserId = iterator.next().toString();
                try {
    
    
                    ObjectOutputStream oos =
                            new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
                    oos.writeObject(message);
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }

    }
}

com/hspedu/qqserver/service/ServerConnectClientThread.java

package com.hspedu.qqserver.service;

import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;

/**
 * 该类的一个对象和某个客户端保持通信
 */
public class ServerConnectClientThread extends Thread {
    
    

    private Socket socket;
    private String userId;// 连接到服务端的用户id

    public ServerConnectClientThread(Socket socket, String userId) {
    
    
        this.socket = socket;
        this.userId = userId;
    }

    public Socket getSocket() {
    
    
        return socket;
    }

    @Override
    public void run() {
    
     // 这里线程处于run的状态,可以发送/接收消息
        // 读数据和写数据需要通过 socket
        while (true) {
    
    
            try {
    
    
                System.out.println("服务端和客户端" + userId + " 保持通信,读取数据...");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject();
                // 后面会使用message, 根据message的类型,做相应的业务处理
                if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
    
    
                    // 客户端要在线用户列表
                    /*
                        在线用户列表形式 100  200  紫霞仙子
                     */
                    System.out.println(message.getSender() + " 要在线用户列表");
                    String onlineUser = ManageClientThreads.getOnlineUser();
                    // 返回message
                    // 构建一个Message 对象,返回给客户端
                    Message message2 = new Message();
                    message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
                    message2.setContent(onlineUser);
                    message2.setGetter(message.getSender());
                    //返回给客户端
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(message2);

                } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
    
    
                    //根据message获取getter id, 然后在得到对应先线程
                    ServerConnectClientThread serverConnectClientThread =
                            ManageClientThreads.getServerConnectClientThread(message.getGetter());
                    //得到对应socket的对象输出流,将message对象转发给指定的客户端
                    ObjectOutputStream oos =
                            new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
                    oos.writeObject(message);//转发,提示如果客户不在线,可以保存到数据库,这样就可以实现离线留言

                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
    
    
                    //需要遍历 管理线程的集合,把所有的线程的socket得到,然后把message进行转发即可
                    HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();

                    Iterator<String> iterator = hm.keySet().iterator();
                    while (iterator.hasNext()) {
    
    

                        //取出在线用户id
                        String onLineUserId = iterator.next().toString();

                        if (!onLineUserId.equals(message.getSender())) {
    
    //排除群发消息的这个用户

                            //进行转发message
                            ObjectOutputStream oos =
                                    new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
                            oos.writeObject(message);
                        }

                    }

                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
    
    
                    //根据getter id 获取到对应的线程,将message对象转发
                    ObjectOutputStream oos =
                            new ObjectOutputStream(ManageClientThreads.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
                    //转发
                    oos.writeObject(message);
                } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
    
    //客户端退出

                    System.out.println(message.getSender() + " 退出");
                    //将这个客户端对应线程,从集合删除.
                    ManageClientThreads.removeServerConnectClientThread(message.getSender());
                    socket.close();//关闭连接
                    //退出线程
                    break;

                } else {
    
    
                    System.out.println("其他类型的message , 暂时不处理");
                }
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

com/hspedu/utils/Utility.java

package com.hspedu.utils;


/**
   工具类的作用:
   处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/

import java.util.Scanner;

/**

   
*/
public class Utility {
    
    
   //静态属性。。。
    private static Scanner scanner = new Scanner(System.in);

    
    /**
     * 功能:读取键盘输入的一个菜单选项,值:1——5的范围
     * @return 1——5
     */
   public static char readMenuSelection() {
    
    
        char c;
        for (; ; ) {
    
    
            String str = readKeyBoard(1, false);//包含一个字符的字符串
            c = str.charAt(0);//将字符串转换成字符char类型
            if (c != '1' && c != '2' && 
                c != '3' && c != '4' && c != '5') {
    
    
                System.out.print("选择错误,请重新输入:");
            } else break;
        }
        return c;
    }

   /**
    * 功能:读取键盘输入的一个字符
    * @return 一个字符
    */
    public static char readChar() {
    
    
        String str = readKeyBoard(1, false);//就是一个字符
        return str.charAt(0);
    }
    /**
     * 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符
     * @param defaultValue 指定的默认值
     * @return 默认值或输入的字符
     */
    
    public static char readChar(char defaultValue) {
    
    
        String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符
        return (str.length() == 0) ? defaultValue : str.charAt(0);
    }
   
    /**
     * 功能:读取键盘输入的整型,长度小于2位
     * @return 整数
     */
    public static int readInt() {
    
    
        int n;
        for (; ; ) {
    
    
            String str = readKeyBoard(10, false);//一个整数,长度<=10位
            try {
    
    
                n = Integer.parseInt(str);//将字符串转换成整数
                break;
            } catch (NumberFormatException e) {
    
    
                System.out.print("数字输入错误,请重新输入:");
            }
        }
        return n;
    }
    /**
     * 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数
     * @param defaultValue 指定的默认值
     * @return 整数或默认值
     */
    public static int readInt(int defaultValue) {
    
    
        int n;
        for (; ; ) {
    
    
            String str = readKeyBoard(10, true);
            if (str.equals("")) {
    
    
                return defaultValue;
            }
         
         //异常处理...
            try {
    
    
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
    
    
                System.out.print("数字输入错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串
     * @param limit 限制的长度
     * @return 指定长度的字符串
     */

    public static String readString(int limit) {
    
    
        return readKeyBoard(limit, false);
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串
     * @param limit 限制的长度
     * @param defaultValue 指定的默认值
     * @return 指定长度的字符串
     */
   
    public static String readString(int limit, String defaultValue) {
    
    
        String str = readKeyBoard(limit, true);
        return str.equals("")? defaultValue : str;
    }


   /**
    * 功能:读取键盘输入的确认选项,Y或N
    * 将小的功能,封装到一个方法中.
    * @return Y或N
    */
    public static char readConfirmSelection() {
    
    
        System.out.println("请输入你的选择(Y/N): 请小心选择");
        char c;
        for (; ; ) {
    
    //无限循环
           //在这里,将接受到字符,转成了大写字母
           //y => Y n=>N
            String str = readKeyBoard(1, false).toUpperCase();
            c = str.charAt(0);
            if (c == 'Y' || c == 'N') {
    
    
                break;
            } else {
    
    
                System.out.print("选择错误,请重新输入:");
            }
        }
        return c;
    }

    /**
     * 功能: 读取一个字符串
     * @param limit 读取的长度
     * @param blankReturn 如果为true ,表示 可以读空字符串。 
     *                   如果为false表示 不能读空字符串。
     *           
    * 如果输入为空,或者输入大于limit的长度,就会提示重新输入。
     * @return
     */
    private static String readKeyBoard(int limit, boolean blankReturn) {
    
    
        
      //定义了字符串
      String line = "";

      //scanner.hasNextLine() 判断有没有下一行
        while (scanner.hasNextLine()) {
    
    
            line = scanner.nextLine();//读取这一行
           
         //如果line.length=0, 即用户没有输入任何内容,直接回车
         if (line.length() == 0) {
    
    
                if (blankReturn) return line;//如果blankReturn=true,可以返回空串
                else continue; //如果blankReturn=false,不接受空串,必须输入内容
            }

         //如果用户输入的内容大于了 limit,就提示重写输入  
         //如果用户如的内容 >0 <= limit ,我就接受
            if (line.length() < 1 || line.length() > limit) {
    
    
                System.out.print("输入长度(不能大于" + limit + ")错误,请重新输入:");
                continue;
            }
            break;
        }

        return line;
    }
}

Guess you like

Origin blog.csdn.net/m0_52316372/article/details/130592998