多用户即时通信系统

涉及到的内容

1、项目框架设计
2、java面向对象编程
3、网络编程
4、多线程
5、IO流
6、Mysql / 使用集合充当内存数据库

项目开发流程简介

1、需求分析

  • 需求分析师:懂技术+行业
  • 出一个需求分析报告(白皮书),该项目功能,客户具体要求

2、设计阶段

  • 架构师 / 项目经理
  • 设计工作(UML类图,流程图,模块设计,数据库,架构)
  • 原型开发
  • 组建团队

3、编码阶段

  • 程序员 / 码农
  • 完成架构师的模块功能
  • 测试自己的模块

4、测试阶段

  • 测试工程师
  • 单元测试,测试用例,白盒测试,黑盒测试,集成测试

5、实施阶段

  • 实施工程师(开发能力 / 环境配置部署能力)
  • 项目正确的部署到客户的平台,并保证运行正常
  • 身体好 _

6、维护阶段

  • 发现bug解决 / 项目升级

需求分析

1、用户登录
2、拉取在线用户
3、无异常退出(客户端、服务端)
4、私聊
5、群发
6、发文件
7、服务器推送新闻

客户端界面设计

在这里插入图片描述

程序框架图在这里插入图片描述

代码实现

服务端

在这里插入图片描述
Message.java

package com.leaveriver.common;

import java.io.Serializable;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 12:03
 * @Version 1.0
 * 说明:
 */
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;   //为了保证序列化的兼容性。
    //消息发送者、和目标
    private String sender;
    private String getter;
    //消息内容
    private String content;
    //发送时间
    private String senderTime;
    //消息类型, 这个很重要
    private String mesType;

    //进行扩展,和文件相关的成员
    private byte[] fileBytes;
    private int fileLen = 0;
    private String srcFilePath;
    private String destFilePath;

    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 getSrcFilePath() {
        return srcFilePath;
    }

    public void setSrcFilePath(String srcFilePath) {
        this.srcFilePath = srcFilePath;
    }

    public String getDestFilePath() {
        return destFilePath;
    }

    public void setDestFilePath(String destFilePath) {
        this.destFilePath = destFilePath;
    }

    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 getSenderTime() {
        return senderTime;
    }

    public void setSenderTime(String senderTime) {
        this.senderTime = senderTime;
    }

    public String getMesType() {
        return mesType;
    }

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

MessageType.java

package com.leaveriver.common;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 12:09
 * @Version 1.0
 * 说明:
 */
public interface MessageType {

    String MESSAGE_LOGIN_SUCCEED = "1" ;//表示登录成功
    String MESSAGE_LOGIN_FAILED = "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_OUTPUT = "8"; //传输文件

    String MESSAGE_SERVER_MES = "9";  //服务端推送消息

}

User.java

package com.leaveriver.common;

import java.io.Serializable;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 12:01
 * @Version 1.0
 * 说明:
 */
public class User implements Serializable {

    private static final long serialVersionUID = 1L;   //为了保证序列化的兼容性。
    private String userID ;     //  用户id/用户名
    private String password;    //  密码

    public User() {
    }

    public User(String userID, String password) {
        this.userID = userID;
        this.password = password;
    }

    public String getUserID() {
        return userID;
    }

