学习springboot的旅程,就像蜗牛爬山,一点点的往上爬,一点点的欣赏旅途的风景
小猿公司的系统又要升级了,现在要实现在线聊天。这个怎么搞呢?
特别是在分布式系统下,怎么搞分布式在线聊天呢?
- 第一点:客户端与服务端通过websocket技术创建连接(这样服务端才能向客户端推送数据)
- 第二点:服务端之间的通信,请看redis的订阅和发布
- 第三点:创建聊天测试页面
springboot 实现分布式在线聊天
-
第一点实现步骤如下:
-
第一步:pom.xml配置
<!-- websocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
-
第二步:创建配置类@Configuration【注意】:如果有安全框架的,必须排除websocket的路径拦截
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * 开启WebSocket支持 */ @Configuration public class WebSocketConfig { /** * websocket的核心对象 * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } @Autowired public void setMessageService(RedisUtil redisUtil) { HxzSocketServer.redisUtil = redisUtil; } }
- 相关依赖类
//import com.example.hxzboot.Dome.Sys.Core.Service.CoreService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; /** * redis 操作工具类 必须线程安全 synchronized修饰其方法 */ @Component("redisUtil") public class RedisUtil { @Value("${spring.redis.msg.title}") private String title; @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisTemplate<String, String> redisTemplate; //@Autowired //private CoreService coreService; //以下方法必须线程安全 public synchronized void save(String key,String value){ stringRedisTemplate.opsForValue().set(key,value); } public synchronized void del(String key){ stringRedisTemplate.delete(key); } public synchronized String get(String key){ return stringRedisTemplate.opsForValue().get(key); } /** * 发送主题消息 * @param msg */ public synchronized void sendTitle(String msg){ stringRedisTemplate.convertAndSend(title,msg); } } /********************************************************************/ /** * 静态常量区 */ public class StaticClass { public final static int HXZ_USER_YH=1;//用户 public final static int HXZ_USER_YK=0;//游客 } /********************************************************************/ import java.io.Serializable; /** * 消息通知模型 */ public class SocketMode implements Serializable { /** * 消息类型是Map 可解析 */ public final static String MEG_TYPE_JSON="Map"; /** * 消息类型是String */ public final static String MEG_TYPE_STRING="String"; /** * 表示解绑 */ public final static int ACTION_DEL=-1; /** * 表示绑定 */ public final static int ACTION_ADD=1; /** * 表示发送消息 */ public final static int ACTION_SEND=0; private int action;//行为定义 private String sendSid;//发送方 private String msgSid;//接收方id(【注意】 * 表示所有人接收) private String msgType;//消息类型 private String msgkey;//消息存放在Redis的key private String msg;//消息 public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getMsgType() { return msgType; } public void setMsgType(String msgType) { this.msgType = msgType; } public String getMsgkey() { return msgkey; } public void setMsgkey(String msgkey) { this.msgkey = msgkey; } public String getMsgSid() { return msgSid; } public void setMsgSid(String msgSid) { this.msgSid = msgSid; } public int getAction() { return action; } public void setAction(int action) { this.action = action; } public String getSendSid() { return sendSid; } public void setSendSid(String sendSid) { this.sendSid = sendSid; } }
-
第四步:websocket业务类,说明都在类里面,请认真看说明
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.example.hxzboot.Dome.Util.RedisUtil; import com.example.hxzboot.Dome.Util.StaticClass; import org.apache.shiro.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.List; import java.util.Vector; import java.util.concurrent.CopyOnWriteArraySet; /** * 用@ServerEndpoint标识 socket的服务类 * 【注意】:每个客户端连接都是一个HxzSocketServer * */ @ServerEndpoint("/hxzsocket/{sid}")//表示启动websocket服务--并拦截请求头是/hxzsocket/的请求 @Component public class HxzSocketServer { private Logger logger = LoggerFactory.getLogger(this.getClass()); public static RedisUtil redisUtil; //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static int onlineCount = 0; //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<HxzSocketServer> webSocketSet = new CopyOnWriteArraySet<HxzSocketServer>(); //concurrent包的线程安全Set(记录连接人) private CopyOnWriteArraySet<String> linkUserId=new CopyOnWriteArraySet<String>(); //接收sid--唯一标识 private String sid=""; //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; //当前socket的状态(游客和非游客),默认表示用户; private int SocketType=StaticClass.HXZ_USER_YH; //用户名(默认游客) private String realmName="游客"; /** * 连接建立成功调用的方法*/ @OnOpen public void onOpen(Session session,@PathParam("sid") String ssid,@PathParam("lid") String lid) { if(ssid.indexOf("&")!=-1){ String[] ss=ssid.split("&"); for(String s:ss){ if(s.indexOf("sid")!=-1){ ssid=s.split("=")[1]; } if(s.indexOf("lid")!=-1){ lid=s.split("=")[1]; } } } //身份鉴别 /** try{ this.realmName = (String) SecurityUtils.getSubject().getPrincipal(); if(null==this.realmName||"".equals(this.realmName)||"null".equals(this.realmName)||"游客".equals(this.realmName)){ this.SocketType=StaticClass.HXZ_USER_YK;//游客 realmName="游客"; } }catch (Exception e){ this.SocketType=StaticClass.HXZ_USER_YK;//游客 this.realmName="游客"; } **/ this.session = session; this.sid=ssid; //构建连接桥梁 if(!StringUtils.isEmpty(lid)){ linkUserId.add(lid);//绑定 SocketMode sm=new SocketMode(); sm.setAction(SocketMode.ACTION_ADD); sm.setSendSid(this.sid); sm.setMsgSid(lid); redisUtil.sendTitle(JSON.toJSONString(sm)); } webSocketSet.add(this); //加入set中 addOnlineCount(); //在线数加1 logger.info("有新窗口开始监听:"+sid+",当前在线人数为" + getOnlineCount()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { redisUtil.del(this.sid);//下线就清空消息,key默认sid //通知所有绑定的人解绑 for(String lid:linkUserId){ SocketMode sm=new SocketMode(); sm.setAction(SocketMode.ACTION_DEL); sm.setSendSid(this.sid); sm.setMsgSid(lid); redisUtil.sendTitle(JSON.toJSONString(sm)); } webSocketSet.remove(this); //从set中删除 subOnlineCount(); //在线数减1 logger.info("有一连接关闭!当前在线人数为" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息*/ @OnMessage public void onMessage(String message, Session session) { JSONObject jo=JSON.parseObject(message); redisUtil.save(this.sid,jo.getString("msg")); SocketMode sm=new SocketMode(); sm.setAction(SocketMode.ACTION_SEND); sm.setSendSid(this.sid); sm.setMsgSid(jo.getString("lid")); sm.setMsgkey(this.sid); redisUtil.sendTitle(JSON.toJSONString(sm)); logger.info("==》接收到"+sid+"的信息:"+message); } /** * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { logger.error("发生错误"); error.printStackTrace(); } /** * 服务器主动推送 */ public void sendMessageByKey(String rediskey) throws Exception { SocketMode sm=new SocketMode(); sm.setAction(SocketMode.ACTION_SEND);//消息行为 sm.setMsg(redisUtil.get(rediskey)); sendMessage(JSON.toJSONString(sm)); } /** * 服务器主动推送 */ public void sendMessage(String msg) throws Exception { this.session.getBasicRemote().sendText(msg); } /** * 群发自定义消息 * */ public void sendInfo(String message,@PathParam("sid") String sid) throws Exception { logger.info("推送消息到窗口"+sid+",推送内容:"+message); for (HxzSocketServer item : webSocketSet) { try { //这里可以设定只推送给这个sid的,为null则全部推送 if(sid==null) { item.sendMessage(message); }else if(item.sid.equals(sid)){ item.sendMessage(message); } } catch (Exception e) { continue; } } } /** * 绑定聊天对象 * @param lid */ public void addLinkUser(String lid){ linkUserId.add(lid); } /** * 删除聊天对象 * @param lid */ public void delLinkUser(String lid){ linkUserId.remove(lid); } /** * 获取连接人集合 * @return */ public CopyOnWriteArraySet<String> getLinkUserId() { return linkUserId; } public int getSocketType() { return SocketType; } public String getRealmName() { return realmName; } public String getSid() { return sid; } public void setSid(String sid) { this.sid = sid; } public Session getSession() { return session; } public void setSession(Session session) { this.session = session; } public String getSessionId(){ return this.session.getId(); } /** * 在线数量 * @return */ public static synchronized int getOnlineCount() { return onlineCount; } /** * 增加在线数量 */ public static synchronized void addOnlineCount() { HxzSocketServer.onlineCount++; } /** * 减少在线数量 */ public static synchronized void subOnlineCount() { HxzSocketServer.onlineCount--; } /** * 获取当前服务的所有socket 连接对象 * @return */ public static CopyOnWriteArraySet<HxzSocketServer> getWebSocketSet() { return webSocketSet; } }
-
-
第二点的实现,请参考redis的订阅和发布,以下是监听器代码的实现代码
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
//import com.example.hxzboot.Dome.Sys.Core.Service.CoreService;
import com.example.hxzboot.Dome.Sys.WebSocket.HxzSocketServer;
import com.example.hxzboot.Dome.Sys.WebSocket.SocketMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* 监听
*/
@Component("hxzReceiver")
public class HxzReceiver {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private StringRedisTemplate stringRedisTemplate;
//@Autowired
//private CoreService coreService;
/**
* 主键监听
* @param message
*/
public void receiveMessage(String message) {
SocketMode sm=JSONObject.parseObject(message, SocketMode.class);
if(SocketMode.ACTION_ADD==sm.getAction()){//建立连接
for(HxzSocketServer hss:HxzSocketServer.getWebSocketSet()){
if(hss.getSid().equals(sm.getMsgSid())){
hss.addLinkUser(sm.getSendSid());
try{
sm.setMsg(sm.getSendSid()+"与你连接聊天");
hss.sendMessage(JSON.toJSONString(sm));
}catch (Exception e){
logger.error("==>"+sm.getSendSid()+"发送消息给"+sm.getMsgSid()+"失败!!!key="+sm.getMsgkey(),e);
}
}
}
}else if(SocketMode.ACTION_DEL==sm.getAction()){//删除连接
for(HxzSocketServer hss:HxzSocketServer.getWebSocketSet()){
if(hss.getSid().equals(sm.getMsgSid())){
hss.delLinkUser(sm.getSendSid());
try{
sm.setMsg(sm.getSendSid()+"下线了");
hss.sendMessage(JSON.toJSONString(sm));
}catch (Exception e){
logger.error("==>"+sm.getSendSid()+"发送消息给"+sm.getMsgSid()+"失败!!!key="+sm.getMsgkey(),e);
}
}
}
}else if(SocketMode.ACTION_SEND==sm.getAction()){//发送消息
if(sm.getMsgSid().equals("*")){//发送所有人
for(HxzSocketServer hss:HxzSocketServer.getWebSocketSet()){
try{
hss.sendMessageByKey(sm.getMsgkey());
}catch (Exception e){
logger.error("==>"+sm.getSendSid()+"发送消息给"+sm.getMsgSid()+"失败!!!key="+sm.getMsgkey(),e);
}
}
}else{//1对1发送
for(HxzSocketServer hss:HxzSocketServer.getWebSocketSet()){
if(hss.getSid().equals(sm.getMsgSid())){
try{
hss.sendMessageByKey(sm.getMsgkey());
}catch (Exception e){
logger.error("==>"+sm.getSendSid()+"发送消息给"+sm.getMsgSid()+"失败!!!key="+sm.getMsgkey(),e);
}
break;
}
}
}
}
//System.out.println("监听消息发送:"+sm.getSendSid()+"发送消息给"+sm.getMsgSid()+"消息key="+sm.getMsgkey());
}
}
- 第三点:页面websocket连接创建(测试页面创建)
<!doctype html>
<html style="margin: 0px;padding: 0px;height: 100%;">
<head>
<title>首页</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="/static/com/hxz/js/jquery-3.3.1.min.js" ></script>
</head>
<body style="margin: 0px;padding: 0px;height: 100%;overflow-y: hidden;">
<button onclick="linksk()">连接</button>
<br/>
<button onclick="sendmsg()">发送</button><input type="text" id="tx"/>
</body>
<script type="text/javascript">
var ws;
var lid="1568970073976";//连接1号的sid
function linksk(){
ws=new WebSocket("ws://localhost:8033/hxzsocket/"+(new Date()).getTime());//连接1号
//ws=new WebSocket("ws://localhost:8033/hxzsocket/sid=hxz&lid=1568970073976");//连接二号
ws.onopen=function(event){
//alert(event.data);
}
/**
*接收服务端消息
*/
ws.onmessage=function(event){
var data=eval('(' + event.data + ')');
if(data.action==1){//表示创建连接
lid=data.sendSid;//对方id
}
alert(data.msg);
}
/**
*消息错误时提示
*/
ws.onerror = function(){
alert("发生了错误..\n");
}
//关闭事件
ws.onclose = function() {
console.log("websocket已关闭");
};
}
function sendmsg(){
ws.send("{'lid':'"+lid+"','msg':'"+$("#tx").val()+"'}");
}
</script>
</html>
- 到此就实现了简单版分布式聊天功能!!!