Java笔记(二十八):多用户即时通信系统【下】(功能实现--私聊/群聊发文件/服务端推送消息/离线信息存储接收、虚拟机作为服务端代码修改)

五、系统代码设计流程

4、功能实现 - 私聊

1)思路

在这里插入图片描述

2)客户端代码修改

QQview类:新增对象属性,调用新增类的新方法

private MessageClientService messageClientService = new MessageClientService(); //用于调用和消息相关的方法

	case "3":
	   System.out.print("请输入要私聊的姓名(在线的):");
	   String getterId = Utility.readString(50);
	   System.out.print("请输入要私聊的内容:");
	   String sendContent = Utility.readString(100);
	   //调用方法,向服务端发message
	   messageClientService.sendMessageToOne(sendContent,userId,getterId);
	   break;

MessageClientService类:新增类

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

    /**
     * 私聊--客户端向服务端发送一个message对象
     * @param content 消息内容
     * @param senderId 发送者
     * @param getterId 接收者
     */
    public void sendMessageToOne(String content, String senderId, String getterId) {
        //包装message
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_COMM_MES);
        message.setGetter(getterId);
        message.setSender(senderId);
        message.setContent(content);
        message.setSendTime(new Date().toString()); //设置发送时间
        System.out.println(senderId + "对" + getterId + "说: " + content);

        //将message发送给服务端
        Socket socket = ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message);

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

    }
}

ClientConnectServerThread类:新增类型判断

else if(message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) { //Type3: 私聊
    String content = message.getContent();
    System.out.println("\n用户" + message.getGetter() + "接收到用户" + message.getSender() +
                        "发来的消息: " + message.getContent());
} 

3)服务端代码修改

ServerConnectClientThread:新增socket的getter方法,新增类型判断、业务处理

public Socket getSocket() {
	return socket;
}


else if(type.equals(MessageType.MESSAGE_COMM_MES)){ //Type3: 私聊
    System.out.println("【服务端】检测到用户" + message.getSender() + "想要和用户" + message.getGetter() + "私聊");
    //1. 获取私聊对象的对应socket
    Socket socket2 = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket();
    //2. 发送给对应私聊对象
    ObjectOutputStream oos = new ObjectOutputStream(socket2.getOutputStream());
    oos.writeObject(message); //发送的是原来的消息
} 

4)测试结果

在这里插入图片描述

5、功能实现 - 群聊

1)思路

服务端进行转发接收到的message时,遍历管理线程的集合

将message发送给所有在线的客户端线程(除了发送这个message的客户端)

2)MessageType类拓展

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

3)客户端代码修改

QQview类:新增类型判断

case "2":
    System.out.println("请输入想对大家说的话:");
    String sendContent2 = Utility.readString(100);
    //调用方法,向服务端发message
    messageClientService.sendMessageToAll(sendContent2,userId);
    break;

MessageClientService类:新增方法

    /**
     * 群发--客户端向服务端发送一个message对象,和其余所有人对话
     * @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()); //设置发送时间
        System.out.println(senderId +  "群发说: " + content);

        //将message发送给服务端
        Socket socket = ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message);

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

ClientConnectServerThread类:新增类型判断

else if(message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) { //Type3: 私聊
    System.out.println(message.getSender() + "对大家说: " + message.getContent());
} 

4)服务端代码修改

ServerConnectClientThread:新增类型判断

else if(type.equals(MessageType.MESSAGE_TO_ALL_MES)) { //Type2: 群发消息
    //遍历线程集合
    HashMap<String, ServerConnectClientThread> hm = ManageServerConnectClientThread.getHm();
    Set keyset = hm.keySet();
    Iterator iterator = keyset.iterator();
    while (iterator.hasNext()) {
        if(iterator.next() != message.getSender()){ //转发时,排除发消息的那个人
            //1. 获取每个在线客户端线程对应的socket
            Socket socket3 = hm.get(iterator.next()).getSocket();
            //2. 发送给对应私聊对象
            ObjectOutputStream oos = new ObjectOutputStream(socket3.getOutputStream());
            oos.writeObject(message); //发送的是原来的消息
        }
    }
}

ManageServerConnectClientThread类:新增方法

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

5)测试结果

在这里插入图片描述

6、功能实现 - 发文件

1)思路

在这里插入图片描述

2)Common包下类拓展

MessageType类

    String MESSAGE_FILE_MES = "8"; //文件消息(发送)

Message类

    //扩展==>和文件相关的成员
    private byte[] fileBytes; //文件字节数组
    private int fileLen = 0; //文件字节长度
    private String dest; //将文件传输到哪里
    private String src; //源文件路径
    
    //相应getter和setter
    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;
    }

3)客户端代码修改

QQview类

case "4":
	System.out.print("请输入想要发送文件的对象(在线的):");
	String getterId4 = Utility.readString(50);
	System.out.print("请输入你的文件路径(形式 d:\\\\xx.jpg):"); //要显示两个\要加转义字符
    String sendFileSrc = Utility.readString(100);
    System.out.print("请输入文件在接收方的路径(形式 d:\\\\yy.jpg):");
	String sendFileDest = Utility.readString(100);
	//调用方法
	fileClientService.sendFileToOne(sendFileSrc, sendFileDest, userId,getterId4);
break;

FileClientService类

/**
 * 该类/对象完成 文件传输服务
 */
