Java网络编程的小结--多用户即时通信系统

一、前言

  千言万语化为一个项目,想说的都在这个项目里面了。这是用网络编程结合多线程完成的一个多用户即时通信系统,跟着韩老师敲出来的。最难最关键的部分在开头,看似仅仅是完成一个用户登录验证,实际上最重大的意义是打通了客户端和服务端的数据通道。后续私聊群聊发文件都是在数据通道上进行,只是根据不同的请求采取不同的逻辑处理。

二、项目概述

这是一个多用户即时通信系统,分为服务端和客户端。

  实现的功能有:
  • 登录验证(用HashMap + 静态代码块模拟数据库)
  • 拉取在线用户列表
  • 私聊
  • 群聊
  • 发文件
  • 服务器推送新闻
  具体步骤我写在注释里,这里说一下实现思路:

1.所有消息,不管类型如何(私聊群聊发文件等),都是封装成对象,用对象流发。
2.每个用户和服务器的连接都是一个线程,该线程持有socket。所以可以和服务器连接,且能实现多用户,并相对独立。
3.消息类型MessageType非常重要,服务端客户端都根据它采取不同的业务处理方式,它是message对象的一个属性。
4.一般消息处理:客户端发–>服务器收–>服务端业务处理–>服务器转发或回复–>客户端收–>客户端业务处理。
5.服务端把所有线程都放入到一个Map集合中,以便功能的实现,可以通过key(某用户的名字)找到该用户的数据通道。或者遍历集合得到所有用户的数据通道群发消息。

三、测试

登录验证

在这里插入图片描述

拉取在线用户列表

在这里插入图片描述

私聊

在这里插入图片描述

群聊

在这里插入图片描述

发文件

在这里插入图片描述

在这里插入图片描述

服务器推送新闻

在这里插入图片描述

四、项目结构

客户端:

在这里插入图片描述

服务端:

在这里插入图片描述

五、源代码

客户端:
view包
QQView (界面,业务逻辑框架)
package com.serein.qqclient.view;

import com.serein.qqclient.server.FileClientService;
import com.serein.qqclient.server.MessageClientService;
import com.serein.qqclient.server.UserClientServer;
import com.serein.qqclient.utils.Utility;

/**
 * 这个类是控制面板,也可以说是界面.
 * 是客户端是逻辑框架,调用各个类的方法来完成功能
 * 说几点:
 * 1.所有消息,不管类型如何(私聊群聊发文件等),都是封装成对象,用对象流发.
 * 2.每个用户和服务器的连接都是一个线程,该线程持有socket.所以可以和服务器连接.并且相对独立.
 * 3.消息类型MessageType非常重要,服务端客户端都根据它采取不同的业务处理方式
 * 4.一般消息处理:客户端发-->服务器收-->服务端业务处理-->服务器转发或回复-->客户端收-->客户端业务处理
 * 5.服务端把所有线程都放入到一个Map集合中,以便功能的实现,可以通过key(某用户的名字)找到该用户的数据通道。或者遍历集合得到所有用户的数据通道群发消息。
 */
