websocket 集群处理方案

  2020年初 疫情的突然袭来、让人们都宅在家里,越来越多的公司、平台上线了直播电商的业务。笔者的公司也打算做小程序直播订单的业务。直播互动、点赞功能、统计用户在线时长的频率(1/1min)的心跳消息打算用websocket 来实现。 

  项目中使用的spring结合websocket配置。直播的互动消息和聊天室的功能很像。网上也有很多demo 可以参考。

  在服务器集群的环境下,会涉及到WebSocket Session共享的问题。针对这个问题想到一个比较简单的解决办法是。

  1、关于在线人数用redis集群去维护在线人数 ,可以根据活动id (或者聊天室id)等构件key-value 唯一键值对,通过人数递增、递减去维护。

  2、关于发送消息,可能会存在同一直播间的用户被nginx 负载均衡到不同的机器上,也就会出现 一个直播活动(聊天室) 同时被创建并存在  A 服务器 和 B 服务器上  所以就要想办法让处于两个服务器上的用户像在一台上一样,可以交流。这里发弹幕(发消息)、上线通知、点赞、用户心跳信息等消息 都是用kafka 去处理。消息的类型可以用一个type字段进行区分, producer 把消息发送到 一个topic中。 然后kafka consumer  去接收这个topic 的消息去处理,这里面接收的时候 consumer 需要设置不同的group_id(可以设置成服务器ip)确保都接收到消息。接收到消息在判断是否存在 该活动直播(聊天室),如果有则处理推送消息等相关操作。下面是部分功能代码

 1、websocket (连接、断开连接、发送消息 、接收消息、在线人数)

  1 /**
  2  *
  3  * webSocket消息
  4  * @author kim
  5  * @date 2020/3/12
  6 */
  7 @Component
  8 @Slf4j
  9 @ServerEndpoint("/websocket/{activityId}/{username}")
 10 public class WebSocket {
 11 
 12     public static RedisUtil redisUtil;
 13 
 14     private static String liveMessageTopic = "liveMessageTopic";
 15 
 16     public static ShopLiveMessageService shopLiveMessageService;
 17 
 18     /**
 19      * 使用map来收集session,key为直播活动id,value为同一个活动的用户集合
 20     */
 21     private static Map<Integer, Set<Session>> lives = new ConcurrentHashMap<>();
 22 
 23     @Autowired
 24     private DocSensitiveService docSensitiveService;
 25 
 26     /**
 27      * 建立连接
 28      *
 29      * @param session
 30      */
 31     @OnOpen
 32     public void onOpen(@PathParam("activityId")Integer activityId, @PathParam("username") String username, Session session)
 33     {
 34         //onlineNumber++;
 35         if (!lives.containsKey(activityId)){
 36             //创建一个新的活动房间
 37             Set<Session> room = new HashSet<Session>();
 38             room.add(session);
 39             lives.put(activityId, room);
 40             // 初始该活动点赞数
 41             updateZanNum(activityId, 0);
 42         }else {
 43             lives.get(activityId).add(session);
 44         }
 45         // 该活动在线人数+1
 46         updateOnlineNum(activityId);
 47         log.info("现在来直播活动"+activityId+"连接的客户id:"+session.getId()+"用户名:"+username);
 48         log.info("有新连接加入! 当前在线人数" + getOnlineNum(activityId));
 49         try {
 50             //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息 5 在线人数
 51             //先给所有人发送通知,说我上线了
 52             String dateStr = TimeUtils.date2Str(new Date(), "yyyy-MM-dd HH:mm:ss");
 53             LiveMessageProduct liveMessageProduct = new LiveMessageProduct();
 54             liveMessageProduct.setNickname(username);
 55             liveMessageProduct.setActivityId(activityId);
 56             liveMessageProduct.setCreateTime(dateStr);
 57             liveMessageProduct.setUpdateTime(dateStr);
 58             liveMessageProduct.setMessageType(1);
 59             shopLiveMessageService.liveMessageSendToKafka(liveMessageProduct, liveMessageTopic);
 60             //给所有人发一条消息:更新在线人数
 61             sendOnlineNum(activityId);
 62             // 给自己发:目前活动点赞数量
 63             Map<String,Object> map3 = new HashMap<>();
 64             map3.put("messageType",6);
 65             map3.put("zanNumber",getZanNum(activityId));
 66             sendMessageTo(JSON.toJSONString(map3), session);
 67         }
 68         catch (IOException e){
 69             log.error(username+"上线的时候通知所有人发生了错误");
 70         }
 71     }
 72 
 73     @OnError
 74     public void onError(Session session, Throwable error) {
 75         log.error("服务端发生了错误"+error);
 76         //error.printStackTrace();
 77     }
 78     /**
 79      * 连接关闭
 80      */
 81     @OnClose
 82     public void onClose(@PathParam("activityId")Integer activityId, @PathParam("username") String username, Session session)
 83     {
 84         // 更新在线人数
 85         decrOnlineNum(activityId);
 86         lives.get(activityId).remove(session);
 87         try {
 88             //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息 5 在线人数
 89             //发送kafka 在线人数
 90             sendOnlineNum(activityId);
 91         }
 92         catch (Exception e){
 93             log.error(username+"下线的时候通知所有人发生了错误");
 94         }
 95         log.info("有连接关闭! 当前在线人数" + getOnlineNum(activityId));
 96     }
 97 
 98     /**
 99      * 收到客户端的消息
100      *
101      * @param message 消息
102      * @param session 会话
103      */
104     @OnMessage
105     public void onMessage(@PathParam("activityId")Integer activityId, @PathParam("username") String username, String message, Session session)
106     {
107         try {
108             log.info("来自客户端消息:" + message+"客户端的id是:"+session.getId());
109             JSONObject jsonObject = JSON.parseObject(message);
110             Integer companyId = Integer.parseInt(jsonObject.getString("company_id"));
111             String openid = jsonObject.getString("openid");
112             String unionid = jsonObject.getString("unionid");
113             String avatar = jsonObject.getString("avatar");
114             Integer messageType = jsonObject.getInteger("messageType");
115             //messageType 1代表上线 2代表下线 3代表在线名单  4代表普通消息 5 在线人数 6点赞 7 心跳包
116             LiveMessageProduct liveMessageProduct = new LiveMessageProduct();
117             if (messageType.equals(4)){
118                 String textMessage = jsonObject.getString("message");
119                 liveMessageProduct.setContent(textMessage);
120             }else if (messageType.equals(6)){
121                 updateZanNum(activityId, 1);
122                 liveMessageProduct.setZanNumber(getZanNum(activityId));
123             }
124             // 消息同步kafka
125             String dateStr = TimeUtils.date2Str(new Date(), "yyyy-MM-dd HH:mm:ss");
126             liveMessageProduct.setNickname(username);
127             liveMessageProduct.setActivityId(activityId);
128             liveMessageProduct.setCompanyId(companyId);
129             liveMessageProduct.setOpenid(openid);
130             liveMessageProduct.setUnionid(unionid);
131             liveMessageProduct.setAvatar(avatar);
132             liveMessageProduct.setCreateTime(dateStr);
133             liveMessageProduct.setUpdateTime(dateStr);
134             liveMessageProduct.setMessageType(messageType);
135             shopLiveMessageService.liveMessageSendToKafka(liveMessageProduct, liveMessageTopic);
136             shopLiveMessageService.liveMessageSendToKafka(liveMessageProduct, "saveLiveMessageTopic");
137         }
138         catch (Exception e){
139             log.error("发生了错误了-{}", e);
140         }
141 
142     }
143 
144     /**
145      *
146      * 获取在线人数
147      * @param activityId 直播id
148      * @author kim
149      * @date 2020/4/2
150      * @return java.lang.Long
151     */
152     private Long getOnlineNum(Integer activityId){
153         long onlineNum = 0;
154         String redisKey = String.format("LiveActivityOnlineNum:%s",activityId.toString());
155         if (redisUtil.hasKey(redisKey)){
156             onlineNum = Long.parseLong(redisUtil.get(redisKey).toString());
157         }
158         return onlineNum;
159     }
160 
161     /**
162      *
163      * 获取点赞数
164      * @param activityId 直播id
165      * @author kim
166      * @date 2020/4/2
167      * @return java.lang.Long
168     */
169     private Long getZanNum(Integer activityId){
170         long zanNum = 0;
171         String redisKey = String.format("LiveActivityZanNum:%s",activityId.toString());
172         if (redisUtil.hasKey(redisKey)){
173             zanNum = Long.parseLong(redisUtil.get(redisKey).toString());
174         }
175         return zanNum;
176     }
177     /**
178      *
179      * redis 中写入该直播在线人数 初始或者递增
180      * @param activityId 直播id
181      * @author kim
182      * @date 2020/4/2
183     */
184     private void updateOnlineNum(Integer activityId){
185         String redisKey = String.format("LiveActivityOnlineNum:%s",activityId.toString());
186         if (!redisUtil.hasKey(redisKey)){
187             redisUtil.set(redisKey,1);
188         }else {
189             redisUtil.incrBy(redisKey,1L);
190         }
191     }
192     /**
193      *
194      * 递减在线人数
195      * @param activityId 直播id
196      * @author kim
197      * @date 2020/4/2
198     */
199     private void decrOnlineNum(Integer activityId){
200         String redisKey = String.format("LiveActivityOnlineNum:%s",activityId.toString());
201         if (redisUtil.hasKey(redisKey)){
202             redisUtil.decrBy(redisKey,1L);
203         }
204     }
205 
206     /**
207      *
208      * redis 中写入该直播点赞人数 初始或者递增
209      * @param activityId 直播id
210      * @param num 点赞数
211      * @author kim
212      * @date 2020/4/2
213     */
214     private void updateZanNum(Integer activityId, Integer num){
215         String redisKey = String.format("LiveActivityZanNum:%s",activityId.toString());
216         System.out.println(redisUtil.hasKey(redisKey));
217         if (!redisUtil.hasKey(redisKey)){
218             redisUtil.set(redisKey,num);
219         }else {
220             redisUtil.incrBy(redisKey,num.longValue());
221         }
222     }
223 
224     /**
225      *
226      * 发送kafka 在线人数消息
227      * @param activityId 直播id
228      * @author kim
229      * @date 2020/4/2
230     */
231     private void sendOnlineNum(Integer activityId){
232         String dateStr = TimeUtils.date2Str(new Date(), "yyyy-MM-dd HH:mm:ss");
233         LiveMessageProduct liveMessageProduct5 = new LiveMessageProduct();
234         liveMessageProduct5.setActivityId(activityId);
235         liveMessageProduct5.setCreateTime(dateStr);
236         liveMessageProduct5.setUpdateTime(dateStr);
237         liveMessageProduct5.setMessageType(5);
238         liveMessageProduct5.setOnLineNumber(getOnlineNum(activityId));
239         shopLiveMessageService.liveMessageSendToKafka(liveMessageProduct5, liveMessageTopic);
240     }
241 
242     /**
243      *
244      * 点对点发消息
245      * @param message 消息内容
246      * @param session session
247      * @author kim
248      * @date 2020/4/2
249     */
250     private void sendMessageTo(String message, Session session) throws IOException {
251         session.getAsyncRemote().sendText(message);
252     }
253 
254     /**
255      *
256      * 给同一直播间用户发送消息
257      * @param message 消息内容
258      * @param activityId 直播id
259      * @author kim
260      * @date 2020/4/2
261     */
262     public void sendMessageByKafka(String message, Integer activityId)throws IOException {
263         if (lives.containsKey(activityId)){
264             for (Session session: lives.get(activityId)){
265                 session.getBasicRemote().sendText(jsonObject.toJSONString());
266             }
267         }
268     }
269 
270 }
websocket (连接、断开连接、发送消息 、接收消息、在线人数)
 2、直播互动消息发送kafka
 1 /**
 2      *
 3      * 直播互动消息发送kafka
 4      * @param liveMessageProduct
 5      * @author kim
 6      * @date 2020/3/27
 7      * @return void
 8     */
 9     @Override
