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 和后端建立连接后,即使前端有断线重连,也有可能因为一些玄学问题导致连接发生异常,这时候需要整个保活机制确保连接一直正常工作)。