public class QQView {
    private boolean loop  =true;//循环用的,业务逻辑置为false是退出循环
    private String key = "";//用户输入的指令
    //以下创建3个对象,以便调用相关功能
    private UserClientServer userClientServer = new UserClientServer();
    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("请输入你的选择:");
          //调用工具类,读取一位输入的字符.好处是不用定义scanner再使用,直接调用即可.
          key = Utility.readString(1);
          //根据key的值进入不同的业务逻辑
          switch (key){
              case "1":
                  System.out.println("登录系统");
                  System.out.print("请输入用户号:");
                  String userId = Utility.readString(50);
                  System.out.print("请输入密码:");
                  String pwd = Utility.readString(50);
                  //用户名密码是否正确.业务逻辑已经在相关方法里,这里是结果(true or false)
                  if (userClientServer.cheekUser(userId,pwd)){
                      System.out.println("======欢迎用户" + userId + "登陆成功==========");
                      //根据业务逻辑给loop赋值,看是否退出while循环
                      while (loop){
                          System.out.println("\n=======网络通信系统二级菜单" + userId + "==========");
                          System.out.println("\t\t1 显示在线用户列表");
                          System.out.println("\t\t2 群发消息");
                          System.out.println("\t\t3 私聊消息");
                          System.out.println("\t\t4 发送文件");
                          System.out.println("\t\t9 退出系统");
                          System.out.print("请输入你的选择:");
                          key = Utility.readString(1);
                          switch (key){
                              case "1":
                                  //调用拉取在线用户的方法
                                  userClientServer.olineFriendList();
                                  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":
                                  //退出系统,注意,专门写了一个退出的方法.因为这个while循环退出后,
                                  //由main方法启动的其他线程还没有退出,是要抛异常的.必须退出整个进程,详情见logout方法
                                  userClientServer.logout();
                                  loop = false;
                                  break;
                          }
                      }
                  }else {
                      System.out.println("=========登录失败========");
                  }
                  break;
              case "9":
                    loop = false;
                  break;
          }
      }
    }
}

server包
ClientConnectServerThread(主要业务处理逻辑)
package com.serein.qqclient.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;

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

/**
 * 该类是客户端,和服务器端的主要业务处理逻辑
 * 对服务端的回复做出处理
 */
public class ClientConnectServerThread extends Thread{
    //创建一个管道
    private Socket socket;
    public ClientConnectServerThread(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        //循环,以便客户端可以多次向服务端发送请求
        while (true){
            try {
                System.out.println("客户端线程,等待读取从服务端发送来的消息。。");
                //接收服务端的回应,读取服务端回复的消息
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                Message message = (Message) ois.readObject();
                //服务端回复了在线用户列表
                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.getContent());
                    //下载文件(消息正确且在线,则自动下载)
                    //创建字节输出流管道,说明下载路径(发送者定义的)
                    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();
            }
        }
    }

    public Socket getSocket() {
        return socket;
    }
}

FileClientService(文件发送的方法)
package com.serein.qqclient.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;

import java.io.*;

/**
 * 该类专门处理文件发送的方法
 */
public class FileClientService {
    public void sendFileToOne(String src,String dest,String senderId,String getterId){
        //设置消息的类型,这里是文件发送,定义好相关参数
        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);
            //把该数组写到消息属性里面去
            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();
        }
    }
}

ManageClientConnectServerThread (C–S的管道的集合)
package com.serein.qqclient.server;

import java.util.HashMap;

/**
 * 该类在客户端这一端管理和服务端的通信管道
 * 做成集合,可以满足用户既私聊又发文件.只有一个管道则一次只能实现一个功能
 */
public class ManageClientConnectServerThread {
    //key==>用户id,value===>线程
    private static HashMap<String,ClientConnectServerThread> hm = new HashMap<>();

    //把该线程(某需求)加入集合
    public static void addClientConnectServerThread(String usrID,ClientConnectServerThread clientConnectServerThread){
        hm.put(usrID,clientConnectServerThread);
    }
    public static ClientConnectServerThread getClientConnectServerThread(String uerID){
        return hm.get(uerID);
    }
}


MessageClientService (私聊和群发的办法)
package com.serein.qqclient.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;

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

/**
 * 该类提供私聊消息和群发的处理办法
 */
public class MessageClientService {

    //发消息给所有用户
    public void sendMessageToAll(String content,String senderId){
        //把相关参数封装成消息.参数传入发送者和内容即可.
        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);

