微信小程序房间多人PK答题

最近做一款小程序的答题,接到的需求是答题最后一种玩法为房间PK方式,用户创建房间,邀请好友进入房间,准备后开始PK答题,房间最后一人答题完成则到房间结算页。

这里我们用websocket作为长连接来通知房间用户状态变化,由于生产环境服务器有4台且用nginx做了负载均衡,使用的是轮询策略,所以需要考虑服务器之间的通讯,决定用redis的发布订阅来做消息推送,处理服务器之间的通讯。注意的是:当客户端连上websocket时,此时redis客户端向redis服务器订阅该房间的频道。

1.springboot集成websocket


     javax.websocket
     javax.websocket-api
     1.1
     provided
 
 
     javax
     javaee-api
     8.0
 

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 webSocketSet = new CopyOnWriteArraySet();
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    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消息是:{}",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经测试发现,如果客户端和服务端一段时间没有消息来往就会断掉,所以客户端需要定时发送一个心跳消息到客户端

 

 

你可能感兴趣的:(springboot,webscoket,java)