10     public void liveMessageSendToKafka(LiveMessageProduct liveMessageProduct, String topic){
11         String liveMessageStr = JSONObject.toJSONString(liveMessageProduct);
12         log.info("直播互动消息---{}", liveMessageStr);
13         try {
14             kafkaTemplate.send(topic, liveMessageStr);
15         } catch (Exception e) {
16             log.error("直播互动消息发送kafka[{}]-失败:{}",topic, liveMessageStr);
17         }
18     }
直播互动消息发送kafka
 3、直播互动消息数据kafka监听处理
 1 @Slf4j
 2 @Component
 3 public class LiveMessageConsumer {
 4 
 5     @Autowired
 6     ShopLiveMessageService shopLiveMessageService;
 7 
 8     @Autowired
 9     WebSocket webSocket;
10     /**
11      * 直播互动消息数据监听处理
12      *
13      * @param records
14      */
15     @KafkaListener(topics = "liveMessageTopic", groupId = "#{getGroupId.getLocalHost()}",containerFactory = "liveKafkaListenerContainerFactory")
16     public void listenModelEvent(List<ConsumerRecord<?, ?>> records, Acknowledgment ack){
17         //立即commit,保证最多接收一次
18         ack.acknowledge();
19         for (ConsumerRecord<?,?> record : records){
20             String key = (String) record.key();
21             String value = (String) record.value();
22             JSONObject jsonObject = JSONObject.parseObject(value);
23             System.out.println("当前消息内容:"+jsonObject.toJSONString());
24             try {
25                 Integer activityId = jsonObject.getInteger("activity_id");
26                 Integer messageType = jsonObject.getInteger("messageType");
27                 Map<String,Object> map = new HashMap<>(16);
28                 map.put("messageType",messageType);
29                 switch (messageType){
30                     case 6:
31                         // 推送websocket 点赞人数
32                         map.put("zanNumber", jsonObject.getBigInteger("zanNumber"));
33                         break;
34                     case 5:
35                         // 更新在线人数 推送websocket消息
36                         map.put("onlineNumber", jsonObject.getBigInteger("onLineNumber"));
37                         break;
38                     case 4:
39                         // 推送websocket消息
40                         map.put("textMessage",jsonObject.getString("content"));
41                         map.put("avatar", jsonObject.getString("avatar"));
42                         map.put("fromusername",jsonObject.getString("nickname"));
43                         break;
44                     case 1:
45                         // 上线 推送websocket消息
46                         map.put("username", jsonObject.getString("nickname"));
47                         break;
48                     default:
49                 }
50                 webSocket.sendMessageByKafka(JSON.toJSONString(map), activityId);
51             } catch (Exception e) {
52                 log.error(e.getMessage(),key);
53             }
54         }
55     }
56 }   
直播互动消息数据kafak监听处理

 4、获取服务器ip代码

 1 @Service
 2 @Slf4j
 3 public class GetGroupId {
 4 
 5     public String getLocalHost() {
 6         List<String> ipList = new ArrayList<String>();
 7         InetAddress[] addrList;
 8         String finalIp = "";
 9         try
10         {
11             Enumeration interfaces= NetworkInterface.getNetworkInterfaces();
12             while(interfaces.hasMoreElements())
13             {
14                 NetworkInterface ni=(NetworkInterface)interfaces.nextElement();
15                 Enumeration ipAddrEnum = ni.getInetAddresses();
16                 while(ipAddrEnum.hasMoreElements())
17                 {
18                     InetAddress addr = (InetAddress)ipAddrEnum.nextElement();
19                     if (addr.isLoopbackAddress() == true)
20                     {
21                         continue;
22                     }
23 
24                     String ip = addr.getHostAddress();
25                     if (ip.indexOf(":") != -1)
26                     {
27                         //skip the IPv6 addr
28                         continue;
29                     }
30                     log.debug("Interface: " + ni.getName()
31                             + ", IP: " + ip);
32                     ipList.add(ip);
33                 }
34             }
35             Collections.sort(ipList);
36             if (ipList.size()>0){
37                 finalIp = ipList.get(0);
38             }
39         }
40         catch (Exception e)
41         {
42             e.printStackTrace();
43             log.error("Failed to get local ip list. " + e.getMessage());
44             throw new RuntimeException("Failed to get local ip list");
45         }
46         return finalIp;
47     }
48 }
获取服务器ip

 参考资料:Spring Boot整合websocket实现群聊,点对点聊天

posted @ 2020-04-28 17:56  yangjl01  阅读(6549)  评论(5编辑  收藏  举报