        try {
            //创建发送者和服务器的输出流通道,把消息写到服务器
            ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //私聊的方法
    public void sendMessageToOne(String content,String senderId,String getterId){
        //封装该类型的消息,参数需要传入发送者,接受者,内容
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_COMM_MES);
        message.setSender(senderId);
        message.setGetter(getterId);
        message.setContent(content);
        message.setSendTime(new Date().toString());
        System.out.println(senderId + " 对 " + getterId + " 说:" + content);

        try {
            //创建发送者和服务端的输入流通道,把消息写入服务端.
            //服务端再找到接收用户并转发,那就是服务端的事情了.
            ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}



UserClientServer (用户本身操作登录退出等方法)
package com.serein.qqclient.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import com.serein.qqcommom.User;

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

/**
 * 该类主要提供用户在客户端的用户本身操作的一些方法
 */
public class UserClientServer {
    //创建一个用户对象,表示该该主体是自己
    private User u = new User();
    private Socket socket;

    //登录的验证,验证当然是服务端的事情.这里是,把用户名密码提交过去.
    // 然后根据服务器判断的结果做出相应处理
    //有那么点司法权(我判案子对错,怎么执行我不管)和行政权(事情是我来做的,原则上我要根据他的判断来做)分立的意思在里面
    public boolean cheekUser(String userId, String pwd) {
        //先默认false,等服务器验证通过后,再置为true
        boolean b = false;
        u.setUserId(userId);
        u.setPasswd(pwd);

        try {
            //建立输出通道,并且把用户类型的消息发给服务器
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(u);
            //读取服务器返回的结果,看用户密码是否合法
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Message ms = (Message) ois.readObject();
            //服务器认为用户名面正确
            if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {
                //创建一个线程并启动,即该用户上线了
                ClientConnectServerThread clientConnectServerThread = new ClientConnectServerThread(socket);
                clientConnectServerThread.start();
                //把该线程加入集合中,以便统一管理.当然,在客户端这边更大的意义在于后续扩展
                ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);
                //服务端说这是对的,所以我遵从这个结果,把判断置位true
                b = true;
            } else {
                //用户名密码错了,关闭管道
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //返回判断的结果
        return b;
    }

    //拉取在线用户列表
    public void olineFriendList(){
        //封装消息类型
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
        message.setSender(u.getUserId());

        try {//创建一个线程,是谁要拉取用户列表就创建一个他和服务器相连的线程
            ClientConnectServerThread clientConnectServerThread = ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());
            //该线程持有socket管道
            Socket socket = clientConnectServerThread.getSocket();
            //把消息写入管道
            ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
            oos.writeObject(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //退出登录
    public void logout(){
        //封装消息类型
        Message message = new Message();
        message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
        message.setSender(u.getUserId());

        try {
            //是谁退出,谁就创建一个和服务器相连的输出管道
            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();
        }
    }
}

utils包
Utility (工具类,主要是键盘读写)
package com.serein.qqclient.utils;


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

import java.util.*;
/**

	
*/
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;
    }
}

qqcommom包
Message (定义消息为对象,并给属性)
package com.serein.qqcommom;

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

MessageType (消息类型接口)
package com.serein.qqcommom;

/**
 * 这个接口定义了消息的类型,以便采取不同的方法处理不同的消息
 */
public interface MessageType {
    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";//发送文件
}

User (定义了用户的属性)
package com.serein.qqcommom;

import java.io.Serializable;

/**
 * 该类定义了用户的属性
 */
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    private String userId;
    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 getPasswd() {
        return passwd;
    }

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

服务端:
qqframe包
QQFrame (启动面板)
package com.serein.qqframe;

import com.serein.qqserver.server.QQServer;

/**
 * 该类是服务端启动面板
 */
public class QQFrame {
    public static void main(String[] args) {
        new QQServer();
    }
}

qqserver.server包
ManageClientThreads (管理通信线程)
package com.serein.qqserver.server;

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

/**
 * 该类是管理多个线程用的
 */
public class ManageClientThreads {
    private static HashMap<String, ServerClientClientThread> hm = new HashMap<>();

    public static HashMap<String,ServerClientClientThread> getHm(){
        return hm;
    }
    //把某用户的线程添加到服务器集合中
    public static void addClientThread(String userId, ServerClientClientThread serverClientClientThread) {
        hm.put(userId, serverClientClientThread);
    }

    public static ServerClientClientThread getServerClientClientThread(String userId) {
        return hm.get(userId);
    }
    //移除某个用户与服务器建立的连接
    public static ServerClientClientThread removeServerClientClientThread(String userId){
        return hm.remove(userId);
    }
    //拉取在线用户列表的方法,就是遍历map集合
    public static String getOnlineUser() {
        Iterator<String> iterator = hm.keySet().iterator();
        String onlineUserList = "";
        while (iterator.hasNext()){
            onlineUserList += iterator.next().toString() + "";
        }
        return onlineUserList;
    }
}

QQServer (登录验证服务,打通数据通道)
package com.serein.qqserver.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import com.serein.qqcommom.User;

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

/**
 * 该类的主要作用是,提供登录验证服务
 */
public class QQServer {
    private ServerSocket ss = null;
    //ConcurrentHashMap是线程安全的map集合
    private static ConcurrentHashMap<String,User> validUsers = new ConcurrentHashMap<>();
    //静态代码块,和类预加载用户登录名密码(模拟了数据库)
    static {
        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){
            return false;
        }
        if (!user.getPasswd().equals(passwd)){
            return false;
        }
        return true;
    }

    //启动服务器,打通了服务端和客户端的数据通道
    public QQServer() {
            try {
                System.out.println("服务端在 9999端口监听...");
                //启动服务端推送新闻的线程
                new Thread(new SendNewsToAllService()).start();
                ss = new ServerSocket(9999);
                //用while循环,一直保持读取客户端连接的状态
                while (true) {
                    //等待客户端连接
                    Socket socket = ss.accept();

                    ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                    ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
                    User u = (User) ois.readObject();

                    Message message = new Message();
                    //判断当前用户名密码是否合法
                    if (checkUser(u.getUserId(),u.getPasswd())) {
    //                if (true) {
                        message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
                        oos.writeObject(message);
                        //创建一个客户端与服务端连接的线程(通道)
                        ServerClientClientThread serverClientClientThread = new ServerClientClientThread(socket, u.getUserId());
                        serverClientClientThread.start();
                        //把这个线程添加到集合中,以便后续统一管理.
                        ManageClientThreads.addClientThread(u.getUserId(), serverClientClientThread);
                    } else {
                        //登录失败
                        System.out.println("用户 id=" + u.getUserId() + " pwd=" + u.getPasswd() + " 验证失败");
                        message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
                        oos.writeObject(message);
                        socket.close();
                    }
                }


            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    //关闭管道
                    ss.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

}

SendNewsToAllService (推送新闻)
package com.serein.qqserver.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;
import com.serein.utils.Utility;

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

/**
 * 该类的主要作用是,服务端向所有用户推送新闻.
 * 做成线程,以便独立启动与关闭
 */
public class SendNewsToAllService implements Runnable{

    @Override
    public void run() {


        while (true) {
            System.out.println("请输入服务器要推送的新闻/消息[输入exit表示退出推送服务线程]");
            String news = Utility.readString(100);
            //输入exit则退出该线程,关闭推送
            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);

            HashMap<String,ServerClientClientThread> hm = ManageClientThreads.getHm();
            //用迭代器遍历map集合,得到所有的用户id
            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();
                }
            }
        }
    }
}

ServerClientClientThread (主要的业务逻辑处理)
package com.serein.qqserver.server;

import com.serein.qqcommom.Message;
import com.serein.qqcommom.MessageType;

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

/**
 * 这个类是服务端,和客户端的通信管道,是主要的业务逻辑处理
 */
public class ServerClientClientThread extends Thread{
    private Socket socket;
    private String userId;

