服务端推送新闻
思路分析
服务端
- 推送消息/新闻,实质上就是群发消息
- 在服务端启动一条独立的线程,专门负责发送推送新闻
具体实现
服务端
SendNewsToAllService.java : 创建线程,管理新闻的推送
package com.ming.qqserver.service;
import com.ming.qqcommon.Message;
import com.ming.qqcommon.MessageType;
import com.ming.qqserver.untils.Utility;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
/**
* @Author: mei_ming
* @DateTime: 2022/10/3 23:25
* @Description: 服务器端推送新闻的服务
*/
public class SendNewsToAllService implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("请输入服务端要推送的新闻/消息[输入exit退出]");
String news = Utility.readString(1000);
if ("exit".equals(news)){
break;
}
//构建Message
Message message = new Message();
message.setSender("服务器");
message.setContent(news);
message.setSendTime(new Date().toString());
message.setMesType(MessageType.MESSAGE_TO_ALL_MES);
System.out.println("服务端推送消息给所有人 :" + news);
//遍历当前所有的通信线程,得到socket,并发送message
HashMap<String, ServerConnectClientThread> hm = ManageClientThreads.getHm();
Iterator<String> iterator = hm.keySet().iterator();
while (iterator.hasNext()) {
String onlineUser = iterator.next().toString();
try {
ObjectOutputStream oos = new ObjectOutputStream(hm.get(onlineUser).getSocket().getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
QQServer .java : 在监听端口下面,加上启动推送新闻的线程
//启动推送新闻线程
new Thread(new SendNewsToAllService()).start();
运行效果
- 登录2个用户。孙悟空、猪八戒
- 服务器上发送消息,孙悟空、猪八戒接收系统消息
离线发送
思路分析
- 在QQServer 写一个集合用于接收离线消息
ConcurrentHashMap<String,ArrayList<Message>>
- 用户登录后判断他是否有离线消息
- 新增MessageType 用于返回私聊消息 ‘对方不在线’
功能实现
共通
MessageType.java :新增消息类型
package com.ming.qqcommon;
/**
* @Author: mei_ming
* @DateTime: 2022/10/2 18:31
* @Description: 表示消息类型
*/
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_RET_COMM_MES = "9"; //延迟发送,返回提示信息
}
服务端
package com.ming.qqserver.service;
import com.ming.qqcommon.Message;
import com.ming.qqcommon.MessageType;
import com.ming.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.HashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author: mei_ming
* @DateTime: 2022/10/2 20:52
* @Description: 这是服务器,在监听9999,等待客户端的连接,并保持通信
*/
public class QQServer {
private ServerSocket ss = null;
//创建一个集合,存放多个用户,如果是这些用户则登录通过
private static ConcurrentHashMap<String, User> validUsers = new ConcurrentHashMap<>();
//创建一个集合,存放离线消息Message, Message可以有多条 所以用List
private static ConcurrentHashMap<String, ArrayList<Message>> offMessages = new ConcurrentHashMap<>();
public static ConcurrentHashMap<String, ArrayList<Message>> getOffMessages() {
return offMessages;
}
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("猪八戒", new User("猪八戒", "123456"));
validUsers.put("沙和尚", new User("沙和尚", "123456"));
}
//判断用户是否有离线消息,如果有,则发送
private void checkOffMessages(String userId, Socket socket) {
ArrayList<Message> messages = offMessages.get(userId);
if (messages == null) {
return;
}
for (Message message : messages) {
try {
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(message);
} catch (IOException e) {
e.printStackTrace();
}
}
offMessages.remove(userId);
}
//验证用户是否有效的方法
private boolean checkUser(String userId, String passwd) {
User user = validUsers.get(userId);
if (user == null) {
return false;
}
if (!user.getPasswd().equals(passwd)) {
return false;
}
return true;
}
public QQServer() {
try {
//注意:端口可以写在配置文件中
System.out.println("服务端在9999端口监听...");
//启动推送新闻线程
new Thread(new SendNewsToAllService()).start();
ss = new ServerSocket(9999);
while (true) { // 当和某个客户端连接后,会继续监听
Socket socket = ss.accept();
ObjectInputStream ois =
new ObjectInputStream(socket.getInputStream());
// 得到socket关联的对象输出流
ObjectOutputStream oos =
new ObjectOutputStream(socket.getOutputStream());
User user = (User) ois.readObject();
//创建一个Message对象,准备回复客户端
Message message = new Message();
//验证用户,是否有效
boolean isExists = checkUser(user.getUserId(), user.getPasswd());
if (isExists) { //登录成功
message.setMesType(MessageType.MESSAGE_LOGIN_SUCCEED);
//将message对象回复到客户端
oos.writeObject(message);
//创建一个线程,和客户端保持通信,该线程需要持有socket对象 ServerConnectClientThread
ServerConnectClientThread connectClientThread = new ServerConnectClientThread(socket, user.getUserId());
connectClientThread.start();
//把该线程放入集合
ManageClientThreads.addServerConnectClientThread(user.getUserId(), connectClientThread);
//检查登录用户是否有离线消息,若有,则发送
checkOffMessages(user.getUserId(),socket);
} else {
System.out.println("用户 id=" + user.getUserId() + " pwd=" + user.getPasswd() + "登录失败");
message.setMesType(MessageType.MESSAGE_LOGIN_FAIL);
oos.writeObject(message);
//关闭socket
socket.close();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ServerConnectClientThread.java : 修改私聊逻辑代码,先判断对方是否在线,若不在线,则存到离线消息集合中
package com.ming.qqserver.service;
import com.ming.qqcommon.Message;
import com.ming.qqcommon.MessageType;
import com.sun.deploy.net.proxy.ProxyUnavailableException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author: mei_ming
* @DateTime: 2022/10/2 21:09
* @Description: 该类的一个对象和某个客户端保持通信
*/
public class ServerConnectClientThread extends Thread {
private Socket socket;
private String userId; //区分是哪一个客户端
public Socket getSocket() {
return socket;
}
public ServerConnectClientThread(Socket socket, String userId) {
this.socket = socket;
this.userId = userId;
}
@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)){
//客户端要在线用户列表
// 格式 "100 200 孙悟空"
System.out.println(message.getSender() + " 要在线用户列表");
String onlineUser = ManageClientThreads.getOnlineUser();
//返回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)){
//私聊功能
//判断对方是否在线,若不在线,则存到 offMessages 集合中去
boolean userAlive = isUserAlive(message);
if (userAlive){
//根据message获取getterId,然后得到对应的线程
ServerConnectClientThread serverConnectClientThread = ManageClientThreads.getServerConnectClientThread(message.getGetter());
//在线程里获取对应的socket,在获取socket对应的对象输出流
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
oos.writeObject(message); //转发, 如果客户不在线,可以把内容存到数据库, 做成离线发送消息
}else{ //返回提示消息给自己,说明当前不在线,消息延后送达
ServerConnectClientThread serverConnectClientThread = ManageClientThreads.getServerConnectClientThread(message.getSender());
ObjectOutputStream oos = new ObjectOutputStream(serverConnectClientThread.getSocket().getOutputStream());
Message message2 = new Message();
message2.setSender("服务器");
message2.setContent(message.getGetter()+" 当前不在线,消息将延时送达。");
message2.setMesType(MessageType.MESSAGE_RET_COMM_MES);
oos.writeObject(message2);
}
}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()){
//取出在线用户的id
String onlineUser = iterator.next().toString();
if (!onlineUser.equals(message.getSender())){ //排除群发消息的用户
// 发送message
ServerConnectClientThread serverConnectClientThread = ManageClientThreads.getServerConnectClientThread(onlineUser);
OutputStream outputStream = serverConnectClientThread.getSocket().getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(outputStream);
oos.writeObject(message);
}
}
}else if(message.getMesType().equals(MessageType.MESSAGE_FILE_MES)){
//发送文件消息功能
//将message转发给getterId的线程的socket
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 (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//判断对方是否在线,若不在线,则存到 offMessages 集合中去
private boolean isUserAlive(Message message) {
String getter = message.getGetter();
String onlineUser = ManageClientThreads.getOnlineUser();
String[] users = onlineUser.split(" ");
for (int i =0;i<users.length;i++){
if (!getter.equals(users[i])){
//不在线
ConcurrentHashMap<String, ArrayList<Message>> offMessages = QQServer.getOffMessages();
ArrayList<Message> messages = offMessages.get(getter);
if (messages == null){
messages =new ArrayList<>();
}
messages.add(message);
offMessages.put(getter,messages);
return false;
}else{
return true;
}
}
return true;
}
}
客户端
ClientConnectServerThread.java : 新增对类型的判断MESSAGE_RET_COMM_MES
package com.ming.qqclient.service;
import com.ming.qqcommon.Message;
import com.ming.qqcommon.MessageType;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
/**
* @Author: mei_ming
* @DateTime: 2022/10/2 20:25
* @Description: 线程生成类
*/
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){
try {
System.out.println("客户端线程,等待读取从服务端发送的消息");
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
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.getSendTime()+" "+ message.getSender()+" 对 "+
message.getGetter()+" 说: "+message.getContent());
} else if (message.getMesType().equals(MessageType.MESSAGE_RET_COMM_MES)){
//当对方不在线,发送消息返回提示
System.out.println("\n"+ message.getSender()+"通知:"+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 (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public Socket getSocket() {
return socket;
}
public void setSocket(Socket socket) {
this.socket = socket;
}
}
运行效果
-
孙悟空 上线,对猪八戒发送消息
-
猪八戒上线,接收孙悟空发的消息