    public void setUserID(String userID) {
        this.userID = userID;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

ManageServerConnectClientThread .java

package com.leaveriver.service;

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

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 14:39
 * @Version 1.0
 * 说明:管理线程类
 */
public class ManageServerConnectClientThread {
    private static HashMap<String, ServerConnectClientThread> hashMap = new HashMap<>();

    public static boolean getOnline(String userId) {
        return hashMap.containsKey(userId);
    }

    public static HashMap<String, ServerConnectClientThread> getHashMap() {
        return hashMap;
    }

    public static void addServerConnectClientThread(
            String userId, ServerConnectClientThread serverConnectClientThread) {
        hashMap.put(userId, serverConnectClientThread);
    }
    
    public static ServerConnectClientThread getServerConnectClientThread(String userId){
        return hashMap.get(userId);
    }


    //拉取在线用户
    public static String getOnlineUser(){
        //集合遍历,遍历hashmap的key
        Iterator<String> iterator = hashMap.keySet().iterator();
        String onlineUserList = "";

        while (iterator.hasNext()) {
            onlineUserList += iterator.next().toString() + " " ;   //规定用空格拼接所有用户id

        }
        return onlineUserList;
    }

    //移除一个线程
    public static void removeServerConnectClientThread(String userId){
        hashMap.remove(userId);
    }
}

ManageUsers .java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.User;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author Administrator
 * @Date 2021/11/4 0004 12:37
 * @Version 1.0
 * 说明:
 */
public class ManageUsers {

    private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();


    //创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法的
    private static HashMap<String, User> validUsers = new HashMap<>();


    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"));
    }

    public static User get(String userId){
        return validUsers.get(userId);
    }
    public static void addOffLineDb(String getter, ArrayList<Message> message) {
        offLineDb.put(getter, message);
    }

    public static ArrayList<Message> getOffLineDb(String getter){
        return offLineDb.get(getter);
    }
    public static boolean containsOffLineDb(String getter) {
        return offLineDb.containsKey(getter);
    }

    public static void removeOffLineDb(String getter) {
        offLineDb.remove(getter);
    }
}

PushMesServerService .java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;
import com.leaveriver.utils.Utility;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Iterator;

/**
 * @Author Administrator
 * @Date 2021/11/3 0003 15:36
 * @Version 1.0
 * 说明:
 */
public class PushMesServerService extends Thread {

    @Override
    public void run() {

        while (true) {
            System.out.println("请输入你要推送的消息:");
            String newsMes = Utility.readString(100);
            //创建message对象
            Message message = new Message();
            message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
            message.setSender("服务器");
            message.setContent(newsMes);
            message.setSenderTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()));

            //创建完之后发送。要拿到目标用户的socket
            HashMap<String, ServerConnectClientThread> hashMap = ManageServerConnectClientThread.getHashMap();
            Iterator<String> iterator = hashMap.keySet().iterator();

            while (iterator.hasNext()) {
                String next = iterator.next();
                try {
                    OutputStream os = hashMap.get(next).getSocket().getOutputStream();
                    ObjectOutputStream oos = new ObjectOutputStream(os);
                    oos.writeObject(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("[服务器推送记录][" + message.getSenderTime() + "]" +
                    message.getSender() + "推送信息:" + newsMes);
        }
    }
}

ServerConnectClientThread.java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;

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

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 14:28
 * @Version 1.0
 * 说明:与客户端保持通信
 */
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() {
        //这里线程处于运行状态,可以发送/接受消息
        while (true) {
            try {
                System.out.println("服务器和客户端[" + userId + "]保持通信,读取数据。。。。");
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject();

                /*
                 *这块加入一个验证对方用户是否在线的检测
                 *1、看管理线程中,是否有对方用户的线程,没有说明他不在线
                 *  */

                if (!ManageServerConnectClientThread.getOnline(message.getGetter()) && (ManageUsers.get(message.getGetter()) != null)) {
                    //如果不在线,将信息存在 ConcurrentHashMap中
                    if (!ManageUsers.containsOffLineDb(message.getGetter())) {
                        ArrayList<Message> arrayList = new ArrayList<>();
                        arrayList.add(message);
                        ManageUsers.addOffLineDb(message.getGetter(), arrayList);
                    } else {
                        ArrayList<Message> offLineDb = ManageUsers.getOffLineDb(message.getGetter());
                        offLineDb.add(message);
                        ManageUsers.addOffLineDb(message.getGetter(), offLineDb);
                    }
                    //区分文件和普通消息,服务端控制台输出不同的信息
                    if (message.getMesType().equals(MessageType.MESSAGE_FILE_OUTPUT)) {
                        System.out.println("[离线文件记录][" + message.getSenderTime() + "]" +
                                message.getSender() + "发送文件给" + message.getGetter() + "\n" +
                                message.getSrcFilePath() + " 到 " + message.getDestFilePath());

                    } else {
                        System.out.println("[离线消息记录][" + message.getSenderTime() + "]" +
                                message.getSender() + " 对 " + message.getGetter() + "说: " + message.getContent());
                    }

                } else {


                    //后面会使用message
                    if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
                        System.out.println(message.getSender() + "要获取在线用户");
                        String onlineUser = ManageServerConnectClientThread.getOnlineUser();
                        //返回给客户端
                        //构建一个message对象,
                        Message mes = new Message();
                        mes.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
                        mes.setContent(onlineUser);
                        mes.setGetter(mes.getSender());
                        //写入
                        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                        oos.writeObject(mes);

                    } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
                        System.out.println(message.getSender() + "退出");
                        //
                        ManageServerConnectClientThread.removeServerConnectClientThread(message.getSender());
                        socket.close();
                        break; //退出循环
                    } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {

                        //在服务端输出一句话。记录
                        System.out.println("[私聊记录][" + message.getSenderTime() + "]" +
                                message.getSender() + " 对 " + message.getGetter() + "说: " + message.getContent());
                        //转发给目标用户
                        //获取对应用户的socket的输出流,将信息转发出去
                        ServerConnectClientThread scct = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter());
                        ObjectOutputStream oos = new ObjectOutputStream(scct.getSocket().getOutputStream());
                        oos.writeObject(message);

                    } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {


                        System.out.println("[群聊消息记录][" + message.getSenderTime() + "]" +
                                message.getSender() + " 对大家说: " + message.getContent());
                        HashMap<String, ServerConnectClientThread> hashMap = ManageServerConnectClientThread.getHashMap();
                        Iterator<String> iterator = hashMap.keySet().iterator();
                        while (iterator.hasNext()) {
                            String next = iterator.next();
                            if (!next.equals(message.getSender())) {
                                OutputStream os = hashMap.get(next).getSocket().getOutputStream();
                                ObjectOutputStream oos = new ObjectOutputStream(os);
                                oos.writeObject(message);
                            }
                        }
                    } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_OUTPUT)) {


                            System.out.println("[文件传输记录][" + message.getSenderTime() + "]" +
                                    message.getSender() + "发送文件给" + message.getGetter() + "\n" +
                                    message.getSrcFilePath() + " 到 " + message.getDestFilePath());
                            ServerConnectClientThread scct = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter());
                            OutputStream os = scct.getSocket().getOutputStream();
                            ObjectOutputStream oos = new ObjectOutputStream(os);
                            oos.writeObject(message);

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

        }
    }
}