    public ServerClientClientThread(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();
                //客户端请求拉取在线用户列表
                if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){
                    System.out.println(message.getSender() + " 要在线用户列表");
                    //从管理线程的集合中,拿到所有线程的名字(因为一个线程代表一个客户端)(getOnlineUser方法
                    //就是遍历map集合的key)
                    String onlineUser = ManageClientThreads.getOnlineUser();
                    //把拿到的结果封装成消息
                    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)) {
                    //私聊消息,获取发消息的客户端,做一个和目标用户的线程
                    ServerClientClientThread serverClientClientThread = ManageClientThreads.getServerClientClientThread(message.getGetter());
                    //把该消息转发给目标用户
                    ObjectOutputStream oos = new ObjectOutputStream(serverClientClientThread.getSocket().getOutputStream());
                    oos.writeObject(message);
                } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
                    //群聊消息,先得到所有线程的集合
                    HashMap<String,ServerClientClientThread> hm = ManageClientThreads.getHm();
                    //用迭代器遍历集合,取出所有用户id
                    Iterator<String> iterator = hm.keySet().iterator();
                    while (iterator.hasNext()){
                        String onLineUserId = iterator.next().toString();
                        //给除自己外的所有用户转发该消息
                        if (!onLineUserId.equals(message.getSender())){
                            ObjectOutputStream oos = new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());
                            oos.writeObject(message);
                        }
                    }
                } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
                    //发送文件,创建一个与目标用户的写入通道,并写入
                    ObjectOutputStream oos = new ObjectOutputStream(ManageClientThreads.getServerClientClientThread(message.getGetter()).getSocket().getOutputStream());
                    oos.writeObject(message);
                } else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
                    //该用户打算退出系统
                    System.out.println(message.getSender() + " 退出");
                    //从管理线程的集合中移除该用户和服务器建立的连接(即相关线程)
                    ManageClientThreads.removeServerClientClientThread(message.getSender());
                    socket.close();
                    break;

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

