Java学习-第一部分-第三阶段-项目实战:多用户即时通讯系统

news/2024/4/29 13:16:43/文章来源:https://www.cnblogs.com/wenjie2000/p/16683379.html

多用户即时通讯系统

包含推消息 私聊 发文件 等功能

笔记目录:(https://www.cnblogs.com/wenjie2000/p/16378441.html)

为什么选择这个项目

  1. 有趣

  2. 涉及到java各个方面的技术

    ✔项目框架设计

    ✔java面向对象编程

    ✔网络编程

    ✔多线程

    ✔IO流

    ✔Mysql/ 学习前使用集合充当内存数据库

  3. 巩固旧知识,学习新知识

项目开发流程

需求分析-->设计阶段--->编码实现-->测试阶段-->实施阶段

需求分析

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

界面设计

  1. 用户登录

    image

  2. 拉取在线用户列表

    image

  3. 私聊

    image

  4. 群聊

    image

  5. 发文件

    image

  6. 文件服务器推送新闻

    image

功能实现-用户登录

  1. 功能说明

    因为还没有学习数据库,我们人为规定用户名/id = 100,密码123456就可以登录,其它用户不能登录

    后面使用HashMap模拟数据库,可以多个用户登录。

    image

  2. 思路分析+程序框架图

    image

  3. 代码实现(每部分功能代码实现较为分散,推荐看视频)

    P688~P695

功能实现-拉取在线用户列表

  1. 功能说明

    image

  2. 代码实现

    P696~P698

功能实现-无异常退出

  1. 功能说明

    某一端直接强行退出会造成另一端socket报错,因为使用了catch会导致死循环报错

  2. 思路分析+程序框架图

    image

  3. 代码实现

    P699~P700

功能实现-私聊

  1. 功能说明

    image

  2. 代码实现

    P701~P703

功能实现-群聊

  1. 功能说明

    image

  2. 代码实现

    P704

功能实现-发文件

  1. 功能说明

    image

  2. 思路分析+程序框架图

    image

  3. 代码实现

    P705~P707

功能实现-文件服务器推送新闻

  1. 功能说明

    image

  2. 思路分析+程序框架图

    image

  3. 代码实现

    P708

扩展功能-自己独立完成

  1. 功能说明

    提示:核心技术已经讲过了,小伙伴认真想想,是完全可以完成的,并不难.在实际工作中,独立解决问题的能力非常重要,必须有意识培养。

    1. 实现离线留言,如果某个用户没有在线,当登录后,可以接受离线的消息

    2. 实现离线发文件,如果某个用户没有在线,当登录后,可以接受离线的文件

  2. 实现思路+程序框架图

    image

  3. 代码实现

    这部分没写(以后可能会补上),实现并不难。

完整代码

服务器端

程序文件结构

image

代码

com.hspedu

qqcommon

Message

package com.hspedu.qqcommon;import java.io.Serializable;/*** 表示客户端和服务端通信时的消息对象*/
public class Message implements Serializable {private static final long serialVersionUID = 1L;private String sender;//发送者private String getter;//接收者private String content;//消息内容private String sendTime;//发送时间private String mesType;//消息类型[可以在接口定义消息类型]//进行扩展 和文件相关的成员private byte[] fileBytes;private int fileLen = 0;private String dest; //将文件传输到哪里private String src; //源文件路径public byte[] getFileBytes() {return fileBytes;}public void setFileBytes(byte[] fileBytes) {this.fileBytes = fileBytes;}public int getFileLen() {return fileLen;}public void setFileLen(int fileLen) {this.fileLen = fileLen;}public String getDest() {return dest;}public void setDest(String dest) {this.dest = dest;}public String getSrc() {return src;}public void setSrc(String src) {this.src = src;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;}public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}public String getGetter() {return getter;}public void setGetter(String getter) {this.getter = getter;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getSendTime() {return sendTime;}public void setSendTime(String sendTime) {this.sendTime = sendTime;}
}

MessageType

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

User

package com.hspedu.qqcommon;import java.io.Serializable;/*** 表示一个用户信息*/
public class User implements Serializable {private static final long serialVersionUID = 1L;private String userId;//用户Id/用户名private String passwd;//用户密码public User(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.hspedu.qqframe;import com.hspedu.qqserver.service.QQServer;/*** 该类创建QQServer ,启动后台的服务*/
public class QQFrame {public static void main(String[] args)  {new QQServer();}
}

qqserver.service

ManageClientThreads

package com.hspedu.qqserver.service;import java.util.HashMap;
import java.util.Iterator;/**
* 该类用于管理和客户端通信的线程
*/
public class ManageClientThreads {private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();//返回 hmpublic static HashMap<String, ServerConnectClientThread> getHm() {return hm;}//添加线程对象到 hm 集合public static void addClientThread(String userId, ServerConnectClientThread serverConnectClientThread) {hm.put(userId, serverConnectClientThread);}//根据userId 返回ServerConnectClientThread线程public static ServerConnectClientThread getServerConnectClientThread(String userId) {return hm.get(userId);}//增加一个方法,从集合中,移除某个线程对象public static void removeServerConnectClientThread(String userId) {hm.remove(userId);}//这里编写方法,可以返回在线用户列表public static String getOnlineUser() {//集合遍历 ,遍历 hashmap的keyIterator<String> iterator = hm.keySet().iterator();String onlineUserList = "";while (iterator.hasNext()) {onlineUserList += iterator.next() + " ";}return  onlineUserList;}
}

QQServer

package com.hspedu.qqserver.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.qqcommon.User;import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;/**
* 这是服务器, 在监听9999,等待客户端的连接,并保持通信
*/
public class QQServer {private ServerSocket ss = null;//创建一个集合,存放多个用户,如果是这些用户登录,就认为是合法//这里我们也可以使用 ConcurrentHashMap, 可以处理并发的集合,没有线程安全//HashMap 没有处理线程安全,因此在多线程情况下是不安全//ConcurrentHashMap 处理的线程安全,即线程同步处理, 在多线程情况下是安全private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();//private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();static { //在静态代码块,初始化 validUsersvalidUsers.put("100", new User("100", "123456"));validUsers.put("200", new User("200", "123456"));validUsers.put("300", new User("300", "123456"));validUsers.put("至尊宝", new User("至尊宝", "123456"));validUsers.put("紫霞仙子", new User("紫霞仙子", "123456"));validUsers.put("菩提老祖", new User("菩提老祖", "123456"));}//验证用户是否有效的方法private boolean checkUser(String userId, String passwd) {User user = validUsers.get(userId);//过关的验证方式if(user == null) {//说明userId没有存在validUsers 的key中return  false;}if(!user.getPasswd().equals(passwd)) {//userId正确,但是密码错误return false;}return true;}public QQServer() {//注意:端口可以写在配置文件.try {System.out.println("服务端在9999端口监听...");//启动推送新闻的线程new Thread(new SendNewsToAllService()).start();ss = new ServerSocket(9999);while (true) { //当和某个客户端连接后,会继续监听, 因此whileSocket 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();//把该线程对象,放入到一个集合中,进行管理.ManageClientThreads.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();}}}
}

SendNewsToAllService

package com.hspedu.qqserver.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.utils.Utility;import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.*;/**
* 发送新闻到所用用户
*/
public class SendNewsToAllService implements Runnable {@Overridepublic void run() {//为了可以推送多次新闻,使用whilewhile (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,并发送messageHashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();Iterator<String> iterator = hm.keySet().iterator();while (iterator.hasNext()) {String onLineUserId = iterator.next().toString();try {ObjectOutputStream oos =new ObjectOutputStream(hm.get(onLineUserId).getSocket().getOutputStream());oos.writeObject(message);} catch (IOException e) {e.printStackTrace();}}}}
}

ServerConnectClientThread

package com.hspedu.qqserver.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;/**
* 该类的一个对象和某个客户端保持通信, 根据用户发来的数据,进行对应操作
*/
public class ServerConnectClientThread extends Thread {private Socket socket;private String userId;//连接到服务端的用户idpublic 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)) {//返回在线用户列表//客户端要在线用户列表/*在线用户列表形式 100  200  紫霞仙子*/System.out.println(message.getSender() + " 要在线用户列表");String onlineUser = ManageClientThreads.getOnlineUser();//返回message//构建一个Message 对象,返回给客户端Message message2 = new Message();message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);message2.setContent(onlineUser);message2.setGetter(message.getSender());//返回给客户端ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(message2);} else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {//普通信息包(用户之间的文字消息)//根据message获取getter id, 然后在得到对应先线程ServerConnectClientThread serverConnectClientThread =ManageClientThreads.getServerConnectClientThread(message.getGetter());//得到对应socket的对象输出流,将message对象转发给指定的客户端ObjectOutputStream oos =new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());oos.writeObject(message);//转发,提示如果客户不在线,可以保存到数据库,这样就可以实现离线留言} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {//群发消息//需要遍历 管理线程的集合,把所有的线程的socket得到,然后把message进行转发即可HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();Iterator<String> iterator = hm.keySet().iterator();while (iterator.hasNext()) {//取出在线用户idString onLineUserId = iterator.next();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)) {//发送文件//根据getter id 获取到对应的线程,将message对象转发ObjectOutputStream oos =new ObjectOutputStream(ManageClientThreads.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());//转发oos.writeObject(message);} else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {//客户端退出System.out.println(message.getSender() + " 退出");//将这个客户端对应线程,从集合删除.ManageClientThreads.removeServerConnectClientThread(message.getSender());socket.close();//关闭连接//退出线程break;} else {System.out.println("其他类型的message , 暂时不处理");}} catch (Exception e) {e.printStackTrace();}}}
}

utils

Utility//此处为工具类,可直接复制使用,没必要自己写

package com.hspedu.utils;/**工具类的作用:处理各种情况的用户输入,并且能够按照程序员的需求,得到用户的控制台输入。
*/import java.util.Scanner;/***/
public class Utility {//静态属性。。。private static Scanner scanner = new Scanner(System.in);/*** 功能:读取键盘输入的一个菜单选项,值:1——5的范围* @return 1——5*/public static char readMenuSelection() {char c;for (; ; ) {String str = readKeyBoard(1, false);//包含一个字符的字符串c = str.charAt(0);//将字符串转换成字符char类型if (c != '1' && c != '2' && c != '3' && c != '4' && c != '5') {System.out.print("选择错误,请重新输入:");} else break;}return c;}/*** 功能:读取键盘输入的一个字符* @return 一个字符*/public static char readChar() {String str = readKeyBoard(1, false);//就是一个字符return str.charAt(0);}/*** 功能:读取键盘输入的一个字符,如果直接按回车,则返回指定的默认值;否则返回输入的那个字符* @param defaultValue 指定的默认值* @return 默认值或输入的字符*/public static char readChar(char defaultValue) {String str = readKeyBoard(1, true);//要么是空字符串,要么是一个字符return (str.length() == 0) ? defaultValue : str.charAt(0);}/*** 功能:读取键盘输入的整型,长度小于2位* @return 整数*/public static int readInt() {int n;for (; ; ) {String str = readKeyBoard(10, false);//一个整数,长度<=10位try {n = Integer.parseInt(str);//将字符串转换成整数break;} catch (NumberFormatException e) {System.out.print("数字输入错误,请重新输入:");}}return n;}/*** 功能:读取键盘输入的 整数或默认值,如果直接回车,则返回默认值,否则返回输入的整数* @param defaultValue 指定的默认值* @return 整数或默认值*/public static int readInt(int defaultValue) {int n;for (; ; ) {String str = readKeyBoard(10, true);if (str.equals("")) {return defaultValue;}//异常处理...try {n = Integer.parseInt(str);break;} catch (NumberFormatException e) {System.out.print("数字输入错误,请重新输入:");}}return n;}/*** 功能:读取键盘输入的指定长度的字符串* @param limit 限制的长度* @return 指定长度的字符串*/public static String readString(int limit) {return readKeyBoard(limit, false);}/*** 功能:读取键盘输入的指定长度的字符串或默认值,如果直接回车,返回默认值,否则返回字符串* @param limit 限制的长度* @param defaultValue 指定的默认值* @return 指定长度的字符串*/public static String readString(int limit, String defaultValue) {String str = readKeyBoard(limit, true);return str.equals("")? defaultValue : str;}/*** 功能:读取键盘输入的确认选项,Y或N* 将小的功能,封装到一个方法中.* @return Y或N*/public static char readConfirmSelection() {System.out.println("请输入你的选择(Y/N): 请小心选择");char c;for (; ; ) {//无限循环//在这里,将接受到字符,转成了大写字母//y => Y n=>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;}
}

客户端

程序文件结构

image

代码

com.hspedu

qqclient

service

ClientConnectServerThread

package com.hspedu.qqclient.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.net.Socket;/**
*该类为登录后创建的线程,和服务器保持通讯,根据服务器传回的数据进行对应操作
*/
public class ClientConnectServerThread extends Thread {//该线程需要持有Socketprivate Socket socket;//构造器可以接受一个Socket对象public ClientConnectServerThread(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//判断这个message类型,然后做相应的业务处理//如果是读取到的是 服务端返回的在线用户列表if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)) {//取出在线列表信息,并显示//规定String[] onlineUsers = message.getContent().split(" ");System.out.println("\n=======当前在线用户列表========");for (int i = 0; i < onlineUsers.length; i++) {System.out.println("用户: " + onlineUsers[i]);}} else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {//普通的聊天消息//把从服务器转发的消息,显示到控制台即可System.out.println("\n" + message.getSender()+ " 对 " + message.getGetter() + " 说: " + message.getContent());} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {//显示在客户端的控制台System.out.println("\n" + message.getSender() + " 对大家说: " + message.getContent());} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {//如果是文件消息//让用户指定保存路径。。。System.out.println("\n" + message.getSender() + " 给 " + message.getGetter()+ " 发文件: " + message.getSrc() + " 到我的电脑的目录 " + message.getDest());//取出message的文件字节数组,通过文件输出流写出到磁盘FileOutputStream fileOutputStream = new FileOutputStream(message.getDest(), true);fileOutputStream.write(message.getFileBytes());fileOutputStream.close();System.out.println("\n 保存文件成功~");} else {System.out.println("是其他类型的message, 暂时不处理....");}} catch (Exception e) {e.printStackTrace();}}}//为了更方便的得到Socketpublic Socket getSocket() {return socket;}
}

FileClientService

package com.hspedu.qqclient.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;import java.io.*;/**
* 该类/对象完成 文件传输服务
*/
public class FileClientService {/**** @param src 源文件* @param dest 把该文件传输到对方的哪个目录* @param senderId 发送用户id* @param getterId 接收用户id*/public void sendFileToOne(String src, String dest, String senderId, String getterId) {//读取src文件  -->  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文件读入到程序的字节数组//将文件对应的字节数组设置messagemessage.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

package com.hspedu.qqclient.service;import java.util.HashMap;/**
* 该类管理客户端连接到服务器端的线程的类
*/
public class ManageClientConnectServerThread {//我们把多个线程放入一个HashMap集合,key 就是用户id, value 就是线程private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();//将某个线程加入到集合public static void addClientConnectServerThread(String userId, ClientConnectServerThread clientConnectServerThread) {hm.put(userId, clientConnectServerThread);}//通过userId 可以得到对应线程public static ClientConnectServerThread getClientConnectServerThread(String userId) {return hm.get(userId);}}

MessageClientService

package com.hspedu.qqclient.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;/**
* 该类/对象,提供和消息相关的服务方法 发送信息给服务器
*/
public class MessageClientService {/*** @param content  内容* @param senderId 发送者*/public void sendMessageToAll(String content, String senderId) {//构建messageMessage message = new Message();message.setMesType(MessageType.MESSAGE_TO_ALL_MES);//群发消息这种类型message.setSender(senderId);message.setContent(content);message.setSendTime(new Date().toString());//发送时间设置到message对象System.out.println(senderId + " 对大家说 " + content);//发送给服务端try {ObjectOutputStream oos =new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());oos.writeObject(message);} catch (IOException e) {e.printStackTrace();}}/*** @param content  内容* @param senderId 发送用户id* @param getterId 接收用户id*/public void sendMessageToOne(String content, String senderId, String getterId) {//构建messageMessage message = new Message();message.setMesType(MessageType.MESSAGE_COMM_MES);//普通的聊天消息这种类型message.setSender(senderId);message.setGetter(getterId);message.setContent(content);message.setSendTime(new Date().toString());//发送时间设置到message对象System.out.println(senderId + " 对 " + getterId + " 说 " + content);//发送给服务端try {ObjectOutputStream oos =new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());oos.writeObject(message);} catch (IOException e) {e.printStackTrace();}}
}

UserClientService

package com.hspedu.qqclient.service;import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.qqcommon.User;import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;/**
* 该类向服务器发送信息 完成用户登录验证、退出登录和用户注册等功能.
*/
public class UserClientService {//因为我们可能在其他地方用使用user信息, 因此作出成员属性private User u = new User();//因为Socket在其它地方也可能使用,因此作出属性private Socket socket;//根据userId 和 pwd 到服务器验证该用户是否合法public boolean checkUser(String userId, String pwd) {boolean b = false;//创建User对象u.setUserId(userId);u.setPasswd(pwd);try {//连接到服务端,发送u对象socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);//得到ObjectOutputStream对象ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(u);//发送User对象//读取从服务器回复的Message对象ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());Message ms = (Message) ois.readObject();if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {//登录OK//创建一个和服务器端保持通信的线程-> 创建一个类 ClientConnectServerThreadClientConnectServerThread clientConnectServerThread =new ClientConnectServerThread(socket);//启动客户端的线程clientConnectServerThread.start();//这里为了后面客户端的扩展,我们将线程放入到集合管理ManageClientConnectServerThread.addClientConnectServerThread(userId, clientConnectServerThread);b = true;} 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 {//从管理线程的集合中,通过userId, 得到这个线程对象ClientConnectServerThread clientConnectServerThread =ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId());//通过这个线程得到关联的socketSocket socket = clientConnectServerThread.getSocket();//得到当前线程的Socket 对应的 ObjectOutputStream对象ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());oos.writeObject(message); //发送一个Message对象,向服务端要求在线用户列表} catch (IOException e) {e.printStackTrace();}}//编写方法,退出客户端,并给服务端发送一个退出系统的message对象public void logout() {Message message = new Message();message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);message.setSender(u.getUserId());//一定要指定我是哪个客户端id//发送messagetry {//ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());ObjectOutputStream oos =new ObjectOutputStream(ManageClientConnectServerThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());oos.writeObject(message);System.out.println(u.getUserId() + " 退出系统 ");System.exit(0);//结束进程} catch (IOException e) {e.printStackTrace();}}
}

utils

Utility

package com.hspedu.qqclient.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=>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;}
}

view

QQView

package com.hspedu.qqclient.view;import com.hspedu.qqclient.service.FileClientService;
import com.hspedu.qqclient.service.MessageClientService;
import com.hspedu.qqclient.service.UserClientService;
import com.hspedu.qqclient.utils.Utility;/**
* 登录界面和登录后的界面
*/
public class QQView {private boolean loop = true; //控制是否显示菜单private String key = ""; // 接收用户的键盘输入private UserClientService userClientService = new UserClientService();//对象是用于登录服务/注册用户private MessageClientService messageClientService = new MessageClientService();//对象用户私聊/群聊.private FileClientService fileClientService = new FileClientService();//该对象用户传输文件public static void main(String[] args) {new QQView().mainMenu();System.out.println("客户端退出系统.....");}//显示主菜单private void mainMenu() {while (loop) {System.out.println("===========欢迎登录网络通信系统===========");System.out.println("\t\t 1 登录系统");System.out.println("\t\t 9 退出系统");System.out.print("请输入你的选择: ");key = Utility.readString(1);//根据用户的输入,来处理不同的逻辑switch (key) {case "1":System.out.print("请输入用户号: ");String userId = Utility.readString(50);System.out.print("请输入密  码: ");String pwd = Utility.readString(50);//这里就比较麻烦了, 需要到服务端去验证该用户是否合法//这里有很多代码, 我们这里编写一个类 UserClientService[用户登录/注册]if (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.print("请输入你的选择: ");key = Utility.readString(1);switch (key) {case "1"://这里老师准备写一个方法,来获取在线用户列表userClientService.onlineFriendList();break;case "2"://群发消息System.out.println("请输入想对大家说的话: ");String s = Utility.readString(100);messageClientService.sendMessageToAll(s, userId);break;case "3"://私发消息System.out.print("请输入想聊天的用户号(在线): ");String getterId = Utility.readString(50);System.out.print("请输入想说的话: ");String content = Utility.readString(100);//编写一个方法,将消息发送给服务器端messageClientService.sendMessageToOne(content, userId, getterId);break;case "4"://发文件System.out.print("请输入你想把文件发送给的用户(在线用户): ");getterId = Utility.readString(50);System.out.print("请输入发送文件的路径(形式 d:\\xx.jpg)");String src = Utility.readString(100);System.out.print("请输入把文件发送到对应的路径(形式 d:\\yy.jpg)");String dest = Utility.readString(100);fileClientService.sendFileToOne(src, dest, userId, getterId);break;case "9"://调用方法,给服务器发送一个退出系统的messageuserClientService.logout();loop = false;break;}}} else { //登录服务器失败System.out.println("=========登录失败=========");}break;case "9":loop = false;break;}}}
}

qqcommon

Message

package com.hspedu.qqcommon;import java.io.Serializable;/**
* 表示客户端和服务端通信时的消息对象
*/
public class Message implements Serializable {private static final long serialVersionUID = 1L;private String sender;//发送者private String getter;//接收者private String content;//消息内容private String sendTime;//发送时间private String mesType;//消息类型[可以在接口定义消息类型]//进行扩展 和文件相关的成员private byte[] fileBytes;private int fileLen = 0;private String dest; //将文件传输到哪里private String src; //源文件路径public byte[] getFileBytes() {return fileBytes;}public void setFileBytes(byte[] fileBytes) {this.fileBytes = fileBytes;}public int getFileLen() {return fileLen;}public void setFileLen(int fileLen) {this.fileLen = fileLen;}public String getDest() {return dest;}public void setDest(String dest) {this.dest = dest;}public String getSrc() {return src;}public void setSrc(String src) {this.src = src;}public String getMesType() {return mesType;}public void setMesType(String mesType) {this.mesType = mesType;}public String getSender() {return sender;}public void setSender(String sender) {this.sender = sender;}public String getGetter() {return getter;}public void setGetter(String getter) {this.getter = getter;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public String getSendTime() {return sendTime;}public void setSendTime(String sendTime) {this.sendTime = sendTime;}
}

MessageType

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

User

package com.hspedu.qqcommon;import java.io.Serializable;/**
* 表示一个用户信息
*/
public class User implements Serializable {private static final long serialVersionUID = 1L;private String userId;//用户Id/用户名private String passwd;//用户密码public User() {}public User(String userId, String passwd) {this.userId = userId;this.passwd = passwd;}public String getUserId() {return userId;}public void setUserId(String userId) {this.userId = userId;}public String getPasswd() {return passwd;}public void setPasswd(String passwd) {this.passwd = passwd;}
}
```1.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_6256.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue的过滤器

过滤器(Filters)是vue为开发者提供的功能,常用于文本的格式化。过滤器可以用在两个地方:插值表达式和v-bind属性绑定。 过滤器应该被添加在JavaScript表达式的尾部,由“管道符”进行调用,示例代码如下: 过滤器的注意点:1.要定义到filters节点下,本质上是一个函数2.在过…

我的设计模式之旅 ⑦ 观察者模式

一个菜鸟的设计模式之旅,本程序实现观察者模式。使用C#、Go两门语言分别进行实现。程序创建一个全局游戏死亡事件通知,5个玩家、1个Boss,当任意一方死亡时,在场存活者都能收到阵亡者的消息。一个菜鸟的设计模式之旅,文章可能会有不对的地方,恳请大佬指出错误。 编程旅途是…

计算机网络性能指标之时延(delay)

概念 时延(delay 或 latency)是分组在网络的一端到另一端所需的时间,也叫延迟或迟延。如上图,发送时延、处理时延和排队时延、传播时延共同组成网络的时延。排队时延:分组在经过网络传输时,要经过许多路由器,分组进入路由器后进行排队(队列)等待路由器处理,这里就产生…

【Git总结大全】git操作从入门到实战(总结篇)

目录 1、Git 克隆指定分支的代码 2、把文件从本地推送到远程仓库&#xff08;亲测&#xff09; 3、删除冗余的本地或远程的操作分支 4、解决 “ Author identity unknown ”问题 5、git log 查看历史提交记录 6、git常用命令速查表 7、git分区原理&#xff08;超级详细&a…

谷歌数据洞察简介

谷歌数据洞察简介除了数据收集、数据转换和数据建模,数据科学家的另一个主要任务是讲故事的艺术。这是我们尝试以最引人注目的方式交流发现的见解。通常,我们通过仪表板执行此操作,因为 我们可以创建出色的视觉效果并根据数据主题自定义所有内容 被描绘并具有随着数据流入而…

Boyer-Moore 投票算法

Boyer-Moore 投票算法该算法的原始研究发表在 https://www.cs.utexas.edu/~moore/best-ideas/mjrty/ 该方法基本上非常简单,它依赖于这样一个事实,即如果一个元素出现超过 1/2 次,那么它在体积方面的贡献最大。 所以我们取一个元素,如果下一个元素与取的元素相同,我们将增…

【面试要点】判断企业基本信息——新型骗局“招聘转培训”

核心保护请一定要对个人的身份证与银行卡留有心眼,不要给任何人有中途拿取的操作。 手机不要离手,且一定要设置安全级别较高的密码。 时刻都要保持录音,无论是不是在签合同。招聘流程先是邀请你面试,但是在电话或者任何招聘网站提供的公司信息都与现场不符。 面试通过,他们…

TCP 的自然律

周一傍晚发了一则朋友圈&#xff1a; 放大配图&#xff0c;来自&#xff1a;How to transfer, share and send big files fast&#xff1a; ​ 我对此事的看法&#xff1a;“想高效利用庞大的资源需要的代价和资源规模的增加并不是线性的&#xff0c;而且指数的&#xff0c;下…

新一代网络请求库:python-httpx库

文章目录httpx库一、 概述1、 简介2、 命令行模式3、 快速开始3.1 get请求3.2 post请求3.2.1 表单3.2.2 文件3.2.3 JSON3.2.4 二进制3.3 响应处理3.4 流式响应3.5 cookie3.6 重定向3.7 超时和验证二、 客户端1、 特性2、 发出请求3、 其他配置4、 python_web5、 Request对象6、…

Python基础学习

文章目录1.1变量与运算符案例一&#xff1a;变量与运算符1.2选择结构与循环结构实训案例一&#xff1a;使用for循环输出满足条件的内容案例二&#xff1a;使用while循环1.3数据结构实战案例一&#xff1a;列表案例二&#xff1a;元组案例三&#xff1a;字典案例四&#xff1a;集…

《高级C/C++编译技术》01

Linux程序内存布局:启动程序的默认加载点是在链接阶段才添加的,通常放在程序内存映射的起始处(这是可执行文件和动态库之间的唯一区别)。启动代码有两种不同方式:crt0:“纯粹”的入口点,这是程序代码的第一部分,在内核控制下执行 crt1:更现代化的启动例程,可以在main…

某银行开发一个信用卡管理系统CCMS

38&#xff0e;现准备为某银行开发一个信用卡管理系统CCMS&#xff0c;该系统的基本功能为&#xff1a; (1)信用卡申请&#xff1a;非信用卡客户填写信用卡审请表&#xff0c;说明所要申请的信用卡类型及申 请者的蒸本信息&#xff0c;并提交给CCMS。如果信用卡申请者被…

Day08__异常

异常 Error和Exception捕获和抛出异常package exception;public class Demo01 {public static void main(String[] args) {int a=1;int b=0;//Ctrl+Alt+ttry {//try监控区域System.out.println(a/b);}catch (ArithmeticException e){//捕获异常System.out.println("除数不…

JAVA基础知识

JAVA基础知识目录IDEA快捷键简洁语法CtrlAltShift操作其他常用操作JAVA基础知识注释字面量变量数据类型关键字标识符类型转换自动类型转换表达式的自动类型转换强制类型转换运算符算数运算符符号做连接符自增自减运算符关系运算符和逻辑运算符三元运算符运算符优先级数组数组的…

Code For Better 谷歌开发者之声——Flutter - Google 开源的移动 UI 框架

写在前面 如今&#xff0c;人们都希望自己手机里的应用又漂亮的设计&#xff0c;顺滑的动画以及优异的功能&#xff0c;为了做到这些&#xff0c;开发者们需要在不妥协质量和性能的前提下&#xff0c;更快速地推进新功能的研发&#xff0c;这就是Flutter的核心。 文章目录写在前…

【转】VS2019 安装完成之后再安装其它功能模块组件安装

VisualStudio2019安装完成之后,由于开发需求需要安装其它模块或组件,点击工具,选择获取工具和功能,进行选择安装。 做个笔记随笔记录一下。选择需要的模块进行即可。【转自】https://www.cnblogs.com/jiayan1578/p/13665423.html

网络抖动了解

转自:https://blog.csdn.net/W_317/article/details/112801222, https://zhuanlan.zhihu.com/p/403614008 1.网络抖动 指网络中的延迟是指信息从发送到接收经过的延迟时间,一般由传输延迟及处理延迟组成; 抖动=最大延迟-最小延迟 如访问一个网站的最大延迟是20毫秒,最小延迟…

【转载】预测算法--时间序列(ARIMA)模型

ARIMA:AutoregressiveIntegratedMovingAverage model。自回归差分移动平均模型(p,d,q),可以说AR自回归模型,MA移动平均模型,ARMA自回归移动平均模型都是ARIMA的特殊形式. 时间序列模型一般性步骤包括:1. 数据平稳性检验;2. 确定模型参数;3. 构建时间序列模型;4.模型预测…

STM32二十:OLED和LCD

一.概述 1.OLED介绍 1 //OLED的显存2 //存放格式如下.3 //[0]0 1 2 3 ... 127 4 //[1]0 1 2 3 ... 127 5 //[2]0 1 2 3 ... 127 6 //[3]0 1 2 3 ... 127 7 //[4]0 1 2 3 ... 127 8 //[5]0 1 2 3 ... 127 9 //[6]0 1 2 3 ... 127 10 //[7]0 1 2 3 ... 127 …

一.xv6环境搭建

内容大致来源:1.视频教程:https://space.bilibili.com/16765968/channel/collectiondetail?sid=86878 2.文档:https://tarplkpqsm.feishu.cn/docs/doccnoBgv1TQlj4ZtVnP0hNRETd#W8iZmH一.windows升级为专业版注意:docker支持Windows 10 操作系统专业版,所以要升级windows…