webSocket与redis结合,和客户端交互及统计在线人数的实现

前提

实现对客户端的在线统计,及与客户端的交互和接受redis的消息

设置spring上下文

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

/**
 * <b><code>ApplicationContextRegister</code></b>
 * <p>
 * class_comment
 * </p>
 * <b>Create Time:</b> 2019/12/30 16:40
 *
 * @author ong
 * @version 0.0.1
 * @since core-be 0.0.1
 */

@Component
@Lazy(false)//不延时代表查询出对象A的时候,会把B对象也查询出来放到A对象的引用中,A对象中的B对象是有值的。
public class ApplicationContextRegister implements ApplicationContextAware {
    private static ApplicationContext APPLICATION_CONTEXT;

    /**
     * 设置spring上下文  *  * @param applicationContext spring上下文  * @throws BeansException  * author:huochengyan https://blog.csdn.net/u010919083
     */

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        APPLICATION_CONTEXT = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return APPLICATION_CONTEXT;
    }
}

WebSocketConfig 类

首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理

 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * <b><code>WebSocketConfig</code></b>
 * <p>
 * class_comment
 * </p>
 * <b>Create Time:</b> 2019/12/9 15:58
 *
 */

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

如上图所示,就知道,为什么在组件类加@ServerEndpoint的注解了!

3、WebSocket与redis结合

使用@ServerEndpoint创立websocket endpoint

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSONObject;
import com.a.b.c.commons.util.ApplicationContextRegister;
import com.a.b.c.commons.util.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

/**
 * <b><code>GrendWebSocket</code></b>
 * <p>
 * class_comment
 * </p>
 * <b>Create Time:</b> 2019/12/9 16:18
 *
 * @author ong
 * @version 0.0.1
 * @since core-be 0.0.1
 */
@ServerEndpoint(value = "/trend/curve")
@Component
public class GrendWebSocket {

    /**
     * The constant LOG.
     */
    private static Logger LOG = LoggerFactory.getLogger(GrendWebSocket.class);


    private RedisUtil redisUtil;

    public static GrendWebSocket GrendWebSocket;

    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<GrendWebSocket> webSocketSet = new CopyOnWriteArraySet<GrendWebSocket>();

    private static CopyOnWriteArraySet<String> queryTypes= new CopyOnWriteArraySet<String>();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //某个客户端连接请求的数据类型
    private String queryType;

    //是否开始接受数据
    private String status;

    /**
     * 连接建立成功调用的方法
     * */
    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        this.queryType = session.getQueryString(); //
        this.status = "wait";
        queryTypes.add(this.queryType);
        LOG.info("有新连接加入!当前在线人数为" + getOnlineCount());
        try {
            sendMessage("连接成功");
            LOG.info("请求数据类型为:${}",session.getQueryString());
        } catch (IOException e) {
            LOG.error("websocket IO异常");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除
        //queryTypes.remove(this.queryType);
        subOnlineCount();           //在线数减1
        LOG.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }
/**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        LOG.error("发生错误");
        error.printStackTrace();
    }


    public void sendMessage(String message) throws IOException {
        
    }


    /**
     * 群发自定义消息
     * */
    public static void sendInfo(String queryType,String message) throws IOException {
        
    }


    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized Set getQueryTypes() {
        return queryTypes;
    }

    public static synchronized void addOnlineCount() {
        GrendWebSocket.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        GrendWebSocket.onlineCount--;
    }
} 

 收到客户端消息后调用的方法

 /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        LOG.info("来自客户端的消息:" + message);
        //群发消息
        if(message != null && "request_history".equals(message) ){
            if(redisUtil == null){
                ApplicationContext act = ApplicationContextRegister.getApplicationContext();
                redisUtil= act.getBean(RedisUtil.class);
            }
            List<Object> historyData;
            if(this.queryType.contains("all_")){
                historyData = redisUtil.lRange(this.queryType,-2700,-1);
            }else {
                historyData = redisUtil.lRange(this.queryType,-900,-1);
            }
            List coverData = new ArrayList();
            for(Object record : historyData){
                try{
                    coverData.add(JSONObject.parse(record.toString()));
                }catch (Exception e){}
            }
            this.sendMessage(JSONObject.toJSONString(coverData));
            this.status = "request";
        }else if("request".equals(message)){
            this.status="request";
        }
    }

