目录
ClientConnectServerThread.java
ServerConnectClientThread.java
涉及的java基础
1.IO流
2.TCP对象流编程
3.HashMap集合
4.面向对象基础
5.多线程基础
整体思路梳理
基本框架
多用户通讯系统分为客户端QQClient与服务器端QQServer,客户端可以同时启动多个分为客户端A,客户端B,客户端C...。服务端循环监听一个端口,等待客户端连接,登入成功后得到soket对象,两端分别创建并启动一个线程并且将socket对象放进线程中用于两端保持通讯,还需要将线程放入一个集合进行管理(key:用户id , value:线程),因为一个客户端可以创建多个socket。
将需要传递消息的成员封装成User,Message类等,使用对象流的方式读写数据。MySQl数据库存放的用户ID和pwd使用集合HashMap代替用于登入验证。
客户端QQClient所需类
服务器端QQServer所需类
功能实现
1.用户登入
2.拉取在线用户列表
3.无异常退出
4.私聊
5.群聊
6.发文件
7.服务器推送新闻
8.离线留言(课后作业部分)
准备工作
工具类Utility
该类是一个工具类,封装了一些Scanner控制台输入方法,需要客户端和服务器端共有
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=>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;
}
}
User,Message信息类和接口(共有部分)
1.Message.java
package com.hspedu.qqcommon;
import java.io.Serializable;
/**
* Message
* 表示客户与服务器端的消息对象
*/
public class Message implements Serializable {
/**
* 发送者
*/
private String sender;
/**
* 接收者
*/
private String getter;
/**
* 消息内容
*/
private String content;
/**
* 发送时间
*/
private String getSenderTime;
/**
* 消息类型,{可以在接口中定义消息类型}
*/
private String mesType;
//扩展,和文件相关
private String src;//源文件路径
private String desk;//发送文件路径
private int fileLen = 0;//数组长度
private byte[] fileBytes;//字节数组存放文件
public String getSrc() {
return src;
}
public void setSrc(String src) {
this.src = src;
}
public String getDesk() {
return desk;
}
public void setDesk(String desk) {
this.desk = desk;
}
public int getFileLen() {
return fileLen;
}
public void setFileLen(int fileLen) {
this.fileLen = fileLen;
}
public byte[] getFileBytes() {
return fileBytes;
}
public void setFileBytes(byte[] fileBytes) {
this.fileBytes = fileBytes;
}
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 getGetSenderTime() {
return getSenderTime;
}
public void setGetSenderTime(String getSenderTime) {
this.getSenderTime = getSenderTime;
}
}
2.MessageType接口
根据设置message.setmesType()不同的消息类型判断需要进行对应的业务逻辑
package com.hspedu.qqcommon;
/**
* MessageType
* 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 = "9";//离线留言功能
}
3.User.java
package com.hspedu.qqcommon;
import java.io.Serializable;
/**
* User
* 表示一个用户信息
*/
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;
}
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;
}
}
功能思路分析
0.控制台输入输出
1.QQView类编写一个mainMenu()方法
2.使用while循环让控制台输入不同的数字实现相关的业务逻辑并输出相应的提示信息
1.用户登入(重点)
QQServer创建一个集合,并且在静态代码块存放添加多个合法用户Id和password,
//创建一个集合,存放多个合法用户
/*
这里使用ConcurrentHashMap,可以处理并发的集合,在多线程情况下是安全的
HashMap没有处理线程安全,在多线程情况下是不安全的
*/
private static ConcurrentHashMap<String, User> validUser = new ConcurrentHashMap<>();
//在静态代码块内初始化validUser(String Id,User user(Id,pwd))
static {
validUser.put("100", new User("100", "123"));
validUser.put("200", new User("200", "123"));
validUser.put("300", new User("300", "123"));
validUser.put("400", new User("400", "123"));
validUser.put("至尊宝", new User("至尊宝", "123"));
validUser.put("紫霞仙子", new User("紫霞仙子", "123"));
}
1.输入 1登入系统 提示输入用户名和密码,用于进行登入验证
2.创建UserClientService类编写登陆验证方法checkUser(String UserID, String pwd),,将验证信息放入User对象。返回bool值,如果成功则进入二级菜单
3.连接本地服务器,使用ObjectInputStream将User对象发送给服务器端判断
4.客户端创建并启动线程while循环读取从服务器回复的Message信息,用于控制台输出----------------ClientConnectServerThread
/**
* 该方法是连接并向服务器发送User对象,依据返回Message对象判断是否登入成功
*
* @param UserID
* @param pwd
* @return boolean
*/
public boolean checkUser(String UserID, String pwd) {
boolean b = false;
//得到User对象ID和pwd
u.setUserId(UserID);
u.setPasswd(pwd);
try {
//1.连接服务器,创建socket对象
socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);//本地服务器地址
//2.得到ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//3.发送User对象
oos.writeObject(u);
//4.读取从服务器回复的Message对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message ms = (Message) ois.readObject();//向下转型
/**
* 5.if语句判断登入是否成功
*/
if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {
/*
* 创建一个和服务器端保持通讯的线程
* 该线程持有一个socket对象--->ClientConnectServerThread线程类
* 因为一个客户端可以创建多个socket
* 不创建线程的话没有办法对多个socket进行管理
*/
ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
//启动线程
ccst.start();
//将线程放入到集合进行管理,以便将来扩展其他登入的线程
ManageThread.addClientConnectServerThread(UserID, ccst);
b = true;//ok登入成功
} else {
//如果登入失败,就不能启动和服务器通信的线程,关闭socket
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
4.QQServer构造器里服务器端while循环监听9999端口,如果连接上一个客户端线程,需要使用ObjectInputStream拿到User对象,得到关联的socket对象输入流
5.编写方法从集合中验证ID和pwd
/**
* 验证用户是否有效的方法
* @return boolean
*/
public boolean checkUser(String userId,String pwd){
User user = validUser.get(userId);
if (user == null){
return false;
}
if (!user.getPasswd().equals(pwd)){
return false;
}
return true;
}
6.return true则使用对象输出流放入数据通道回复Message对象,得到关联的socket对象输出流
7.服务器端创建并启动一个线程,用于和客户端while循环保持通讯用于接收Message信息并实现相应业务逻辑----------ServerConnectClientThread
8.把该线程对象放入到一个集合中 ManageThread.addServerConnectClientThread(u.getUserId(), scct);
//把多个线程放入一个HashMap集合,(key:用户id , value:线程) private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();
9.客户端读取从服务器返回的message对象并且判断登入是否成功,创建并启动线程ClientConnectServerThread类
10.客户端如果checkUser()return true则进入二级菜单,此时服务端启动线程循环读取客户端传来的Message对象,客户端启动线程循环读取服务端传来的Message对象,客户端可以通过线程里socket对象向服务器端发送Message信息,数据通道打通,用户完成登入。
2.拉取在线用户列表
1).UserClientService编写方法onlineFriendList用于拉取用户列表
//发送一个Message发送给服务器端 Message message = new Message(); message.setSender(u.getUserId()); message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
2).服务器端根据Message类型,监听到拉取用户请求,从hashmap中取出所有的key拼接成字符串放进Message对象里返回给客户端,客户端接收后在控制台遍历输出message.getContent()
3.无异常退出
1).UserClientService编写方法noExceptionExit用于无异常退出客户端
//发送一个Message发送给服务器端
Message message = new Message(); message.setSender(u.getUserId());//一定要指明哪个客户端需要退出 message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
2).服务器端根据Message类型,监听到无异常退出请求,服务端break退出线程循环,客户端结束进程
//需要将客户端对应线程,从集合中删除 ManageThread.removeServerConnectClientThread(message.getSender()); socket.close(); break;//退出线程
System.exit(0);//结束进程
4.私聊
控制台输入:接收者id,content消息内容
1).创建MessageClientService编写sendMessageToOne方法用于私聊消息
//构建message对象发送给服务器端 Message message = new Message(); message.setMesType(MessageType.MESSAGE_COMM_MES); message.setSender(senderId); message.setGetter(getterId); message.setContent(content); Date date = new Date(); // 获取当前时间 SimpleDateFormat sdf = new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒"); String formattedDate = sdf.format(date); message.setGetSenderTime(formattedDate);//java.util.Date年月日时分秒
2).服务器端根据Message类型,监听到私聊消息请求将Message对象转发给在线的对应客户端
ManageThread.getServerConnectClientThread(message.getGetter())
3).客户端线程的socket接收Message对象后在控制台显示
String MESSAGE_COMM_MES = "3";//普通信息包,私聊
5.群聊
控制台输入:content消息内容
1).在MessageClientService类编写sendMessageToAll方法用于群聊消息
//构建message对象发送给服务端 Message message = new Message(); message.setMesType(MessageType.MESSAGE_TO_ALL_MES); message.setSender(senderId); message.setContent(content); System.out.println(senderId+" 对所有用户说:"+content);
2).服务器端接收后取得遍历所有在线的线程并将Message对象转发
//需要遍历管理线程的集合,把所有现成的socket得到,把message转发 HashMap<String, ServerConnectClientThread> hm = ManageThread.getHm(); Iterator<String> iterator = hm.keySet().iterator(); while (iterator.hasNext()) { //取出在线用户ID String onlineUserId = iterator.next().toString(); //排除群发消息的用户自身 if (!onlineUserId.equals(message.getSender())) { //服务器将Message消息转发给对应线程的socket,由对应的客户端接收 ObjectOutputStream oos = new ObjectOutputStream (hm.get(onlineUserId).getSocket().getOutputStream()); oos.writeObject(message); } }
3).客户端线程的socket接收Message对象后在控制台显示
String MESSAGE_TO_ALL_MES = "7";//普通信息包,群发
6.发文件
控制台输入:源文件路径src,接收文件路径desk,接收者id
1).创建FileClientService类编写sendFileToOne方法用于发文件
//1.构建message对象 Message message = new Message(); message.setSender(senderId); message.setGetter(GetterId); message.setSrc(src); message.setDesk(desk); message.setMesType(MessageType.MESSAGE_FILE_MES); //2.读取字节输入流src文件 FileInputStream fileInputStream = null; byte[] fileBytes = new byte[(int) new File(src).length()]; fileInputStream = new FileInputStream(src);//这里可能抛出异常 fileInputStream.read(fileBytes);//将src读取到字节数组 message.setFileBytes(fileBytes);//将文件对应的字节数组设置message
2).服务器端根据Message类型,监听到文件传输的消息请求,服务端将message转发给对应message.getGetter()线程的socket
3).接收者客户端将文件传输到对应路径
FileOutputStream fileOutputStream = new FileOutputStream(message.getDesk(),true); fileOutputStream.write(message.getFileBytes()); fileOutputStream.close(); System.out.println("文件保存成功...");
7.服务器推送新闻
1).创建sendNewsToAllService线程类
//为了可以推送多次新闻,使用循环 while (true) { Message message = new Message(); message.setSender("服务器"); message.setMesType(MessageType.MESSAGE_TO_ALL_MES); System.out.print("请输入推送的新闻:(exit退出推送) "); String news = Utility.readString(100); if ("exit".equals(news)){//退出推送 break; } message.setContent(news); }//将message对象发送给客户端
2).客户端接收到群发消息的提醒在控制台显示
String MESSAGE_TO_ALL_MES = "7";//普通信息包,群发
8.离线留言(课后作业部分)
使用私聊消息的控制台输入:接收者id,content消息内容
1).ServerConnectClientThread类需要扩展创建一个集合comcurrentHashMap
//离线留言
ArrayList iOff = new ArrayList<Message>();
private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();
2).该留言属于私聊,将message私聊消息发送给服务端,服务端线程判断如果私聊用户不在线(HashMap集合中没有对应线程和getterId)则转为离线留言消息
//服务端判断用户是否在线
if (ManageThread.getServerConnectClientThread(message.getGetter()) == null) {
System.out.println("消息发送失败转为离线留言...");
//将离线消息的接收者message.getGetter()和message消息存入集合
iOff.add(message);
offLineDb.put(message.getGetter(), iOff);
}
3).在用户完成登入的位置扩展自动调用messageClientService对象
System.out.println("=========欢迎(用户" + userID + ")===========");
/**
* 离线留言判断
*/
messageClientService.offLine(userID);
4).编写一个offLine(userID)方法用于离线留言,再将离线留言message发送给服务端
/**
* 离线留言功能
* @param senderId 发送者
*/
public void offLine(String senderId) {
//构建message对象
Message message = new Message();
message.setMesType(MessageType.MESSAGE_OFF_LINE);
message.setSender(senderId);
//发送给服务器端
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
e.printStackTrace();
}
}
5).服务端根据接收到的message对象判断ConcurrentHashMap集合里有没有离线留言消息,如果有就遍历对应getterId == userId的留言发给客户端,最后将留言从集合里删除
/**
* 离线留言转发
*/
else if (message.getMesType().equals(MessageType.MESSAGE_OFF_LINE)) {
// System.out.println("其他业务暂时不处理...");
//判断有没有对应的getterId
if (offLineDb.containsKey(message.getSender())) {
ArrayList<Message> foundMessages = offLineDb.get(message.getSender());
for (Message message1 : foundMessages) {
/**
* 判断登入的用户Id是否和offLineDb存入过的getterId有没有相等的
* 注意这里是判断字符串相等,需要使用 equals()
* getterId == userId X
*/
if (!message1.getGetter().equals(message.getSender())) {
continue;
}
// 在此处对获取到的Message进行操作
message1.setMesType(MessageType.MESSAGE_OFF_LINE);
//发送给对应客户端
ServerConnectClientThread serverConnectClientThread = ManageThread.getServerConnectClientThread(message.getSender());
ObjectOutputStream oos = new ObjectOutputStream
(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message1);
//最后将离线留言删除
offLineDb.remove(message.getSender());
}
}
}
6).客户端读取到离线留言消息,在控制台输出
else if(message.getMesType().equals(MessageType.MESSAGE_OFF_LINE)){
System.out.println("你收到 "+message.getSender()+" 的离线留言:"+message.getContent());
}
客户端实现代码
QQView.java
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;
/**
* className:QQView
* 1显示客户端主菜单
* @author DiaoXian
*/
public class QQView {
public static void main(String[] args) {
new QQView().mainMenu();
System.out.println("退出多用户通讯系统...");
}
private boolean loop = true;//显示隐藏菜单
private String key = "";//接收用户键盘输入
private UserClientService userClientService = new UserClientService();//该对象用于登入服务器端相关
private MessageClientService messageClientService = new MessageClientService();//该对象用于消息管理相关
private FileClientService fileClientService = new FileClientService();//该对象用于文件传输相关
//显示主菜单
private void mainMenu() {
/**
* 一级菜单
*/
while (loop) {
System.out.println("=========多用户通讯系统===========");
System.out.println("\t\t\t1 登入系统");
System.out.println("\t\t\t9 退出系统");
System.out.print("请输入选择:");
key = Utility.readString(1);//使用工具类控制用户输入
//根据用户输入,处理不同逻辑
switch (key) {
case "1":
System.out.println("登入系统");
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 + ")===========");
/**
* 离线留言判断
*/
messageClientService.offLine(userID);
/**
* 二级菜单
*/
while (loop) {
System.out.println("=========多用户通讯系统二级菜单(" + userID + ")===========");
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\t9 退出系统");
//根据用户输入,处理不同逻辑
System.out.print("请输入选择:");
key = Utility.readString(1);
switch (key){
case "1":
//System.out.println("显示用户列表");
//调用方法将消息发送给服务器端
userClientService.onlineFriendList();
break;
case "2":
//System.out.println("群发消息");
System.out.print("请输入群发消息内容:");
String sss = Utility.readString(100);
//调用方法将消息发送给服务器端
messageClientService.sendMessageToAll(userID,sss);
break;
case "3":
//System.out.println("私聊消息");
System.out.print("请输入聊天的用户Id(在线):");
String getterId = Utility.readString(50);
System.out.print("请输入消息内容:");
String content = Utility.readString(100);
//调用方法将消息发送给服务器端
messageClientService.sendMessageToOne(userID,getterId,content);
break;
case "4":
//System.out.println("发送文件");
System.out.print("请输入文件发送的用户Id(在线):");
String getterId1 = Utility.readString(50);
System.out.print("请输入源文件传输路径 例:(d:\\\\图片\\\\Saved Pictures\\\\640.jpg)");
String src = Utility.readString(100);
System.out.print("请输入传输文件的存放路径:(d:\\\\白月光冒烟了.jpg)");
String desk = Utility.readString(100);
fileClientService.sendFileToOne(userID,getterId1,src,desk);
break;
case "9":
//System.out.println("退出系统");
userClientService.noExceptionExit();
loop = false;
break;
default:
System.out.println("你的输入有误...");
break;
}
}
}else {
System.out.println("=========登入服务器失败========");
}
break;
case "9":
//System.out.println("退出系统");
userClientService.noExceptionExit();
loop = false;
break;
default:
System.out.println("你的输入有误...");
break;
}
}
}
}
UserClientService.java
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;
/**
* className:UserClientService
* 2完成用户登入和用户注册等功能...
*
* @author DiaoXian
*/
public class UserClientService {
//可能在其他地方使用User信息,因此做出成员属性
private User u = new User();
//可能在其他地方使用socket信息,因此做出属性
private Socket socket;
/**
* 该方法是连接并向服务器发送User对象,依据返回Message对象判断是否登入成功
*
* @param UserID
* @param pwd
* @return boolean
*/
public boolean checkUser(String UserID, String pwd) {
boolean b = false;
//得到User对象ID和pwd
u.setUserId(UserID);
u.setPasswd(pwd);
try {
//1.连接服务器,创建socket对象
socket = new Socket(InetAddress.getByName("127.0.0.1"), 9999);//本地服务器地址
//2.得到ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//3.发送User对象
oos.writeObject(u);
//4.读取从服务器回复的Message对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message ms = (Message) ois.readObject();//向下转型
/**
* 5.if语句判断登入是否成功
*/
if (ms.getMesType().equals(MessageType.MESSAGE_LOGIN_SUCCEED)) {
/*
* 创建一个和服务器端保持通讯的线程
* 该线程持有一个socket对象--->ClientConnectServerThread线程类
* 因为一个客户端可以创建多个socket
* 不创建线程的话没有办法对多个socket进行管理
*/
ClientConnectServerThread ccst = new ClientConnectServerThread(socket);
//启动线程
ccst.start();
//将线程放入到集合进行管理,以便将来扩展其他登入的线程
ManageThread.addClientConnectServerThread(UserID, ccst);
b = true;//ok登入成功
} else {
//如果登入失败,就不能启动和服务器通信的线程,关闭socket
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
/**
* 该方法向服务器请求在线用户列表
*/
public void onlineFriendList() {
//发送一个Message
Message message = new Message();
message.setSender(u.getUserId());
message.setMesType(MessageType.MESSAGE_GET_ONLINE_FRIEND);
//发送给服务器
try {
//得到当前线程socket对应的ObjectOutputStream对象
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());
oos.writeObject(message);//将请求写入数据通道
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 该方法是客户端无异常退出,并且给服务器发送一个退出系统的message对象
*/
public void noExceptionExit() {
Message message = new Message();
message.setSender(u.getUserId());//一定要指明哪个客户端需要退出
message.setMesType(MessageType.MESSAGE_CLIENT_EXIT);
//得到当前线程socket对应的ObjectOutputStream对象
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(u.getUserId()).getSocket().getOutputStream());
oos.writeObject(message);
System.out.println(u.getUserId()+"退出了系统...");
System.exit(0);//结束进程
} catch (IOException e) {
e.printStackTrace();
}
}
}
ClientConnectServerThread.java
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;
/**
* className:ClientConnectServerThread
* 3客户端连接服务端线程类,持有socket对象用于通信
*
* @author DiaoXian
*/
public class ClientConnectServerThread extends Thread {
//该线程需要持有socket对象
private Socket socket;
//接收一个socket对象,使用构造器
public ClientConnectServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
//业务逻辑:因为Thread需要后台和服务器通信,因此while循环
while (true) {
System.out.println("客户端线程,等待读取从服务器端发送消息...");
try {
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();
System.out.println("==========在线用户列表===========");
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(message.getSender() + " 给用户" +
message.getGetter() + "发送消息:" + message.getContent());
System.out.println("发送时间:" + message.getGetSenderTime());
/**
* 显示群发消息提醒
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
//客户端控制台显示发送的消息
System.out.println(message.getSender() + " 对所有在线用户说:" + message.getContent());
/**
* 将源文件src传输到对应路径desk
*/
} else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){
System.out.println(message.getSender()+" 给 "+message.getGetter()
+"传输文件"+message.getSrc()+"到路径 "+message.getDesk());
FileOutputStream fileOutputStream = new FileOutputStream(message.getDesk(),true);
fileOutputStream.write(message.getFileBytes());
fileOutputStream.close();
System.out.println("文件保存成功...");
}else if(message.getMesType().equals(MessageType.MESSAGE_OFF_LINE)){
//System.out.println("其他类型的业务暂时不做处理...");
System.out.println("你收到 "+message.getSender()+" 的离线留言:"+message.getContent());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
}
ManageThread.java
package com.hspedu.qqclient.service;
import java.util.HashMap;
/**
* className:ManageThread
* 4该类管理客户端连接到服务器端的线程的类
* @author DiaoXian
*/
public class ManageThread {
//把多个线程放入一个HashMap集合,(key:用户id , value:线程)
private static HashMap<String, ClientConnectServerThread> hm = new HashMap<>();
//将线程放入集合
public static void addClientConnectServerThread(String userID,ClientConnectServerThread ccst){
hm.put(userID,ccst);
}
//通过userID得到对应线程
public static ClientConnectServerThread getClientConnectServerThread(String userID){
return hm.get(userID);
}
}
MessageClientService.java
package com.hspedu.qqclient.service;
import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import java.io.ObjectOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* className:MessageClientService
* 9该类提供和消息相关的方法
*
* @author DiaoXian
*/
public class MessageClientService {
/**
* 群发消息功能
* @param senderId 发送者
* @param content 消息内容
*/
public void sendMessageToAll(String senderId,String content){
//构建message对象
Message message = new Message();
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
message.setSender(senderId);
message.setContent(content);
System.out.println(senderId+" 对所有用户说:"+content);
//发送给服务器端
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 私聊消息功能
* @param senderId 发送者
* @param getterId 接收者
* @param content 消息内容
*/
public void sendMessageToOne(String senderId, String getterId, String content) {
//构建message对象
Message message = new Message();
message.setMesType(MessageType.MESSAGE_COMM_MES);
message.setSender(senderId);
message.setGetter(getterId);
message.setContent(content);
Date date = new Date(); // 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒");
String formattedDate = sdf.format(date);
message.setGetSenderTime(formattedDate);//java.util.Date年月日时分秒
//客户端提示消息
System.out.println(senderId+" 对 "+getterId+" 说:"+content);
System.out.println("发送时间:" + message.getGetSenderTime());
//发送给服务器端
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 离线留言功能
* @param senderId 发送者
*/
public void offLine(String senderId) {
//构建message对象
Message message = new Message();
message.setMesType(MessageType.MESSAGE_OFF_LINE);
message.setSender(senderId);
//发送给服务器端
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
FileClientService.java
package com.hspedu.qqclient.service;
import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import java.io.*;
/**
* className:FileClientService
* 10该类提供和文件传输相关服务
*
* @author DiaoXian
*/
public class FileClientService {
private boolean top;
/**
* 该方法用于发送文件
* @param senderId 发送这
* @param GetterId 接收者
* @param src 源文件路径
* @param desk 接收文件路径
*/
public void sendFileToOne(String senderId, String GetterId, String src, String desk) {
top = true;
//1.构建message对象
Message message = new Message();
message.setSender(senderId);
message.setGetter(GetterId);
message.setSrc(src);
message.setDesk(desk);
message.setMesType(MessageType.MESSAGE_FILE_MES);
//2.读取字节输入流src文件
FileInputStream fileInputStream = null;
byte[] fileBytes = new byte[(int) new File(src).length()];
try {
fileInputStream = new FileInputStream(src);//这里可能抛出异常
fileInputStream.read(fileBytes);//将src读取到字节数组
message.setFileBytes(fileBytes);//将文件对应的字节数组设置message
} catch (Exception e) {
System.out.println("异常原因:"+e.getMessage());
top = false;
} finally {
//3.fileInputStream关闭流资源
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 判断是否在文件路径就出现异常,防止将无效路径传输给服务器端
*/
if (top == false){
return;
}
//4.提示信息
System.out.println(senderId + " 给 " + GetterId + "发送文件: " + src + "到对方目录: " + desk);
//5.将message发送给服务端
try {
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getClientConnectServerThread(senderId).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端实现代码
QQServer.java
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.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
/**
* className:QQServer
* 5服务器,在监听9999端口,等待客户端连接,并保持通讯
*
* @author DiaoXian
*/
public class QQServer {
//将serverSocket封装成属性,以便使用
private ServerSocket serverSocket = null;
//创建一个集合,存放多个合法用户
/*
这里使用ConcurrentHashMap,可以处理并发的集合,在多线程情况下是安全的
HashMap没有处理线程安全,在多线程情况下是不安全的
*/
private static ConcurrentHashMap<String, User> validUser = new ConcurrentHashMap<>();
//private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();
//在静态代码块内初始化validUser
static {
validUser.put("100", new User("100", "123"));
validUser.put("200", new User("200", "123"));
validUser.put("300", new User("300", "123"));
validUser.put("400", new User("400", "123"));
validUser.put("至尊宝", new User("至尊宝", "123"));
validUser.put("紫霞仙子", new User("紫霞仙子", "123"));
}
/**
* 验证用户是否有效的方法
* @return boolean
*/
public boolean checkUser(String userId,String pwd){
User user = validUser.get(userId);
if (user == null){
return false;
}
if (!user.getPasswd().equals(pwd)){
return false;
}
return true;
}
//构造器
public QQServer() {
try {
//1.端口可以写在配置文件
System.out.println("服务器在9999端口监听...");
serverSocket = new ServerSocket(9999);
/**
* 扩展:启动推送新闻的线程
*/
new Thread(new sendNewsToAllService()).start();
//2.一直保持监听,因为可能有多个客户端连接
while (true) {
Socket socket = serverSocket.accept();
//3.如果连接上一个客户端线程,需要从数据通道拿到User对象,得到关联的socket对象输入流
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
User u = (User) ois.readObject();
//4.使用对象输出流放入数据通道回复Message对象,得到关联的socket对象输出流
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//5.创建一个Message对象,回复客户端登入验证
Message message = new Message();
/**
* 6.登入验证
* checkUser(u.getUserId(),u.getPasswd())
*/
if (checkUser(u.getUserId(),u.getPasswd())) {
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);//成功
oos.writeObject(message);//将ms写入数据通道
//7.创建一个线程,用于和客户端保持通讯,该线程需要持有socket对象
ServerConnectClientThread scct = new ServerConnectClientThread(socket, u.getUserId());
scct.start();//启动线程
//8.把该线程对象放入到一个集合中
ManageThread.addServerConnectClientThread(u.getUserId(), scct);
} else {
System.out.println("用户" + u.getUserId() + "登入失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);//失败
oos.writeObject(message);//将ms写入数据通道
socket.close();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//9.如果服务器端退出了while,说明服务器不在监听,需要关闭serverSocket
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ServerConnectClientThread.java
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.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
/**
* className:ServerConnectClientThread
* 6该线程类的对象用于服务端和某个客户端保持通讯的线程类
*
* @author DiaoXian
*/
public class ServerConnectClientThread extends Thread {
private Socket socket;
private String userId;//连接到服务端的用户ID
//离线留言
ArrayList iOff = new ArrayList<Message>();
private static ConcurrentHashMap<String, ArrayList<Message>> offLineDb = new ConcurrentHashMap<>();
public ServerConnectClientThread(Socket socket, String userId) {
this.socket = socket;
this.userId = userId;
}
public Socket getSocket() {
return socket;
}
@Override
public void run() {
//线程处于run运行状态,可以发送/接收消息
while (true) {
try {
System.out.println("服务端与客户端" + userId + "保持通讯...");
//得到输入流对象
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
Message message = (Message) ois.readObject();
/**
* 根据message类型,业务处理拉取在线用户列表
*/
if (message.getMesType().equals(MessageType.MESSAGE_GET_ONLINE_FRIEND)) {
System.out.println(message.getSender() + " 需要拉取在线用户列表...");
//2.得到需要返回的字符串
String onlineUser = ManageThread.getOnlineUser();
//3.构建message对象,返回给客户端
Message message2 = new Message();
message2.setMesType(MessageType.MESSAGE_RET_ONLINE_FRIEND);
message2.setContent(onlineUser);//包含用户列表的字符串onlineUser
message2.setGetter(message.getSender());
//4.写入数据通道,返回给客户端
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message2);
/**
* 客户端无异常退出
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_CLIENT_EXIT)) {
System.out.println(message.getSender() + "退出");
//需要将客户端对应线程,从集合中删除
ManageThread.removeServerConnectClientThread(message.getSender());
socket.close();
break;//退出线程
/**
* 私聊消息
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_COMM_MES)) {
//服务端判断
if (ManageThread.getServerConnectClientThread(message.getGetter()) == null) {
System.out.println("消息发送失败转为离线留言...");
//将离线消息的接收者message.getGetter()和message消息存入集合
iOff.add(message);
offLineDb.put(message.getGetter(), iOff);
} else {
//服务器将Message消息转发给对应线程的socket,由对应的客户端接收
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
oos.writeObject(message);
}
System.out.println(message.getSender() + "进行了私聊消息");
/**
* 群发消息
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_TO_ALL_MES)) {
//需要遍历管理线程的集合,把所有现成的socket得到,把message转发
HashMap<String, ServerConnectClientThread> hm = ManageThread.getHm();
Iterator<String> iterator = hm.keySet().iterator();
while (iterator.hasNext()) {
//取出在线用户ID
String onlineUserId = iterator.next().toString();
//排除群发消息的用户自身
if (!onlineUserId.equals(message.getSender())) {
//服务器将Message消息转发给对应线程的socket,由对应的客户端接收
ObjectOutputStream oos = new ObjectOutputStream
(hm.get(onlineUserId).getSocket().getOutputStream());
oos.writeObject(message);
}
}
System.out.println(message.getSender() + "进行了群发消息");
/**
* 文件传输
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_FILE_MES)) {
//服务端将message转发给对应message.getGetter()线程的socket
ObjectOutputStream oos = new ObjectOutputStream
(ManageThread.getServerConnectClientThread(message.getGetter()).getSocket().getOutputStream());
oos.writeObject(message);
System.out.println(message.getSender() + "进行了文件传输");
/**
* 离线留言转发
*/
} else if (message.getMesType().equals(MessageType.MESSAGE_OFF_LINE)) {
// System.out.println("其他业务暂时不处理...");
//判断有没有对应的getterId
if (offLineDb.containsKey(message.getSender())) {
ArrayList<Message> foundMessages = offLineDb.get(message.getSender());
for (Message message1 : foundMessages) {
/**
* 判断登入的用户Id是否和offLineDb存入过的getterId有没有相等的
* 注意这里是判断字符串相等,需要使用 equals()
* getterId == userId X
*/
if (!message1.getGetter().equals(message.getSender())) {
continue;
}
// 在此处对获取到的Message进行操作
message1.setMesType(MessageType.MESSAGE_OFF_LINE);
//发送给对应客户端
ServerConnectClientThread serverConnectClientThread = ManageThread.getServerConnectClientThread(message.getSender());
ObjectOutputStream oos = new ObjectOutputStream
(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message1);
//最后将离线留言删除
offLineDb.remove(message.getSender());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ManageThread.java
package com.hspedu.qqserver.service;
import java.util.HashMap;
import java.util.Iterator;
/**
* className:ManageThread
* 7该类管理服务器端连接到客户端的线程的类
*
* @author DiaoXian
*/
public class ManageThread {
//把多个线程放入一个HashMap集合,(key:用户id , value:线程)
private static HashMap<String, ServerConnectClientThread> hm = new HashMap<>();
//返回hm
public static HashMap<String, ServerConnectClientThread> getHm() {
return hm;
}
//将线程对象放入集合
public static void addServerConnectClientThread(String userId, ServerConnectClientThread ccst) {
hm.put(userId, ccst);
}
//通过userID得到对应线程
public static ServerConnectClientThread getServerConnectClientThread(String userId) {
return hm.get(userId);
}
/**
* 编写方法,返回在线用户列表
*
* @return String
*/
public static String getOnlineUser() {
//遍历集合,遍历HashMap的key
Iterator<String> iterator = hm.keySet().iterator();
String onlineUserList = "";
while (iterator.hasNext()) {
onlineUserList += iterator.next().toString() + " ";
}
return onlineUserList;
}
/**
* 编写方法,从集合中删除线程对象
*/
public static void removeServerConnectClientThread(String userId) {
hm.remove(userId);
}
}
QQFrame.java
package com.hspedu.qqframe;
import com.hspedu.qqserver.service.QQServer;
/**
* className:QQFrame
* 8该类创建QQServer,启动后台服务器
* @author DiaoXian
*/
public class QQFrame {
public static void main(String[] args) {
new QQServer();
}
}
sendNewsToAllService.java
package com.hspedu.qqserver.service;
import com.hspedu.qqcommon.Message;
import com.hspedu.qqcommon.MessageType;
import com.hspedu.qqserver.utils.Utility;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
/**
* className:sendNewsToAllService
* 11服务器端推送新闻相关类
*
* @author DiaoXian
*/
public class sendNewsToAllService implements Runnable {
@Override
public void run() {
//为了可以推送多次新闻,使用循环
while (true) {
Message message = new Message();
message.setSender("服务器");
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
Date date = new Date(); // 获取当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒");
String formattedDate = sdf.format(date);//格式化
message.setGetSenderTime(formattedDate);//java.util.Date年月日时分秒
System.out.print("请输入推送的新闻:(exit退出推送) ");
String news = Utility.readString(100);
if ("exit".equals(news)){//退出推送
break;
}
message.setContent(news);
//遍历所有通信线程,得到socket
HashMap<String, ServerConnectClientThread> hm = ManageThread.getHm();
Iterator<String> iterator = hm.keySet().iterator();
while (iterator.hasNext()) {
String next = iterator.next().toString();
//通过oos发送到对应线程
try {
ObjectOutputStream oos = new ObjectOutputStream(hm.get(next).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
//提示信息
System.out.println(message.getSender()+"对所有在线用户说:"+message.getContent());
System.out.println("推送时间:"+message.getGetSenderTime());
}
}
}
运行示例
客户端
服务器端