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

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