多用户即时通信系统
涉及到java
各个方面的技术
- 项目框架设计
java
面向对象编程OOP
- 网络编程
- 多线程
I/O
流mySql
项目开发流程
需求分析
- 用户登录
- 拉取在线用户列表
- 无异常退出
- 私聊
- 群聊
- 发文件
- 服务器推送新闻
通讯系统整体分析
QQ
登录
登录页面
一级登录页面
二级登录页面
客户端登录流程:
当用户输入用户名和密码正确后,则登录成功
在其中进行相关的业务逻辑检验
客户端
- 编写一个
UserClientServer
中编写一个CheckUser
方法进行检验是否登录成功 - 如果登录成功后则启动一个线程类,所以需要编程一个线程类
- 由于一个客户可以有多个线程在同时启动,方便管理所以编写一个专门管理线程类的类
服务器端
- 编写一个
QQServer
中的构造器中初始化一个ServerSocket
对象,用于监听,由于需要不断进行监听,所以将其放在一个列循环中 - 获取一个 连接对象
Socket
,再获取一个对象输入流,读取客户端发送来的认证请求 - 不管是否认证成功要需要向客户端发送认证请求,所以需要创建一个
Massage
并向客户发送,要获取一个对象输出流 - 如果请求成功,编写一个
ServerConnectClientThread
线程类用于持续与用户端通信,服务需要创建线程类对象并启动该线程 - 为方便管理线程对象还需要
ManageClientThread
类管理线程
####显示在线用户列表
####群发消息
![在这里插入图片描述](https://img-blog.csdnimg.cn/
####私聊消息
####发送文件
无异常退出
System.exit(0)
表示结束了该进程,注意是进程不是线程
注意是:如果不确定一个文件大小时,可以定义一个数组,用File
类中length
方法,数组大小就是长度大小,一次性将文件读取到该数组中
服务器新闻推送功能
专门启动一条独立线程,用于推送新闻
离线发送文件和留言
1.实现离线留言,如果某个用户没有在线,当登录后,可以接受离线的消息
1》在QQClient端专门编写一个类OffLineClientServer,在此类中编写两个方法专门用于发送留言和离线文件
并在QQClient端的线程对发送离线对应相应业务逻辑处理
2》在QQServer端中对客户端离线请求做相应的处理,例如将离线信息保存在一个ConcurrentHashMap集合中
用户登录请求时向集合中获取该用户是否有其他用户给其留言
2.实现离线发送文件,如果某个用户没有在线,当登录后,可以接受离线的文件
需要源码找我
公共区
package common;
import java.io.Serializable;
/**
* @author: 海康
* @version: 1.0
*/
public class Message implements Serializable {
private static final long serialVersionUID = 1l;
private String sender;// 发送者
private String getter;// 接收者
private String content;// 发送内容
private String sendTime;// 发送时间
private String mesType;// 信息类型
// 扩展与文件发送相关的属性
private byte[] bytes ;
private int fileLength;
private String sendFileDest;
private String sendFileSrc;
public byte[] getBytes() {
return bytes;
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
public int getFileLength() {
return fileLength;
}
public void setFileLength(int fileLength) {
this.fileLength = fileLength;
}
public String getSendFileDest() {
return sendFileDest;
}
public void setSendFileDest(String sendFileDest) {
this.sendFileDest = sendFileDest;
}
public String getSendFileSrc() {
return sendFileSrc;
}
public void setSendFileSrc(String sendFileSrc) {
this.sendFileSrc = sendFileSrc;
}
public Message(){}
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;
}
public String getMesType() {
return mesType;
}
public void setMesType(String mesType) {
this.mesType = mesType;
}
}
package common;
/**
* @author: 海康
* @version: 1.0
*/
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";// 文件消息(发送文件)
String MESSAGE_OFF_LINE_COMM_MES = "9"; // 离线发送普通信息包
String MESSAGE_OFF_LINE_FILE_MES = "10";// 离线发送文件
}
package common;
import java.io.Serializable;
/**
* @author: 海康
* @version: 1.0
* 用户信息
*/
public class User implements Serializable {
private String userId;
private String passwd;
// 实现兼容性
private static final long serialVersionUID = 1l;
public User(){}
public User(String userId, String passwd) {
this.userId = userId;
this.passwd = passwd;
}
@Override
public String toString() {
return "User{" +
"userId='" + userId + '\'' +
", 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;
}
}
QQClient端
package QQClient.common_.qqView_;
import QQClient.common_.service_.FIleClientServer;
import QQClient.common_.service_.ManageClientServer;
import QQClient.common_.service_.OffLineClientServer;
import QQClient.common_.service_.UserClientService;
import QQClient.common_.utils.Utility;
/**
* @author: 海康
* @version: 1.0
* 客户端登录页面
*/
public class QQView {
public static void main(String[] args) {
new QQView().mainMenu();
//
}
private boolean loop = true;
String key = "";// 用于键盘输入
UserClientService userClientService = new UserClientService();
ManageClientServer clientServer = new ManageClientServer();
public void mainMenu(){
System.out.println("=============== 欢迎登录网络通信系统 ===============\r\n");
System.out.println("\t\t\t\t\t1 登录系统 ");
System.out.println("\t\t\t\t\t9 退出系统 ");
while (loop){
System.out.print("请输入你的选择: ");
key = Utility.readString(1);
switch (key){
case "1":
System.out.print("请输入用户号: ");
String userId = Utility.readString(50);
System.out.print("请输入密 码: ");
String passwd = Utility.readString(20);
// ...
// 由于我们需要验证用户是否合法,需要编写一个类 UserClientService [用户登录/注册]
if (userClientService.checkUser(userId,passwd)){
while (loop){
System.out.println("=============== 欢迎来("+ userId +")网络通信二级菜单 ===============\r\n");
System.out.println("\t\t\t1 显示在线用户列表 ");
System.out.println("\t\t\t2 群 发 消 息 ");
System.out.println("\t\t\t3 私 聊 消 息");
System.out.println("\t\t\t4 发 送 文 件");
System.out.println("\t\t\t5 留 言");
System.out.println("\t\t\t6 离线 发送 文件");
System.out.println("\t\t\t9 退 出 系 统");
System.out.print("请输入您的选择: ");
key = Utility.readString(1);
switch (key){
case "1":
System.out.println("\t\t\t1 显示在线用户列表 ");
userClientService.noLineFriendList();
// 使用一个方法进行处理 由于都UserClientServer请求所以将写该类中
break;
case "2":
// System.out.println("\t\t\t2 群 发 消 息 ");
// 编写一个方法实现发送的功能
System.out.print("请您输入想群发的内容 :");
String contentMass = Utility.readString(200);
clientServer.massChat(userId,contentMass);
break;
case "3":
System.out.println("\t\t\t3 私 聊 消 息");
// 编写一个方法实现私聊功能
System.out.print("请您输入想聊天的用户号(在线): ");
String getterId = Utility.readString(50);
System.out.print("请您输入想说的话: ");
String content = Utility.readString(200);
clientServer.privateChat(userId,getterId,content);
break;
case "4":
System.out.println("\t\t\t4 发 送 文 件");
// 编写一个 FileClientServer类并在该类编写一个 sendFile 方法用于发送文件
System.out.print("请输入想发送文件的用户号(在线):");
String getterID = Utility.readString(50);
System.out.print("请输入想发送文件完整路径(形式 d:\\xxx.jpg):");
String sendFileDest = Utility.readString(100);
System.out.print("请输入发送文件到对方的路径(形式 d:\\xxx.jpg):");
String sendFileSrc = Utility.readString(100);
FIleClientServer.sendFile(userId,getterID,sendFileDest,sendFileSrc);
break;
case "5":
System.out.println("\t\t\t5 留 言");
// 编写一个文件
System.out.print("请您输入想要离线用户(离线):");
String GetterDI = Utility.readString(50);
System.out.print("请您输入想要离线的内容: ");
String OffLineContent = Utility.readString(200);
OffLineClientServer.offLineSendContent(userId,GetterDI,OffLineContent);
break;
case "6":
System.out.println("\t\t\t6 离线 发送 文件");
// 编写sendOffLineFile方法处理
System.out.print("请您输入想要发送离线文件的用户: ");
String offLineUserId = Utility.readString(50);
System.out.print("请您输入发送离线文件路径: ");
String sendOffLineFilePath = Utility.readString(50);
System.out.print("请您输入发送离线文件保存到对方路径: ");
String keepOffLineFilePath = Utility.readString(50);
OffLineClientServer.sendOffLineFile(userId,sendOffLineFilePath,offLineUserId,keepOffLineFilePath);
break;
case "9":
System.out.println("\t\t\t9 退 出 系 统");
loop = false;
// 调用方法,给服务器发送一个退出系统的message
userClientService.logOut();
break;
}
}
}else {
System.out.println("登录"+ userId +"失败!");
}
break;
case "9":
loop = false;
System.out.println("欢迎下次再会!");
break;
}
}
}
}
package QQClient.common_.service_;
import common.Message;
import common.MessageType;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
/**
* @author: 海康
* @version: 1.0
*/
public class ClientConnectServiceThread extends Thread{
// 由于需要 Socket 对象 所以将其做成 一个成员属性
private Socket socket;
public ClientConnectServiceThread(Socket socket){
this.socket = socket;
}
@Override
public void run(){
// 因为 Thread 需要在后台和服务器通信 因此我们需要 while 循环
while (true){
try {
ObjectInputStream ois =
new ObjectInputStream(socket.getInputStream());
// 注意如果服务器端不发送信息过来时,程序会一直阻塞在此处
Message message = (Message) ois.readObject();
// 对象 服务器发送回的 Message 进行判断 是那种类型
if (message.getMesType().equals(MessageType.MESSAGE_RET_ONLINE_FRIEND)){
// 规定服务端发送回用户列表请求之间是用空格隔开
String content = message.getContent();
String[] split = content.split(" ");
System.out.println("=========== 在线用户列表如下 ===========");
for (int i = 0; i < split.length; i++) {
System.out.println("用户 : " + split[i]);
}
// 私聊
}else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)){
String content = message.getContent();
String sender = message.getSender();
String getter = message.getGetter();
System.out.println(sender+" 对 "+getter+" 说时间是:"+message.getSendTime());
System.out.println(sender+" 对 "+getter+" 说 :"+content);
// 群发
}else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)){
String content = message.getContent();
String sendTime = message.getSendTime();
String sender = message.getSender();
System.out.println(sender+" 对 "+"大家说 时间是:"+sendTime);
System.out.println(sender+" 对大家说: "+content);
// 发送文件
}else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){
byte[] bytes = message.getBytes();
String sendFileSrc = message.getSendFileSrc();
String getter = message.getGetter();
String sender = message.getSender();
String sendFileDest = message.getSendFileDest();
System.out.println(sender+" 给 "+getter+"发送文件: "
+sendFileDest+"到我的电脑目录 "+sendFileSrc);
// 调用 keepSendFile 保存文件
FIleClientServer.keepSendFile(sendFileSrc,bytes);
System.out.println("保存文件OK !");
}else if (message.getMesType().equals(MessageType.MESSAGE_OFF_LINE_COMM_MES)){
String sender = message.getSender();
String content = message.getContent();
System.out.println(sender+" 对 您 留言:"+content);
}else if (message.getMesType().equals(MessageType.MESSAGE_OFF_LINE_FILE_MES)){
String sender = message.getSender();
String sendFileDest = message.getSendFileDest();
String sendFileSrc = message.getSendFileSrc();
byte[] bytes = message.getBytes();
// 调用方法保存文件
FIleClientServer.keepSendFile(sendFileSrc,bytes);
System.out.println(sender+" 给 您发送文件:"+sendFileDest+"保存到您电脑目录"+sendFileSrc);
System.out.println("保存离线文件OK!");
}else {
// 其他情况先不处理
System.out.println("其他情况先不处理:");
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public Socket getSocket() {
return socket;
}
}
package QQClient.common_.service_;
import QQServer.common_.server_.ManageClientThreads;
import QQServer.common_.server_.ServerConnectClientThread;
import common.Message;
import common.MessageType;
import java.io.*;
import java.net.Socket;
/**
* @author: 海康
* @version: 1.0
*/
public class FIleClientServer {
/**
* 该方法用于读取 sendFileDest 文件,封装到 Message 对象
* 用于发送
* @param sendId 发送者
* @param getterID
* @param sendFileDest
* @param sendFileSrc
*/
public static void sendFile(String sendId,String getterID,
String sendFileDest,String sendFileSrc){
// 创建一个 Message 对象用于封装 数据发送给客户端
Message message = new Message();
BufferedInputStream bis = null;
// 获取一个发送文件的路径
File file = new File(sendFileDest);
// 定义一个 byte[] 数组长度与文件大小一样
byte[] fileBytes = new byte[(int) file.length()];
try {
bis = new BufferedInputStream(new FileInputStream(file));
// 将文件一次性读取在数组中
bis.read(fileBytes);
// 设置文件数组
message.setBytes(fileBytes);
message.setSender(sendId);
message.setGetter(getterID);
message.setSendFileSrc(sendFileSrc);
message.setSendFileDest(sendFileDest);
message.setMesType(MessageType.MESSAGE_FILE_MES);
// 获取当前线程并向服务器端发送请求
ClientConnectServiceThread serviceThread =
ManageClientConnectServerThread.getClientConnectServiceThread(sendId);
Socket socket = serviceThread.getSocket();
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message);
System.out.println("\r\n "+sendId+" 给 "+"发送文件: "
+sendFileDest+"到对方的电脑目录下:"+sendFileSrc);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void keepSendFile(String sendFileSrc,byte[] bytes){
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(sendFileSrc));
// 将文件一次性保存到磁盘中
bos.write(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
// 不为空关闭流
if (bos!=null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}