前提
實現對客戶端的在線統計,及與客戶端的交互和接受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。 因此,它具有相同的基本屬性:
- 它最適合應用程序的集合大小通常很小,只讀操作遠遠超過可變操作,並且需要防止在遍歷期間線程之間的干擾。
- 線程安全。
- 突變操作(添加,設置,刪除等)是昂貴的,因爲它們通常需要複製整個底層陣列。
- 迭代器不支持可變刪除操作。
- 迭代器遍歷速度快,不會受到來自其他線程的干擾。 迭代器構建時迭代器依賴於數組的不變快照。