websocket實現客戶端跟服務端的雙向傳輸,解決客戶端向服務端輪訓請求。應用到推送GPS位置信息,彈幕,聊天信息等場景。
一、Java服務端實現
maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
核心服務代碼
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
主要是OnOpen OnClose OnMessage OnError方法,裏面再買上業務處理;
@Slf4j
@ServerEndpoint(value = "/websocket/user/{accessToken}")
@Component
@EnableScheduling
public class WebSocketUserServer {
//靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
private static AtomicInteger onlineCount = new AtomicInteger(0);
//concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。
private static CopyOnWriteArrayList<UserSocket> userSockets = new CopyOnWriteArrayList<>();
//與某個客戶端的連接會話,需要通過它來給客戶端發送數據
private Session session;
/*
連接打開時執行
*/
@OnOpen
public void onOpen(@PathParam("accessToken") String accessToken , Session session) {
open:{
addOnlineCount(); //在線數加1
this.session = session;
log.info("WebSocketUserServer 有新連接加入!當前在線人數爲" + getOnlineCount());
if(accessToken ==null || "".equals(accessToken)){
log.info("WebSocketUserServer onOpen accessToken is null");
closeSession(session);
break open;
}
//校驗accessToken,獲取user信息
//獲取service方法
UcTokenFeign ucTokenFeign = SpringUtil.getBean(UcTokenFeign.class) ;
ObjectRestResponse<UcTokenWebsocketVo> userResult = ucTokenFeign.getUserInfo(accessToken) ;
if(userResult.getStatus() != CodeStatus.CODE_SUCCESS.getValue()
||userResult.getData() == null){
log.info("WebSocketUserServer onOpen token feign not success:"+userResult.getMsg());
closeSession(session);
break open;
}
//添加到用戶Session對應關係中
UserSocket userSocket = new UserSocket();
userSocket.setUserId(userResult.getData().getUserId())
userSocket.setWebSocketUserServer(this);
userSockets.add(userSocket) ;
// try {
// sendMessage("server連接成功");
// } catch (IOException e) {
// log.error("websocket IO異常");
// }
log.info("WebSocketUserServer Connected ... " + session.getId());
}
}
/*
服務端不接收非合規的client,進行關閉操作
*/
private void closeSession(Session session){
try {
session.close();
} catch (IOException e) {
log.error("WebSocketUserServer close error:"+e);
e.printStackTrace();
}
}
/**
* 連接關閉調用的方法
*/
@OnClose
public void onClose() {
subOnlineCount(); //在線數減1
log.info("WebSocketUserServer 有一連接關閉!當前在線人數爲" + getOnlineCount());
userSockets.stream()
.forEach(u ->{
if(u.getWebSocketUserServer() == this){
userSockets.remove(u) ;
log.info("WebSocketUserServer userSockets remove user socket,user:"+u.getUserId());
}
} );
log.info("WebSocketUserServer 刪除關閉連接的對應關係");
}
/**
* 收到客戶端消息後調用的方法
*
* @param message 客戶端發送過來的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("WebSocketUserServer 來自客戶端的消息:" + message);
// //羣發消息
// for (WebSocketUserServer item : webSocketSet) {
// try {
// item.sendMessage(message);
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocketUserServer 發生錯誤:"+error);
error.printStackTrace();
}
/*
給客戶端發送文本信息
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
log.info("WebSocketUserServer sendMessage:"+message);
}
/*
發送用戶狀態信息
*/
public void sendUserPushMsgInfo(String status) throws IOException{
this.session.getBasicRemote().sendText(status);
log.info("WebSocketUserServer sendUserPushMsgInfo:"+status);
}
/*
根據userID給客戶端推送用戶狀態
*/
public static void PushMsgInfoToUser(String userId,String os
,String businessType,String status) throws IOException {
if(status !=null && !"".equals(status)){
userSockets.stream()
.forEach(u->{
if(u.getUserId().equals(userId)){
try {
u.getWebSocketUserServer().sendUserPushMsgInfo(status);
log.info("WebSocketUserServer PushMsgInfoToUser success,userId:"+userId);
} catch (IOException e) {
e.printStackTrace();
log.error("WebSocketUserServer PushMsgInfoToUser error,userId:"+userId);
}
}
});
}
}
/*
定時檢查存活的Session,如果未存活進行處理
*/
@Scheduled(cron = "0 0/2 * * * ?")
public static void checkAliveSession(){
log.info("WebSocketUserServer checkAliveSession start:"+new Date());
userSockets.stream()
.forEach(u->{
if(!u.getWebSocketUserServer().session.isOpen()){
userSockets.remove(u) ;
log.info("WebSocketUserServer checkAlive remove not open session,userId:"+u.getUserId());
}
});
log.info("WebSocketUserServer checkAliveSession end:"+new Date());
}
public static synchronized int getOnlineCount() {
return onlineCount.get();
}
public static synchronized void addOnlineCount() {
WebSocketUserServer.onlineCount.getAndIncrement();
}
public static synchronized void subOnlineCount() {
WebSocketUserServer.onlineCount.getAndDecrement();
}
}
二、websocket服務中引入service服務
採用輔助工具類SpringUtil獲取Bean,
UcTokenFeign ucTokenFeign = SpringUtil.getBean(UcTokenFeign.class) ;
@Component
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
//獲取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通過name獲取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通過class獲取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通過name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
三、websocket的分佈式實現
1.session 放到redis中,實現數據共享,但是websocket session不支持序列號,存儲不了
2.加入消息中間件,實現收到消息後的共享
consumer有兩種消費模式:集羣消費和廣播消費。集羣消費:多個consumer平均消費該topic下所有mq的消息,即某個消息在某個message queue中被一個consumer消費後,其他消費者就不會消費到它;廣播消費:所有consumer可以消費到發到這個topic下的所有消息。
因爲Session不支持序列化,nginx分發不能保證一定指定到同一臺服務器,特別移動互聯網,移動設備下。
故採用消息訂閱模式進行實現。每臺服務器都訂閱相同主題的消息,接收到消息後,關聯到session則進行推送消息。
CopyOnWriteArrayList<UserSocket> userSockets = new CopyOnWriteArrayList<>() 這裏便是維護關係點
四、客戶端調用的html測試代碼
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://127.0.0.1:8877/websocket/user1/qw");
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "連接開啓!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "連接被關閉";
};
} else {
alert("你的瀏覽器不支持 WebSocket!");
}
function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message+ new Date());
} else {
alert("連接沒有開啓.");
}
}
function userclose(){
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.close() ;
} else {
alert("連接沒有開啓.");
}
}
</script>
<form onsubmit="return false;">
<h3>WebSocket 聊天室1:</h3>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
<br>
<input type="button" onclick="send('first send message')" value="btnSend">
<input type="button" onclick="userclose()" value="btnClose">
</form>
</body>
</html>
五、nginx配置
upstream websocket {
server 1.203.115.27:8877;
server 127.0.0.1:8877;
}
server {
listen 8888;
location / {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
六、zuul負載websocket
spring cloud zuul 1.x版本臨時不直接支持spring websocket,到2.×版本會支持。
1.x版本也可以支持websocket,配置比較麻煩,需要結合sock.js stomp等。詳細可以參考該網站:https://stackoverflow.com/questions/39472635/zuul-with-web-socket
七、斷網、弱網、切換網等場景,實際使用的問題處理
待補充
八、websocket發送消息支持的長度大小
下面代碼循環執行50000次,前端還可以接收到,不過已經需要好幾秒的時間,這個時間延遲已經不低。
10W次也可以接收到,已經延遲到幾分鐘的程度。
循環次數 length
10000 33400
20000 66800
50000 16700011
100000 33400011
StringBuffer msg =new StringBuffer() ;
// {"centerLat":"27.403234","centerLon":"117.504426","maxLat":"31.849878","maxLon":"121.434785","minLat":"22.956590","minLon":"113.574066","locations":[{"lon":"121.434785","lat":"31.849878","vehicleNo":"京AA8866","transportStatus":"0"},{"lon":"113.574066","lat":"22.956590","vehicleNo":"京ETYUUII","transportStatus":"0"}]}
for(int i=0;i<100000;i++){
msg.append( " {\"centerLat\":\"27.403234\",\"centerLon\":\"117.504426\",\"maxLat\":\"31.849878\"" +
",\"maxLon\":\"121.434785\",\"minLat\":\"22.956590\",\"minLon\":\"113.574066\"" +
",\"locations\":[{\"lon\":\"121.434785\",\"lat\":\"31.849878\",\"vehicleNo\":\"京AA8866\",\"transportStatus\":\"0\"}" +
",{\"lon\":\"113.574066\",\"lat\":\"22.956590\",\"vehicleNo\":\"京ETYUUII\",\"transportStatus\":\"0\"}]}\n"
);
}
sendMessage("server連接成功;"+msg.toString());