ServerService.java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;
import com.leaveriver.common.User;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 14:12
 * @Version 1.0
 * 说明:这是服务器,在监听9999,等待客户端的连接,并保持通信。
 */
public class ServerService {

    private ServerSocket serverSocket;//因为ServerSocket可能在其他地方要使用,所以也作为成员变量。


    //验证用户是否有效的方法
    private boolean checkUser(String userId, String password) {
        User user = ManageUsers.get(userId);
        if (user == null)
            return false;
        if (!user.getPassword().equals(password))
            return false;
        return true;
    }


    public ServerService() {
        //注意:端口可以写在配置文件中。
        try {
            System.out.println("服务端在9999端口监听。");

            new PushMesServerService().start();

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

                //得到socket关联的对象输出流,用于回复message给客户端
                ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());


                //读取从客户端发来的user对象。
                User user = (User) ois.readObject();

                //创建一个message对象,准备回复客户端
                Message message = new Message();
                if (checkUser(user.getUserID(), user.getPassword())) {
                    //账号密码正确,登录成功
                    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                    //回复客户端
                    oos.writeObject(message);
                    //创建一个线程与客户端保持通信。该线程需要持有socket对象。
                    //
                    ServerConnectClientThread scct = new ServerConnectClientThread(socket, user.getUserID());
                    scct.start();
                    ManageServerConnectClientThread.addServerConnectClientThread(user.getUserID(), scct);

                    //登录成功后,获取离线消息。
                    //在map中找 key为 user.getUserId()的,如果有,将value发送给该用户。
                    ArrayList<Message> offLineDb = ManageUsers.getOffLineDb(user.getUserID());
                    if (offLineDb != null)
                        for (int i = 0; i < offLineDb.size() ; i++) {
                            Message mes = offLineDb.get(i);
                            OutputStream outputStream = ManageServerConnectClientThread.getServerConnectClientThread(mes.getGetter()).getSocket().getOutputStream();
                            ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
                            objectOutputStream.writeObject(mes);
                            objectOutputStream.flush();

                            //获取发送完之后,就移除,不然下次登录还会发送。(服务器没重启的话。)
                            ManageUsers.removeOffLineDb(mes.getGetter());
                        }

                } else {
                    //如果验证失败,返回消息类型为2
                    message.setMesType(MessageType.MESSAGE_LOGIN_FAILED);
                    oos.writeObject(message);

                    //关闭socket
                    socket.close();
                }

            }

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

    }
}

