Socket 常用來做前後端的信息通信,但是 Java 端的 Socket server 只負責發送,並不保證這條消息一定能被客戶端接收到(也許有準確送達的方式但是我目前還不知道)。Socket 的這種機制自然有其優勢所在,但是有時候我們需要保證發出的消息被準確送達。
本文思路:後端啓定時器不斷髮送消息,直到收到前端反饋;對每一條消息用 uuid 標識,避免被前端重複響應。
一、Java 端的 Socket server
package com.ysu.gdp.web;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.annotation.Resource;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
@ServerEndpoint("/websocket/{user_id}")
@Component
public class WebSocketServer {
private static int onlineCount = 0;
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
private Session session;
private String user_id="";// 賬號id
@OnOpen
public void onOpen(Session session,@PathParam("user_id") String user_id) {
this.session = session;
webSocketSet.add(this);
addOnlineCount();
this.user_id=user_id;
System.out.println("user_id="+user_id+"的用戶和服務器建立了連接");
}
@OnClose
public void onClose() {
webSocketSet.remove(this);
subOnlineCount();
System.out.println("user_id="+user_id+"的用戶和服務器斷開了連接");
}
@OnMessage
public void onMessage(String message, Session session) {
//log.info("收到來自窗口"+sid+"的信息:"+message);
boolean ijo=isJsonObject(message);//避免message不可轉爲json時,後臺打印大量不必要的異常日誌
if(ijo) {
JSONObject message2 = JSONObject.parseObject(message);
if(message2!=null) {
Object uuid=message2.get("uuid");
if(uuid!=null) {
System.out.println("收到uuid爲:"+uuid.toString()+" 的消息反饋,即將終止發送");
Thread t=findThread(uuid.toString());
if(t!=null) {
t.interrupt();
System.out.println("uuid爲:"+uuid.toString()+" 的消息終止發送成功");
}else{
System.out.println("未找到uuid爲:"+uuid.toString()+" 的消息發送線程");
}
}else{
System.out.println("反饋數據中不含uuid");
}
}
}
}
/**
* 判斷字符串是否可以轉化爲json對象
* @param content
* @return
*/
public static boolean isJsonObject(String content) {
// 此處應該注意,不要使用StringUtils.isEmpty(),因爲當content爲" "空格字符串時,JSONObject.parseObject可以解析成功,
// 實際上,這是沒有什麼意義的。所以content應該是非空白字符串且不爲空,判斷是否是JSON數組也是相同的情況。
if(StringUtils.isBlank(content))
return false;
try {
JSONObject jsonStr = JSONObject.parseObject(content);
return true;
} catch (Exception e) {
return false;
}
}
@OnError
public void onError(Session session, Throwable error) {
//log.error("發生錯誤");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
//this.session.getBasicRemote().sendText(message);
String uuid = UUID.randomUUID().toString().replaceAll("-","");
Thread myThread = new Thread(new Runnable() {
public void run() {
int i=1;
try {
JSONObject message2 = JSONObject.parseObject(message);
message2.put("uuid", uuid);
while (!Thread.currentThread().isInterrupted()) {
System.out.println("正在發送uuid爲:"+uuid+" 的消息,次數:第"+i+"次");
i++;
session.getBasicRemote().sendText(JSONObject.toJSONString(message2));
Thread.sleep(3000);
if(i>100) {
System.out.println("用戶:"+user_id+"持續5分鐘未發送反饋,終止本次消息傳遞");
break;
}
}
} catch (Exception e) {//interrupt一個線程時sleep會拋異常,都打印的話日誌太多了
//e.printStackTrace();
}
}
}, uuid);
myThread.start();
}
/**
* 通過線程組獲得線程
*
* @param threadName
* @return
*/
public static Thread findThread(String threadName) {
ThreadGroup group = Thread.currentThread().getThreadGroup();
while(group != null) {
Thread[] threads = new Thread[(int)(group.activeCount() * 1.2)];
int count = group.enumerate(threads, true);
for(int i = 0; i < count; i++) {
if(threads[i].getName().equals(threadName)) {
return threads[i];
}
}
group = group.getParent();
}
return null;
}
public static void sendInfo(Object ob,String message,@PathParam("user_id") String user_id) throws IOException {
//log.info("推送消息到窗口"+sid+",推送內容:"+message);
for (WebSocketServer item : webSocketSet) {
try {
//這裏可以設定只推送給這個sid的,爲null則全部推送
if(user_id==null) {
if(message!=null) {
synchronized (item) {
item.sendMessage(message);
}
}
if(ob!=null) {
synchronized (item) {
item.sendMessage(JSONObject.toJSONString(ob));
}
}
}else if(item.user_id.equals(user_id)){
if(message!=null) {
synchronized (item) {
item.sendMessage(message);
}
}
if(ob!=null) {
synchronized (item) {
item.sendMessage(JSONObject.toJSONString(ob));
}
}
}
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
Java 後端主要是:
public void sendMessage(String message) 函數發送消息時,向原待發送消息中添加一個隨機的 uuid 進行標識,並啓動一個線程,Thread myThread = new Thread(new Runnable() {...}, uuid); 該線程被命名爲 uuid 對應的值。
線程內部,每隔 3 秒重複發送一次消息,超過 5 分鐘仍沒收到前端反饋時終止發送(此時我們有理由認爲前端出問題了,下線了、斷線了等等),或者收到前端反饋時,終止本次發送。
收到前端反饋時將觸發 public void onMessage(String message, Session session) 函數。在這裏我們將 name 爲 uuid 的線程中斷即可。public static Thread findThread(String threadName) 函數用於在線程組中查找指定 name 的線程。
二、前端的 Socket Client
$(function(){
let uuid_list=new Array();
layui.use(['layer'],function(){
let layer = layui.layer;
let websocket = null;
if('WebSocket' in window){
websocket = new ReconnectingWebSocket("ws://localhost:8080/websocket/"+getCookie('user_id'));
}
else{
layer.msg('本機暫不支持websocket')
}
websocket.onerror = function(){
layer.msg("與服務器的socket連接發生錯誤,請檢查");
};
websocket.onopen = function(event){
// layer.msg("open");
}
websocket.onmessage = function(event){
console.log(JSON.parse(event.data));
let uuid=JSON.parse(event.data).uuid;
let index=uuid_list.indexOf(uuid);
if(index==-1){
let send_info={}
send_info.uuid=uuid
websocket.send(JSON.stringify(send_info));
uuid_list.push(uuid)
if(uuid_list.length>95){
for(let z=0;z<50;z++){
uuid_list.shift()
}
}
}
}
})
})
前端我用的具有斷線重連功能的 ReconnectingWebSocket;資源鏈接: https://pan.baidu.com/s/1m9o8aTUB4H2DAYUTwLpiiQ 提取碼: tcq6
前端消息處理思路:
開一個大概100個長度的數組(100個已經有足夠的消息緩存時間了),收到後端發送的消息時,先查查 uuid_list 中有沒有 該消息的 uuid,有的話說明這條消息已經處理過了,直接忽略,它可能是在路上堵車來晚了導致後端沒有收到消息反饋時重複發送了其他的消息,但是後發的先到了;沒有的話說明是剛到的新消息,然後趕緊把該 uuid 發送給後端進行消息收到反饋。再操作自己的事,存入uuid_list 中 或者處理自己的事務邏輯(我這裏沒有其他事務邏輯)。當收到的消息超過 95 個了,就把最前面的 50 個消息記錄刪除,確保緩衝池的足夠容量。
以上是關於 Socket 消息準確送達的一種實現方式,JS 裏的消息提示我用的 layui。
關於 Socket 的乒乓保活機制我下次再寫(socket 和後端建立連接後,即使前端有斷線重連,也有可能因爲一些玄學問題導致連接發生異常,這時候需要整個保活機制確保連接一直正常工作)。