通过redis的lrange获取存储的数据并输出,如:
        List<String> list = jedis.lrange("site-list", 0 ,2);

使用getBasicRemote()同步发送消息

public void sendMessage(String message) throws IOException {
        synchronized (session) {
            this.session.getBasicRemote().sendText(message);
        }
    }

 getAsyncRemote()getBasicRemote()确实是异步与同步的区别,大部分情况下,推荐使用getAsyncRemote()。由于getBasicRemote()的同步特性,并且它支持部分消息的发送即sendText(xxx,boolean isLast). isLast的值表示是否一次发送消息中的部分消息,对于如下情况:

  • session.getBasicRemote().sendText(message, false); 
  • session.getBasicRemote().sendBinary(data);
  • session.getBasicRemote().sendText(message, true);             

由于同步特性,第二行的消息必须等待第一行的发送完成才能进行,而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。如果要使用getBasicRemote()同步发送消息,则避免尽量一次发送全部消息,使用部分消息来发送。

群发自定义消息

/**
     * 群发自定义消息
     * */
    public static void sendInfo(String queryType,String message) throws IOException {
        for (GrendWebSocket item : webSocketSet) {
            if(item.queryType.equals(queryType) && "request".equals(item.status)){
                try {
                    item.sendMessage(message);
                } catch (IOException e) {
                    LOG.error("消息数据发送失败 websocker to brower!");
                    continue;
                }
            }
        }
    }

  特别指出:

 //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet<GemTrendWebSocket> webSocketSet = new CopyOnWriteArraySet<GemTrendWebSocket>();

在普通的同步机制中,是通过对象加锁实现多个线程对同一变量的安全访问的,该变量是多个线程共享的系统并没有将这份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。    
 但是:   

ThreadLocal类

ThreadLocal类,意思是线程局部变量。作用是为每一个使用该变量的线程都提供一个该变量的副本使每一个线程都能独立操作这个副本而不会与其他线程的副本冲突。

故ThreadLocal将需要并发访问的资源复制多份,每个线程拥有自己的资源副本,从而也就没有必要对该变量进行同步了。

Concurrent开头的集合类

线程安全的类,以Concurrent开头的集合类,都在java.util.concurrent包下,这种集合类采用更复杂的算法来保证永远不会锁住整个集合(并发写入时加锁,读取时不加锁),因此在并发写入时有较好的性能。最常用的是ConcurrentHashMap

ConcurrentHashMap在默认情况下最多支持16个线程并发写入,如果没有设置,则超过16个线程并发向该Map中写入数据时,可能会有一些线程需要等待,可以在创建ConcurrentHashMap实例时调用某个带参构造器显式指定

Concurrent包的集合类CopyOnWrite*类

来自Concurrent包的集合类CopyOnWriteArraySet的特点:

  • CopyOnWriteArraySet继承于AbstractSet,这就意味着它是一个集合。
  • 因为CopyOnWriteArraySet是所有操作都使用内部CopyOnWriteArrayList的Set集合,所以CopyOnWriteArraySet相当于动态数组实现的“集合”,它里面的元素不能重复。
  • CopyOnWriteArraySet的“线程安全”机制是通过volatile和互斥锁来实现的

所有操作使用内部CopyOnWriteArrayList的java.util.Set。 因此,它具有相同的基本属性:

  • 它最适合应用程序的集合大小通常很小,只读操作远远超过可变操作,并且需要防止在遍历期间线程之间的干扰。
  • 线程安全。
  • 突变操作(添加,设置,删除等)是昂贵的,因为它们通常需要复制整个底层阵列。
  • 迭代器不支持可变删除操作。
  • 迭代器遍历速度快,不会受到来自其他线程的干扰。 迭代器构建时迭代器依赖于数组的不变快照。

友情链接:https://www.cnblogs.com/bianzy/p/5822426.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章