Utility.java

package com.leaveriver.utils;


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

import java.util.Scanner;


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


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

    /**
     * 功能:读取键盘输入的一个字符
     */
    public static char readChar() {
        String str = readKeyBoard(1, false);//就是一个字符
        return str.charAt(0);
    }

    /**
     * 功能;读取键盘输入的一个字符,如果直接回车,则返回指定的默认值
     * defaultValue 指定的默认值
     * return 默认值或者输入的字符
     */
    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);//要么是空字符,要么??
        return str.length() == 0 ? defaultValue : str.charAt(0);
    }


    /**
     * 读取键盘输入的整型,长度少于两位
     * return 整数
     */
    public static int readInt() {
        int n;
        for (; ; ) {
            String str = readKeyBoard(2, false);
            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.println("选择错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的整数或者默认值,如果直接回车,则返回默认值
     * defaultValue 指定的默认值
     * return 整数或者默认值
     */
    public static int readInt(int defaultValue) {
        int n;
        for (; ; ) {
            String str = readKeyBoard(2, false);
            if (str.equals("")) {
                return defaultValue;
            }

            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.println("选择错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串
     * limit:限制的长度
     * return:指定长度的字符串
     */
    public static String readString(int limit) {
        return readKeyBoard(limit, false);
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串或者默认值,如果直接回车,返回默认值
     * limit:限制的长度
     * 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;
    }

    /**
     * 功能:读取键盘输入,如果输入为空,或者输入大于limit的长度,就会提示重新输入
     */
    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;
                else continue;//如果blackRuturn=false,不接受空串,必须输入内容
            }

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

QQFrame.java

package com.leaveriver.qqframe;
import com.leaveriver.service.ServerService;
/**
 * @Author Administrator
 * @Date 2021/10/29 0029 15:00
 * @Version 1.0
 * 说明:该类创建一个QQServer对象。启动后台的服务
 */
public class QQFrame {
    public static void main(String[] args) {

              new ServerService();
    }
}

客户端在这里插入图片描述

ClientConnectServerThread .java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;
import com.leaveriver.utils.Utility;

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

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 13:33
 * @Version 1.0
 * 说明:
 */
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) ois.readObject();   //如果服务器没有发送Message对象,线程会在这里阻塞
                //该message为与服务器通信的信息,后续再完善。
                //判断message的类型。然后做相应的业务处理。
                //如果读到的是服务端返回的在线用户列表
                if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {
                    //取出在线列表信息,并显示
                    //规定服务端返回的是一串用户id.中间用空格隔开。 那么这里接收时,就用空格划分。
                    String[] s = message.getContent().split(" ");
                    System.out.println("\n============当前在线用户列表=================");
                    for (int i = 0; i < s.length; i++) {
                        System.out.println("用户: " + s[i]);
                    }
                } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
                    System.out.println("\n [" + message.getSenderTime() + "] " + message.getSender() + "对你说: " + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
                    System.out.println("\n [" + message.getSenderTime() + "] " + message.getSender() + "对大家说: " + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_SERVER_MES)) {
                    System.out.println("\n [" + message.getSenderTime() + "] " + message.getSender() + "对大家说: " + message.getContent());
                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_OUTPUT)) {
                    //接受并保存在指定位置。
                    FileOutputStream fos = new FileOutputStream(message.getDestFilePath());
                    fos.write(message.getFileBytes());
                    fos.flush();
                    System.out.println("\n" + message.getSender() + "给你发送文件,保存在:" + message.getDestFilePath() + "路径");
                    System.out.println("保存成功");
                    fos.close();
                } else {
                    System.out.println("其他类型的信息, 这里暂时不处理");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //为了更方便得到Socket, 增加一个get方法
    public Socket getSocket() {
        return socket;
    }
}

FileService .java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;
import com.leaveriver.utils.StreamUtils;

import java.io.*;
import java.text.SimpleDateFormat;

/**
 * @Author Administrator
 * @Date 2021/11/3 0003 12:23
 * @Version 1.0
 * 说明:文件传输服务
 */
public class FileService {

    //    Message类进行扩展,和文件相关的成员
    //    private byte[] fileBytes;
    //    private int fileLen = 0;
    //    private String srcFilePath;
    //    private String destFilePath;
    //发送文件
    public void sendFile(String sender, String fileToUser, String srcFilePath, String destFilePath){
        //创建message对象。
        Message message = new Message();
        message.setSender(sender);
        message.setGetter(fileToUser);
        message.setMesType(MessageType.MESSAGE_FILE_OUTPUT);
        message.setSrcFilePath(srcFilePath);
        message.setDestFilePath(destFilePath);

        message.setSenderTime(new SimpleDateFormat(
                "yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()));



        //从磁盘上读取文件
        FileInputStream fis = null;
        byte[] fileBytes;
        try {
            fis = new FileInputStream(srcFilePath);
            fileBytes = new byte[(int) new File(srcFilePath).length()];
            fis.read(fileBytes);
            message.setFileBytes(fileBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //发送给服务器
        try {
            ObjectOutputStream oos = new ObjectOutputStream(
                    ManageClientConnetServerThread.getClientConnectServerThread(sender).getSocket().getOutputStream());
            oos.writeObject(message);
            System.out.println("文件发送完成===");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ManageClientConnetServerThread .java

package com.leaveriver.service;

import jdk.nashorn.internal.ir.CallNode;

import java.util.HashMap;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 13:48
 * @Version 1.0
 * 说明:管理线程类
 */
public class ManageClientConnetServerThread {
    //放入hashmap集合中,key为用户id,value为线程
    private static HashMap<String, ClientConnectServerThread> hashMap = new HashMap<>();

    //将某个线程加入到集合中。
    public static void  addClientConnectServerThread(
            String userId, ClientConnectServerThread clientConnectServerThread){
        hashMap.put(userId,clientConnectServerThread);
    }

    //通过userId可以得到对应的线程
    public static ClientConnectServerThread getClientConnectServerThread(String userId) {
        return hashMap.get(userId);
    }

}

MessageService .java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;
import com.leaveriver.common.User;
import com.leaveriver.utils.StreamUtils;
import com.leaveriver.utils.Utility;

import java.io.*;
import java.net.Socket;
import java.text.SimpleDateFormat;

/**
 * @Author Administrator
 * @Date 2021/11/2 0002 16:30
 * @Version 1.0
 * 说明:消息服务
 */
public class MessageService {


    //用户私聊
    public void userOneChat(String senderId, String chatContent, String chatToUserId) {
        //创建message对象。类型为 MESSAGE_USER_CHAT_ONE
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_COMM_MES);
        message.setSender(senderId);
        //将目标用户和内容都加入
        message.setGetter(chatToUserId);
        message.setContent(chatContent);
        //将当前时间设置为发送时间
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        message.setSenderTime(df.format(System.currentTimeMillis()));

        //发送

        try {

            ClientConnectServerThread ccst = ManageClientConnetServerThread.getClientConnectServerThread(senderId);
            OutputStream os = ccst.getSocket().getOutputStream();
            /*OutputStream os = socket.getOutputStream();*/
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(message);
            System.out.println("私聊内容发送成功");

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


    }

    //群发消息
    public void chatAllUsers(String sender, String content) {
        //创建message对象
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
        message.setSender(sender);


        //设置时间
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        message.setSenderTime(df.format(System.currentTimeMillis()));

        message.setContent(content);

        //发送

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

UserClientService .java

package com.leaveriver.service;

import com.leaveriver.common.Message;
import com.leaveriver.common.MessageType;
import com.leaveriver.common.User;
import com.leaveriver.utils.Utility;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 12:52
 * @Version 1.0
 * 说明:该类完成用户登录验证和用户注册等功能
 */
public class UserClientService {

    private User user = new User();  //  User类作为成员属性。 因为我们可能在其他地方要使用user信息
    private Socket socket;//因为socket可能在其他地方要使用,所以也作为成员变量。

    //根据id和密码到服务器验证是否合法
    public boolean checkUser(String userId, String password) {

        boolean b = false;    //该方法的返回值。 true为成功。
        //初始化User对象的id和密码
        user.setUserID(userId);
        user.setPassword(password);
        //连接到服务端,发送user对象
        try {
            socket = new Socket(InetAddress.getLocalHost(), 9999);
            //得到ObjectOutputStream对象
            OutputStream os = socket.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(user);//发送user对象

            //发送user对象到服务器后,服务器会验证账号密码,返回相应的message信息。由该信息可确认是否验证成功

            //读取从服务端回送的Message对象
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message message = (Message) ois.readObject();

            if (message.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {

                //创建一个和服务器端保持通信的线程 ---->创建一个类  ClientConnectServerThread
                ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
                //启动线程
                ccst.start();
                //为了后面客户端的拓展,我们将线程放在集合中。
                ManageClientConnetServerThread.addClientConnectServerThread(userId, ccst);


                //验证合法,返回true
                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(user.getUserID());
        try {
            //发送给服务器:
            //从线程集合中得到当前线程、
            //再拿到对应的socket,然后创建对应的输出流,直接发送message即可。
            /*ClientConnectServerThread ccst = ManageClientConnetServerThread.getClientConnectServerThread(user.getUserID());
            OutputStream os = ccst.getSocket().getOutputStream();*/
            OutputStream os = socket.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(os);
            oos.writeObject(message);

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

    }

    //编写方法, 退出客户端,并给服务器发送一个退出系统的message对象。
    public void systemExit(){

        //发送一个message,类型为   MESSAGE_CLIENT_EXIT
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(user.getUserID());

        //发送
        try {
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message);
            System.out.println("用户 ["+user.getUserID()+"] 退出");
            System.exit(0);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

StreamUtils .java

package com.leaveriver.utils;

import java.io.*;

public class StreamUtils {

    /*
    *streamToByteArray(),  该方法功能:将输入流转化为byte[],即可以把文件的内容读入到byte[]
    * */

    public static byte[] streamToByteArray(InputStream is) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();  //创建输出流对象
        byte[] bytes = new byte[1024];  //字节数组
        int readLen;
        while ((readLen = is.read(bytes)) != -1){ //循环读取
            bos.write(bytes, 0, readLen);       //把读取到数据写入bos
        }
        byte[] array = bos.toByteArray();  //然后将bos转成字节数组
        bos.close();
        return array;
    }

    public static String streamToString(InputStream is) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = bufferedReader.readLine()) != null){
            stringBuilder.append(line+"\n");
        }
        return stringBuilder.toString();
    }
}

Utility.java

package com.leaveriver.utils;


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

import java.util.Scanner;
import java.util.*;


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


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

    /**
     * 功能:读取键盘输入的一个字符
     */
    public static char readChar() {
        String str = readKeyBoard(1, false);//就是一个字符
        return str.charAt(0);
    }

    /**
     * 功能;读取键盘输入的一个字符,如果直接回车,则返回指定的默认值
     * defaultValue 指定的默认值
     * return 默认值或者输入的字符
     */
    public static char readChar(char defaultValue) {
        String str = readKeyBoard(1, true);//要么是空字符,要么??
        return str.length() == 0 ? defaultValue : str.charAt(0);
    }


    /**
     * 读取键盘输入的整型,长度少于两位
     * return 整数
     */
    public static int readInt() {
        int n;
        for (; ; ) {
            String str = readKeyBoard(2, false);
            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.println("选择错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的整数或者默认值,如果直接回车,则返回默认值
     * defaultValue 指定的默认值
     * return 整数或者默认值
     */
    public static int readInt(int defaultValue) {
        int n;
        for (; ; ) {
            String str = readKeyBoard(2, false);
            if (str.equals("")) {
                return defaultValue;
            }

            try {
                n = Integer.parseInt(str);
                break;
            } catch (NumberFormatException e) {
                System.out.println("选择错误,请重新输入:");
            }
        }
        return n;
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串
     * limit:限制的长度
     * return:指定长度的字符串
     */
    public static String readString(int limit) {
        return readKeyBoard(limit, false);
    }

    /**
     * 功能:读取键盘输入的指定长度的字符串或者默认值,如果直接回车,返回默认值
     * limit:限制的长度
     * 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;
    }

    /**
     * 功能:读取键盘输入,如果输入为空,或者输入大于limit的长度,就会提示重新输入
     */
    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;
                else continue;//如果blackRuturn=false,不接受空串,必须输入内容
            }

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

}

View.java

package com.leaveriver.view;

import com.leaveriver.service.FileService;
import com.leaveriver.service.MessageService;
import com.leaveriver.service.UserClientService;
import com.leaveriver.utils.Utility;

/**
 * @Author Administrator
 * @Date 2021/10/29 0029 12:16
 * @Version 1.0
 * 说明:
 */
public class View {
    private boolean loop = true; //控制是否显示主菜单
    private String key = ""; //接收用户的键盘输入

    private UserClientService userClientService = new UserClientService(); //用于登录服务器。
    private MessageService messageService = new MessageService();  //消息服务
    private FileService fileService = new FileService(); //文件服务
/*    public static void main(String[] args) {
        new View().mainMenu();
        System.out.println("退出系统。");
    }*/
    //显示主菜单
    public 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);  //工具类。 读取字符串,控制在1个即可
            //根据用户的输入,来处理不同的逻辑
            switch (key){
                case "1" :
                    System.out.print("请输入用户号:");
                    String userID = Utility.readString(20);
                    System.out.print("请输入密  码:");
                    String password = Utility.readString(20);

                    //到这里,需要构建user对象,去服务端验证。
                    //这里编写一个类Service ,用于用户登录/注册服务
                    boolean b = userClientService.checkUser(userID, password);
                    if (b) {
                        System.out.println("=============欢迎 (用户" + userID +")=============");
                        //进入到二级菜单
                        while (loop) {
                            System.out.println("=============网络通讯系统二级菜单 (用户" + 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);  //工具类。 读取字符串,控制在1个即可

                            //具体实现后面完善
                            switch (key){
                                case "1":
                                    //这里在Service中编写一个方法实现。
                                    userClientService.onlineFriendList();
                                    break;
                                case "2":
                                    //System.out.println("群发消息");
                                    System.out.print("请输入群发消息:");
                                    String chatAll = Utility.readString(50);
                                    //编写群发方法, 规定接受方为all
                                    messageService.chatAllUsers(userID, chatAll);
                                    break;
                                case "3":
                                    //输入要私聊的对象用户名:
                                    System.out.print("请输入要私聊的对象用户名:");
                                    String chatToUserId = Utility.readString(20);
                                    //输入私聊内容:
                                    System.out.print("私聊内容:");
                                    String chatContent = Utility.readString(100);
                                    //这里在Service中编写一个方法实现。
                                    messageService.userOneChat(userID, chatContent, chatToUserId);

                                    break;
                                case "4":
                                    System.out.println("发送文件");
                                    System.out.print("你要发送文件给用户: ");
                                    String fileToUser = Utility.readString(20);
                                    System.out.print("你要发送的文件的完整路径: ");
                                    String fileStartPath = Utility.readString(50);
                                    System.out.print("保存下对方的路径: ");
                                    String fileTarPath = Utility.readString(50);


                                    //编写一个方法 实现。
                                    fileService.sendFile(userID, fileToUser, fileStartPath,fileTarPath);

                                    break;
                                case "9": //退出菜单
                                    //调用方法,给服务器发送一个退出系统的message
                                    userClientService.systemExit();
                                    loop = false;
                                    break;
                            }
                        }
                    }else {  //登录系统失败
                        System.out.println("============登录失败==================");

                    }
                    break;
                case "9" :
                    loop = false;
                    break;


            }
        }
    }
}

QQFrame.java

package com.leaveriver.qqframe;
import com.leaveriver.view.View;
/**
 * @Author Administrator
 * @Date 2021/10/29 0029 15:02
 * @Version 1.0
 * 说明:
 */
public class QQFrame {
    public static void main(String[] args) {
        View view = new View();
        view.mainMenu();
        System.out.println("退出系统。");
    }
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

__Loren

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值