public class FileClientService {

    /**
     *
     * @param src 源文件
     * @param dest 把该文件传输到对方的哪个目录
     * @param senderId 发送者
     * @param getterId 接收者
     */
    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.setDest(dest);
        message.setSrc(src);

        //1. 读取文件,存到message中
        String fileContext = "";
        BufferedInputStream bis = null;
        try {
            bis = new BufferedInputStream(new FileInputStream(src));
            byte[] fileBytes = new byte[(int)new File(src).length()]; //file.length: 求文件大小(字节单位)
            //文件先存到fileBytes数组中,再存进message对象中
            bis.read(fileBytes);
            message.setFileBytes(fileBytes);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        //2. 将message发送给服务端
        try {
            ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(message.getSender()).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(message.getSender() +" 给 " +message.getGetter() + " 发文件,保存到 " +message.getDest());
    }
}

ClientConnectServerThread类:新增类型判断

else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){ //Type4:获取文件
	System.out.println("\n用户" + message.getSender() + "将文件:" + message.getSrc() +
	        "发送给用户" + message.getGetter() + "的电脑上,路径为: " + message.getDest());
	//读取文件字节数组
	BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(message.getDest()));
	bos.write(message.getFileBytes());
	bos.close(); //出了这个代码块,这个写入任务就结束了,所以需要关闭流
	System.out.println("保存文件成功");
}

4)服务端代码修改

ServerConnectClientThread类:新增类型判断

else if(type.equals(MessageType.MESSAGE_FILE_MES)) { //Type2: 传输文件
    //1. 获取传输文件的目标socket
    Socket socket4 = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket();
    //2. 转发message对象
    ObjectOutputStream oos = new ObjectOutputStream(socket4.getOutputStream());
    oos.writeObject(message);
}

7、功能实现 - 服务端推送消息

1)思路

在这里插入图片描述

2)服务端代码修改

SendNewsToAllService类:新建类

/**
 * 推送,相当于服务器群发
 */
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 = ManageServerConnectClientThread.getHm();

            //使用values遍历得到客户线程
            Iterator<ServerConnectClientThread> iterator = hm.values().iterator();
            while (iterator.hasNext()) {
                try {
                    ObjectOutputStream oos = new ObjectOutputStream(iterator.next().getSocket().getOutputStream());
                    oos.writeObject(message);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            
        }
    }
}

ServerConnectClientThread:在构造器中启动推送线程

    System.out.println("服务端在9999端口监听...");
    ss = new ServerSocket(9999);
    //启动推送线程
    new Thread(new SendNewsToAllService()).start();

8、功能实现 - 离线信息存储接收

1)思路分析

在这里插入图片描述

2)MessageType类扩展

String MESSAGE_RET_NOT_ONLINE = "9"; //给对方发送消息/文件时,对方处于离线状态

3)服务端代码修改

OffLineService类

/**
 * 处理离线信息
 */
public class OffLineService {
    public static ConcurrentHashMap<String , ArrayList<Message>> offLineHm = new ConcurrentHashMap<>();

    static { //静态初始化
        offLineHm.put("100",new ArrayList<>());
        offLineHm.put("200",new ArrayList<>());
        offLineHm.put("300",new ArrayList<>());
        offLineHm.put("至尊宝",new ArrayList<>());
        offLineHm.put("紫霞仙子",new ArrayList<>());
        offLineHm.put("菩提老祖",new ArrayList<>());
    }

    public static ConcurrentHashMap<String, ArrayList<Message>> getOffLineHm() {
        return offLineHm;
    }

    //将离线信息存入offlinehm中
    public static void addMessage(String getterId, Message message){
        offLineHm.get(getterId).add(message);
        System.out.println("===存入离线消息成功===");
    }

