Java高级Day40-QQ项目全代码
114.多用户通信系统(QQ)项目
QQServer包
===============//ManageClientThread//=============== public class ManageClientThread {//返回public static HashMap<String, ServerConnectClientThread> getHm(){return hm;}private static HashMap<String,ServerConnectClientThread> hm = new HashMap<>();//添加线程对象到 hm 集合public static void addClientThread(String userId, ServerConnectClientThread serverConnectClientThread) {hm.put(userId, serverConnectClientThread);}//根据userId 返回ServerConnectClientThread线程public static ServerConnectClientThread getServerConnectClientThread(String userId){return hm.get(userId);}//增加一个方法,从集合中,移除某个线程对象public static void removeServerConnectClientThread(String userId){hm.remove(userId);}//这里编写方法,可以返回在线用户列表public static String getOnlineUsers(){//集合遍历,遍历hashmap的keyIterator<String> iterator = hm.keySet().iterator();String onlineUserList = "";while (iterator.hasNext()){onlineUserList += iterator.next().toString() + " ";}return onlineUserList;} } ===============//Message//=============== public class Message implements Serializable {private static final long serialVersionUID = 1L;private String sender;//发送方private String getter;//接收方private String content;//消息内容private String sendTime;//发送时间private String mesType;//消息类型[可以在接口中定义消息类型]//进行扩展 和文件相关的成员private byte[] fileBytes;private int fileLen = 0;private String dest;//将文件传输到那里private String src;//源文件路径 public byte[] getFileBytes() {return fileBytes;} public void setFileBytes(byte[] fileBytes) {this.fileBytes = fileBytes;} public int getFileLen() {return fileLen;} public void setFileLen(int fileLen) {this.fileLen = fileLen;} public String getDest() {return dest;} public void setDest(String dest) {this.dest = dest;} public String getSrc() {return src;} public void setSrc(String src) {this.src = src;} public String getMesType() {return mesType;} public void setMesType(String mesType) {this.mesType = mesType;} public String getSender() {return sender;} public void setSender(String sender) {this.sender = sender;} public String getGetter() {return getter;} public void setGetter(String getter) {this.getter = getter;} public String getContent() {return content;} public void setContent(String content) {this.content = content;} public String getSendTime() {return sendTime;} public void setSendTime(String sendTime) {this.sendTime = sendTime;} } ===============//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";//群发消息String MESSAGE_FILE_MES = "8";//文件消息 } ===============//QQServer//=============== public class QQServer {private ServerSocket ss = null;//创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法的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("ZhangKewei", new User("ZhangKewei", "20040426")); }//验证用户是否有效的方法private boolean checkUser(String userId, String passwd) {User user = validUsers.get(userId);if (user == null) {//说明userId没有存在validUsers 的 key 中return false;}if (!(user.getPasswd().equals(passwd))) {//userId正确,密码错误return false;}return true;} public static void main(String[] args) {new QQServer();}public QQServer() {//注意:端口可以写在配置文件try {System.out.println("服务端在9999端口监听...");ss = new ServerSocket(9999); while (true) {//当和某个客户端连接后,会继续监听,因为while循环Socket socket = ss.accept();//得到Socket关联的对象输入流ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());//得到socket关联的对象输出流ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());User u = (User) ois.readObject();//读取客户端发送的User对象//创建一个Message对象,准备回复客户端Message message = new Message();//验证if (checkUser(u.getUserID(), u.getPasswd())){//合法用户message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);//将message对象回复oos.writeObject(message);//创建一个线程,和客户端保持通信,该线程需要持有socket对象ServerConnectClientThread serverConnectClientThread = new ServerConnectClientThread(socket, u.getUserID());//启动该线程serverConnectClientThread.start();//把该线程对象,放入到一个集合中,进行管理ManageClientThread.addClientThread(u.getUserID(), serverConnectClientThread);} else {//登陆失败System.out.println("用户 id=" + u.getUserID() + " pwd=" + u.getPasswd() + "验证失败");message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);oos.writeObject(message);//关闭socketsocket.close();}}} catch (Exception e) {e.printStackTrace();} finally {//如果服务器退出了while,说明服务器端不在监听,因此需要关闭ServerSockettry{ss.close();} catch (IOException e){e.printStackTrace();}}} } ===============//ServerConnectClientThread//=============== 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;} @Overridepublic void run() {//这里的线程处于run状态,可以发送接收信息while (true) {try {System.out.println("服务器端和客户端" + userId + "保持通讯,读取数据...");ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());Message message = (Message) ois.readObject();//后面会使用message,根据message的类型,做相应的业务处理if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)){//客户端要在线用户列表System.out.println(message.getSender() + "要在线用户列表");String onlineUsers = ManageClientThread.getOnlineUsers();//返回message//构建一个message对象,返回给客户端Message message2 = new Message();message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);message2.setContent(onlineUsers);message2.setGetter(message.getSender());//返回给客户端ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(message2); } else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)){//根据message获取getter id,在得到对应的线程ServerConnectClientThread serverConnectClientThread = ManageClientThread.getServerConnectClientThread(message.getGetter()); //得到对应的socket的对象输出流,将message对象转发给指定的客户ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());oos.writeObject(message);//转发,提示如果客户不在线,可以保存到数据库,这样就可以实现离线留言} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)){//需要遍历 管理线程的合集,把所有的线程的socket得到,然后把message进行转发HashMap<String, ServerConnectClientThread> hm = ManageClientThread.getHm();Iterator<String> iterator = hm.keySet().iterator();while (iterator.hasNext()){//取出在线用户的IDString onLineUserId = iterator.next().toString();if (!onLineUserId.equals(message.getSender())){//排除群发消息的用户//进行转发messageObjectOutputStream oos =new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());oos.writeObject(message);}} } else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {//根据getterid 获取到对应的线程,将message对象转发ServerConnectClientThread serverConnectClientThread = ManageClientThread.getServerConnectClientThread(message.getGetter());ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());//转发oos.writeObject(message);} else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)){//客户端退出System.out.println(message.getSender() + "退出");//将这个客户端对应的线程,从集合中删除ManageClientThread.removeServerConnectClientThread(message.getSender());socket.close();//关闭连接//退出线程break;}else {System.out.println("其他类型的message暂不处理");}} catch (Exception e) {e.printStackTrace();}}} } ===============//User//=============== public class User implements Serializable {private static final long serialVersionUID = 1L;private String userID;//用户IDprivate 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;} }
QQClientService包
===============//ClientConncetServerThread//=============== public class ClientConncetServerThread extends Thread {//该线程需要持有Socketprivate Socket socket; //构造器可以接收一个Socket对象public ClientConncetServerThread(Socket socket){this.socket = socket;} @Overridepublic void run() {//因为Thread需要在后台和服务器通信,所以我们要一个while循环while (true) {try {System.out.println("客户端线程,等待读取从服务器端发送的信息");ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());//如果服务器没有发送Message对象,线程会阻塞在这里Message message = (Message) ois.readObject();//判断这个message类型,然后做相应的业务处理//如果是读取到的是 服务端返回的在线用户列表if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {//取出在线列表信息,并显示String[] onlineUsers = message.getContent().split(" ");System.out.println("\n=====当前在线用户列表=====");for (int i = 0; i < onlineUsers.length; i ++){System.out.println("用户: " + onlineUsers[i]);}} else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)){//普通的聊天消息//把从服务器转发的消息,显示到控制台前即可System.out.println("\n" + message.getSender()+ " 对 " + message.getGetter() + " 说: " + message.getContent()); } else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)){//显示在客户端的控制台System.out.println("\n" + message.getSender() + " 对大家说: " + message.getContent());} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {//如果是文件消息System.out.println("\n" + message.getSender() + " 给 " + message.getGetter()+ " 发文件: " + message.getSrc() + " 到我的电脑目录 " + message.getDest());//取出message的文件字节数组,通过文件输出流写入到磁盘FileOutputStream fileOutputStream = new FileOutputStream(message.getDest());fileOutputStream.write(message.getFileBytes());fileOutputStream.close();System.out.println("\n 保存文件成功~"); } else {System.out.println("是其他类型的message,暂不处理...");}} catch (Exception e) {e.printStackTrace();}}} //为了更方便得到Socketpublic Socket getSocket() {return socket;} } ===============//FileClientService//=============== public class FileClientService {public void sendFileToOne(String src, String dest, String senderId, String getterId){//读取src文件 --> messageMessage message = new Message();message.setMesType(MessageType.MESSAGE_FILE_MES);message.setSender(senderId);message.setGetter(getterId);message.setSrc(src);message.setDest(dest);//需要将文件进行读取FileInputStream fileInputStream = null;byte[] fileBytes = new byte[(int) new File(src).length()];try{fileInputStream = new FileInputStream(src);fileInputStream.read(fileBytes);//将src文件读入到程序的字节数组//将文件的字节数设置到message对象message.setFileBytes(fileBytes);} catch (Exception e) {e.printStackTrace();}finally {//关闭if (fileInputStream != null) {try{fileInputStream.close();} catch (IOException e) {e.printStackTrace();}}}//提示信息System.out.println("\n" + senderId + " 给 " + getterId + " 发送文件: " + src+ " 到对的电脑的目录: " + dest);//发送try{ObjectOutputStream oos =new ObjectOutputStream(ManageClientConnectServerThread.getClientConncetServerThread(senderId).getSocket().getOutputStream());} catch (IOException e) {e.printStackTrace();}} } ===============//ManageClientConnectServerThread//=============== public class ManageClientConnectServerThread {//我们把多个线程放入一个HashMap集合,key 就是用户id;value 就是一个线程private static HashMap<String, ClientConncetServerThread> hm = new HashMap<>(); //将某个线程加入到集合public static void addClientConnectServerThread(String userId, ClientConncetServerThread clientConncetServerThread){hm.put(userId, clientConncetServerThread);} //通过userId,可以得到对应的线程public static ClientConncetServerThread getClientConncetServerThread(String userId){return hm.get(userId);} } ===============//UserClientService//=============== public class UserClientService {//因为可能在其他地方要使用User信息,所以做成属性private User u = new User();//因为Socket在其它地方也有可能使用,因此也做成属性private Socket socket;//根据userId 和 pwd 到服务器验证用户是否合法public boolean checkUser(String userId, String pwd) {boolean b = false;//创建User对象u.setUserID(userId);u.setPasswd(pwd);//连接到服务端,发送u对象try {socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);//得到ObjectOutputStream对象ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(u);//发送User对象 //读取从服务端回复的Message对象ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());Message ms = (Message) ois.readObject(); if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)){//登录成功 b = true;//创建一个和服务器端保持通信的线程 --> 创建一个类 ClientConnectServerThreadClientConncetServerThread clientConncetServerThread = new ClientConncetServerThread(socket);//启动客户端的线程clientConncetServerThread.start();//这里为了后面客户端的扩展,我们将线程放入到集合管理ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConncetServerThread); }else {//如果登陆失败,我们就不能启动和服务器通信的线程,关闭Socketsocket.close();} } catch (Exception e){e.printStackTrace();}return b;}//向服务器请求在线用户列表public void onlineFriendList() {//发送一个Message,类型MESSAGE_GET_ONLINE_FRIENDMessage message = new Message();message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);message.setSender(u.getUserID());//发送给服务器try{//应该得到当前线程的Sock 对应的 ObjectOutputStream对象ObjectOutputStream oos = new ObjectOutputStream(ManageClientConnectServerThread.getClientConncetServerThread(u.getUserID()).getSocket().getOutputStream());oos.writeObject(message);//发送一个message对象,向服务端要求在线用户列表} catch (IOException e) {e.printStackTrace();}}//编写方法,退出客户端,并给服务端发送一个退出系统的message对象public void logout() {Message message = new Message();message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);message.setSender(u.getUserID());//一定要指定我是哪个客户端id//发送messagetry{ObjectOutputStream oos =new ObjectOutputStream(ManageClientConnectServerThread.getClientConncetServerThread(u.getUserID()).getSocket().getOutputStream());oos.writeObject(message);System.out.println(u.getUserID() + " 退出系统");System.exit(0);//结束进程} catch (IOException e) {e.printStackTrace();}} }
QQClient.view包
===============//QQView//=============== public class QQView {private boolean loop = true;//控制是否显示菜单private String key = "";//接收用户的键盘输入private UserClientService userClientService = new UserClientService();//对象是用于登录服务器/注册用户private MessageClientService messageClientService = new MessageClientService();//对象用户私聊/群聊private FileClientService fileClientService = new FileClientService();//该对象用于传输文件 public static void main(String[] args) {new QQView().mainMenu();System.out.println("客户端退出系统...");}//显示主菜单private void mainMenu(){while (loop){System.out.println("===========欢迎登录网络通信系统==========");System.out.println("\t\t 1 登陆系统");System.out.println("\t\t 9 退出系统");System.out.print("请输入你的选择:"); key = utils.readString(1); //根据用户的输入,来处理不同的逻辑switch (key){case "1":System.out.print("请输入用户号:");String userID = utils.readString(50);System.out.print("请输入密 码:");String pwd = utils.readString(50);//需要到服务端验证该用户是否合法//这里有很多代码,所以在这里编写一个UserClientServiceif (userClientService.checkUser(userID, pwd)){System.out.println("===========欢迎(用户"+ userID +"登陆成功)==========");//进入二级菜单while (loop){System.out.println("\n===========网络通信二级菜单(用户"+ userID +")==========");System.out.println("\t\t 1 显示在线用户列表");System.out.println("\t\t 2 群发消息");System.out.println("\t\t 3 私聊消息");System.out.println("\t\t 4 发送文件");System.out.println("\t\t 9 退出系统");System.out.println("请输入你的选择:");key = utils.readString(1);switch (key){case "1":userClientService.onlineFriendList();break;case "2":System.out.println("请输入想对大家说的话");String s = utils.readString(100);messageClientService.sendMessageToAll(s, userID);//调用一个方法,将消息封装成message对象,发送给服务端break;case "3":System.out.println("请输入想聊天的用户号(在线):");String getterID = utils.readString(50);System.out.println("请输入想说的话");String content = utils.readString(100);//编写一个方法,将消息发送给服务端messageClientService.sendMessageToOne(content,userID,getterID);System.out.println("\t\t 3 私聊消息");break;case "4":System.out.println("请输入你想把文件发送给的用户(在线):");getterID = utils.readString(50);System.out.println("请输入发送文件的路径(形式 d:\\xx.jpg)");String src = utils.readString(100);System.out.println("请输入把文件发送到对方的路径(形式 d:\\xx.jpg)");String dest = utils.readString(100);fileClientService.sendFileToOne(src,dest,userID,getterID);break;case "9":loop = false;break;} }}else {//登录服务器失败System.out.println("==========登陆服务器失败==========");}break;case "9"://调用方法,给服务器发送一个退出系统的messageloop = false;break;}}} }
qqframe包
public class QQFrame {public static void main(String[] args) {new QQServer();} }
QQUtils
public class utils {//静态属性。。。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=>NString 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;} }