qqcommom包
Message (定义消息为对象,并给属性)
package com.serein.qqcommom;

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

MessageType (消息类型接口)
package com.serein.qqcommom;

/**
 * 这个接口定义了消息的类型,以便采取不同的方法处理不同的消息
 */
public interface MessageType {
    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";//发送文件
}

User (定义了用户的属性)
package com.serein.qqcommom;

import java.io.Serializable;

/**
 * 该类定义了用户的属性
 */
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    private String userId;
    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 getPasswd() {
        return passwd;
    }

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

Utility (工具类,主要是键盘读写)
package com.serein.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;
    }
}

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
聊天系统采用客户机/服务器(C/S)地模式来设计,是一个3层地C/S结构:数据库服务器→应用程序服务器端→应用程序客户端,其分层结构如下图所示。系统采用C/S结构,可以将任务合理分配到客户机端和服务器端,从而降低了系统的通讯开销。 1. 客户层 客户层也叫应用表示层,是应用程序地客户接口部分。给聊天工具设计一个客户层具用很多优点,这是因为客户层担负着用户与应用间地对话功能。它用于检查用户的输入数据,显示应用的输出数据。为了使用户能直观的进行操作,客户层需要使用接口。若聊天用户变更,系统只需改写显示控制和数据检查程序即可,而不影响其他两层。数据检查的内容限于数据的形式和值得范围,不包括有关业务的处理逻辑。 2. 服务层 服务层又叫功能层,相当于应用的本体,他是讲具体的业务出路逻辑编入程序中。例如,用户需要检索数据,系统没法将有关检索要求的信息一次性的传送给功能层:而用户登陆后,聊天登录信息是由功能层处理过的检索结果数据,他也是一次性传送给表示层的。在应用设计中,不许避免在表示层和功能层之间进行多次的数据交换,这就需要尽可能进行一次性的业务处理,达到优化整体设计的目的。 3. 数据层 数据层就是DBMS,本聊天工具使用了Microsoft公司的SQL Server2000能迅速执行大量的更新和检索,因此,从功能层传送到数据层的“要求”一般都使用SQL语言。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值