    //这里检查离线集合里是否有离线信息或文件,如果有,就传,并且把这个用户离线信息从集合中清空,没有就跳过
    public static void offline(String getterId, Socket socket) {

        if(!offLineHm.get(getterId).isEmpty()){ //离线消息不为空
            System.out.println("离线消息集合中有用户" + getterId + "的消息");
            ArrayList<Message> messages = offLineHm.get(getterId);
            for (int i = 0; i < messages.size(); i++) {
                try {
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    oos.writeObject(messages.get(i));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            //清空所有离线消息
            offLineHm.get(getterId).clear();
        }
    }

}

QQServer类

//验证
if(checkUser(u.getUserId(),u.getPasswd())) { //登录成功
    message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
    oos.writeObject(message); //第一次返回的是登录成功的信息,用于告知双方可以开始建立连接,传数据了-->创建对应线程

    //登陆成功后调用方法,进行离线信息处理
    OffLineService.offline(u.getUserId(), socket);

    //创建一个线程,和客户端保持通信(接收客户端消息),该线程需要持有socket对象
    ServerConnectClientThread serverConnectClientThread =
            new ServerConnectClientThread(socket, u.getUserId());
    //启动线程
    serverConnectClientThread.start();
    //把该线程对象,放入到一个集合中,进行管理...
    ManageServerConnectClientThread.addServerConnectClientThread(u.getUserId(),
            serverConnectClientThread);

} 

ServerConnectClientThread类

else if(type.equals(MessageType.MESSAGE_COMM_MES)){ //Type3: 私聊
    System.out.println("【服务端】检测到用户" + message.getSender() + "想要和用户" + message.getGetter() + "私聊");

    //服务端检测该用户是否在线
    if(!ManageServerConnectClientThread.isOnline(message.getGetter())){ //接收对象不在线
        //将接收到的消息添加到offLineHm集合对应的ArrayList中
        OffLineService.addMessage(message.getGetter() , message);
        //给发送方回送一个信息  【服务器:对方不在线,上线会看到您的消息】
        Message message3 = new Message();
        message3.setMesType(MessageType.MESSAGE_RET_NOT_ONLINE);
        message3.setGetter(message.getSender());
        message3.setContent("服务器:对方不在线,上线会看到您的消息");
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
        oos.writeObject(message3);
    } else { //接收对象在线
        //1. 获取私聊对象的对应socket
        Socket socket2 = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket();
        //2. 发送给对应私聊对象
        ObjectOutputStream oos = new ObjectOutputStream(socket2.getOutputStream());
        oos.writeObject(message); //发送的是原来的消息
    }

} else if(type.equals(MessageType.MESSAGE_FILE_MES)) { //Type4: 传输文件

    //服务端检测该用户是否在线
    if(!ManageServerConnectClientThread.isOnline(message.getGetter())){ //接收对象不在线
        //将接收到的消息添加到offLineHm集合对应的ArrayList中
        OffLineService.addMessage(message.getGetter(),message);
        //给发送方回送一个信息  【服务器:对方不在线,上线会看到您的消息】
        Message message3 = new Message();
        message3.setMesType(MessageType.MESSAGE_RET_NOT_ONLINE);
        message3.setGetter(message.getSender());
        message3.setContent("服务器:对方不在线,上线会看到您的消息");
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
        oos.writeObject(message3);
    } else { //接收对象在线
        //1. 获取传输文件的目标socket
        Socket socket4 = ManageServerConnectClientThread.getServerConnectClientThread(message.getGetter()).getSocket();
        //2. 转发message对象
        ObjectOutputStream oos = new ObjectOutputStream(socket4.getOutputStream());
        oos.writeObject(message);
    }

}

【⭐】注意不能在QQserver中直接写离线信息处理的代码
(本来我想边遍历ArrayList集合边oos.write(message),结果疯狂报错,原因大概是oos被登录成功返回的第一个信息占用了-----也就是MESSAGE_LOGIN_SUCCEED那个。

报错地点是客户端接收服务端发来的消息的输入流ois,感觉就是流冲突问题?(具体没有搞懂,后面再说吧)

解决办法就是把离线信息处理封装起来扔到OffLineService类中,里面会额外创建一个oos,就可以了

4)客户端代码修改

ClientConnectServerThread类

else if(message.getMesType().equals(MessageType.MESSAGE_RET_NOT_ONLINE)) { //Type9:对方不在线
    System.out.println(message.getContent());
}

5)测试结果

在这里插入图片描述

六、虚拟机作为服务端–代码修改

  1. UserClientService
//主机-主机
socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
//主机-虚拟机
socket = new Socket(InetAddress.getByName("192.168.220.129"), 9999);

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值