最近做一款小程序的答题,接到的需求是答题最后一种玩法为房间PK方式,用户创建房间,邀请好友进入房间,准备后开始PK答题,房间最后一人答题完成则到房间结算页。
这里我们用websocket作为长连接来通知房间用户状态变化,由于生产环境服务器有4台且用nginx做了负载均衡,使用的是轮询策略,所以需要考虑服务器之间的通讯,决定用redis的发布订阅来做消息推送,处理服务器之间的通讯。注意的是:当客户端连上websocket时,此时redis客户端向redis服务器订阅该房间的频道。
1.springboot集成websocket
<dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> </dependency>
2.在websocket类中引入redisService,直接通过@Autowired注入,需要通过spring容器来set
import com.company.project.manage.web.WebSocketServer; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * 获取到spring容器对象 */ @Component public class GetApplicationConfig implements ApplicationContextAware { private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; WebSocketServer.setApplicationContext(applicationContext); } }
import cn.hutool.log.Log; import cn.hutool.log.LogFactory; import com.company.project.manage.service.RedisService; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @ServerEndpoint("/websocket/{roomNo}/{workNumber}") @Component public class WebSocketServer{ private static Log log=LogFactory.get(WebSocketServer.class); public static ExecutorService executorPoolService = Executors.newFixedThreadPool(10); //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>(); //与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; //接收roomNo private String roomNo; //接收workNumber private String workNumber; private RedisService redisService; private static ApplicationContext applicationContext; public static void setApplicationContext(ApplicationContext context) { applicationContext = context; } /** * 连接建立成功调用的方法 * @param session:websocket连接 * @param roomNo:会话ID */ @OnOpen public void onOpen(Session session,@PathParam("roomNo") String roomNo,@PathParam("workNumber") String workNumber) { if(redisService == null){ redisService = applicationContext.getBean(RedisService.class); } this.session = session; this.roomNo = roomNo; this.workNumber = workNumber; log.info("工号:{}用户加入了房间{}:",workNumber,roomNo); try { Subscriber subscriber = new Subscriber(); subscriber.setRoomNo(roomNo); subscriber.setWorkNumber(workNumber); redisService.subscribe(subscriber, roomNo); //移除就的session,保存新的session for (WebSocketServer item : webSocketSet) { if(item.workNumber.equals(workNumber) && item.roomNo.equals(roomNo)){ webSocketSet.remove(item); break; } } webSocketSet.add(this); } catch (Exception e) { e.printStackTrace(); log.error("======【建立连接】error:{}",e.getMessage()); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { webSocketSet.remove(this); //从set中删除 log.info("有一连接关闭:"+this.roomNo); } /** * 收到客户端消息后调用的方法 * @param message 客户端发送过来的消息 * */ @OnMessage public void onMessage(String message,Session session) { log.info("收到来自窗口"+roomNo+"的信息:"+message); //群发消息 for (WebSocketServer item : webSocketSet) { try { if(session != null && session.getId().equals(item.session.getId())){ item.sendMessage(message,item.session); } } catch (IOException e) { e.printStackTrace(); } } } /** * 异常处理 * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("发生错误:{}",error.getMessage()); error.printStackTrace(); } /** * 实现服务器主动推送 * @param message:消息内容 * @throws IOException */ public void sendMessage(String message,Session session) throws IOException { log.info("服务器推送的socket消息是:{},Session信息是:{}",message,session); if(session != null){ session.getBasicRemote().sendText(message); }else{ this.session.getBasicRemote().sendText(message); } } /** * 根据房间号移除集合中的session * @param roomNo */ public static void removeByRoomNo(String roomNo){ if(StringUtils.isNotBlank(roomNo)){ for (WebSocketServer item : webSocketSet) { if(item.roomNo.equals(roomNo)){ webSocketSet.remove(item); } } } } /** * 群发自定义消息 * @param message:消息内容 * @param roomNo:特定会话 * @throws IOException */ public static void sendInfo(String message,String roomNo,String workNumber) throws IOException { log.info("推送消息到窗口"+roomNo+",推送内容:"+message+",推送给用户工号:"+workNumber); for (WebSocketServer item : webSocketSet) { try { log.info("Set<WebSocketServer>消息是:{}",item); if(item.workNumber.equals(workNumber) && item.roomNo.equals(roomNo)){ item.sendMessage(message,item.session); } } catch (IOException e) { continue; } } } }
import com.alibaba.fastjson.JSON; import com.company.project.manage.dto.MessageDto; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.JedisPubSub; import java.io.IOException; public class Subscriber extends JedisPubSub { private static Logger logger = LoggerFactory.getLogger(Subscriber.class); private String roomNo;//房间号 private String workNumber;//工号 @Override public void onMessage(String channel, String message) { //收到消息会调用 try { WebSocketServer.sendInfo(message,channel,null); MessageDto messageDto = JSON.parseObject(message, MessageDto.class); if(messageDto != null && StringUtils.isNotBlank(messageDto.getRoomNo())){ if("4".equals(messageDto.getType()) || "7".equals(messageDto.getType())){ this.unsubscribe(messageDto.getRoomNo());//取消订阅 } } //如果是结束则退出 } catch (IOException e) { logger.error("=========redis推送房间号为:{}的消息内容是:{},失败原因:{}",channel,message,e.getMessage()); } } @Override public void onSubscribe(String channel, int subscribedChannels) { //订阅了频道会调用 logger.info("===={} 成功订阅了频道:{}",workNumber,channel); } @Override public void onUnsubscribe(String channel, int subscribedChannels) { //取消订阅 会调用 logger.info("===={} 成功订阅了频道:{}",workNumber,channel); } @Override public void unsubscribe(String... channels){//取消订阅 super.unsubscribe(channels); } public String getRoomNo() { return roomNo; } public void setRoomNo(String roomNo) { this.roomNo = roomNo; } public String getWorkNumber() { return workNumber; } public void setWorkNumber(String workNumber) { this.workNumber = workNumber; } }
3.推送消息
redisService.publish(channel,message);
4.websocke经测试发现,如果客户端和服务端一段时间没有消息来往就会断掉,所以客户端需要定时发送一个心跳消息到客户端