两个客户端如何通过websocket通信

前言

博主在工作中碰到一个需求,现有安卓app1(uniapp编写)和安卓app2(帆软10.0编写),想要实现这样的效果:帆软推送一个消息给uniapp,后者监听到消息来了,然后执行一定的处理逻辑。

分析

首先分析技术栈,帆软和uniapp都是客户端,无法直接通信。

那么就可以考虑在中间架一个服务端,参考预警系统使用到的websocket,帆软和uniapp都连接到这台websokect服务器,当前者向服务器推送消息时,后者就可以感知到并执行消息处理。流程图如下:

问题

然而,理想很丰满,现实很骨感。我们不幸的得知帆软无法与websocket服务器建立连接,只能发送http请求。

那么,可不可以让帆软通过http协议调用websocket服务器,然后websocket服务器主动给uniapp推送消息呢?答案是可行的。

因为websocket与http协议最大的区别就是它可以实现双端通信,客户端可以推送消息给服务端,服务端也可以推送消息给客户端。

流程图修正为:

代码

首先我们需要搭建一个websocket服务器,比较简单的方式就是在SpringBoot后端直接搭建一个。你只需要参考如下流程:

  1. pom.xml引入starter
xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 创建websocket配置类
typescript 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;

/**
 * websocket相关配置
 */
@Configuration
public class WebSocketConfig {
    /**
     * 自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 通信文本消息和二进制缓存区大小
     * 避免对接 第三方 报文过大时,Websocket 1009 错误
     */

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // 在此处设置bufferSize
        container.setMaxTextMessageBufferSize(10240000);
        container.setMaxBinaryMessageBufferSize(10240000);
        container.setMaxSessionIdleTimeout(15 * 60000L);
        return container;
    }
}
  1. 创建websocket服务器
typescript 复制代码
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
@ServerEndpoint("/api/pushMessage/{userId}")
public class WebSocketServer {
    /**
     * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
     */
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;
    /**
     * 接收userId
     */
    private String userId = "";

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            //加入set中
            webSocketMap.put(userId, this);
        } else {
            //加入set中
            webSocketMap.put(userId, this);
            //在线数加1
            addOnlineCount();
        }
        log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
        sendMessage("连接成功");
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId);
            //从set中删除
            subOnlineCount();
        }
        log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
    }

    /**
     * 收到客户端消
     * 息后调用的方法
     * @param message 客户端发送过来的消息
     **/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用户消息:" + userId + ",报文:" + message);
    }


    /**
     * 错误处理
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发送自定义消息
     **/
    public static void sendInfo(String message, String userId) {
        log.info("发送消息到:" + userId + ",报文:" + message);
        if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
            webSocketMap.get(userId).sendMessage(message);
        } else {
            log.error("用户" + userId + ",不在线!");
        }
    }

    /**
     * 发送自定义消息给所有用户
     * @return 是否发送成功
     */
    public static Boolean sendInfoToAll(String message) {
        log.info("发送消息给所有用户,消息内容:{}", message);
        if (webSocketMap.isEmpty()) {
            return false;
        }
        for (String userId : webSocketMap.keySet()) {
            webSocketMap.get(userId).sendMessage(message);
            log.info("发送消息{}给用户{}成功", message, userId);
        }
        return true;
    }

    /**
     * 获得此时的在线人数
     */
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    /**
     * 在线人数加1
     */
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    /**
     * 在线人数减1
     */
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}
  1. 重启项目,此时websocket服务器就已经启动了

uniapp

根据前文可知,安卓app2是一个uniapp项目,参考相关文档:

zh.uniapp.dcloud.io/api/request...

我们可以编写出如下实例代码,用于简单地建立websocket连接和接收消息:

javascript 复制代码
const host = "127.0.0.1:8080";
const websocketUrl = "ws://" + host + "/api/pushMessage/xiaoming";
uni.connectSocket({
  url: websocketUrl,
  complete: ()=> {
    console.log('连接到websocket服务器成功:' + websocketUrl)

    uni.onSocketOpen(function (res) {
      console.log("消息来了:" + JSON.stringify(res))
    });

    uni.onSocketClose(function (res) {
      console.log('WebSocket 已关闭!');
    });

    uni.onSocketMessage(function (res) {
      console.log('收到服务器内容:' + res.data);
    });

    setTimeout(() => {
      uni.sendSocketMessage({
        data: 'uniapp发送的消息'
      })
    }, 1000)
  }
});

帆软

因帆软无法直接和websocket服务器建立连接,所有我们使用http协议调用服务器,让服务器自己去推送消息,后端控制器示例如下:

less 复制代码
@PostMapping("test")
@ResponseBody
public Map<String, Object> test(@RequestBody Map<String, Object> params) {
    Boolean result = WebSocketServer.sendInfoToAll((String) params.get("test"));
    Map<String, Object> map = new HashMap<>();
    if (result) {
        map.put("status", "ok");
    } else {
        map.put("status", "error");
        map.put("errMsg", "安卓客户端没有连接到websocket");
    }
    return map;
}

心跳机制

根据前文所述,uniapp需要与websocket长期建立连接,如果在一定时间内没有消息的收发,那么服务就会下线。这显然不是我们想看到的。

我们可以参考心跳机制,每隔5秒,让uniapp向websocket推送一个心跳包,这样就可以保持长连接了,代码示例如下:

xml 复制代码
<script>
  export default {
    data() {
      return {
        socketTask: null,
        heartbeatInterval: null,
        reconnectInterval: null
      };
    },
    onLaunch: function () {
      console.log('App Launch')
      this.connectWebSocket()-;
    },
    onUnload() {
      if (this.socketTask) {
        this.socketTask.close();
      }
      this.stopHeartbeat();
      this.stopReconnect();
    },
    methods: {
      connectWebSocket() {
        const websocketUrl = 'ws://127.0.0.1:8082/api/pushMessage/xiaoming' 
        console.log('开始连接到websocket服务器:' + websocketUrl)
        this.socketTask = uni.connectSocket({
          url: websocketUrl,
          complete: ()=> {
            console.log('连接到websocket服务器成功:' + websocketUrl)

            uni.onSocketOpen(function (res) {
              console.log("消息来了:" + JSON.stringify(res))
            });

            uni.onSocketClose(function (res) {
              console.log('WebSocket 已关闭!自动重连...');
              this.stopHeartbeat();
              this.startReconnect();
            });

            uni.onSocketMessage(function (res) {
              console.log('收到服务器内容:' + res.data);
            });

            uni.onSocketError(function (err) {
              console.error('WebSocket 发生错误:', JSON.stringify(err));
              this.stopHeartbeat();
              this.startReconnect();
            })

            // 开启心跳机制
            this.startHeartbeat();

            setTimeout(() => {
              uni.sendSocketMessage({
                data: 'uniapp发送的消息'
              })
            }, 1000)
          },
          fail: (err) => {
            console.log('连接到websocket服务器失败:' + websocketUrl)
            // 尝试重连
            this.startReconnect();
          }
        });

      },
      startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
          if (this.socketTask) {
            this.socketTask.send({
              data: 'ping', // 心跳包数据
              success: () => {
                console.log('心跳包发送成功');
              },
              fail: (err) => {
                console.error('心跳包发送失败:', err);
              }
            });
          }
        }, 5000); // 每 5 秒发送一次心跳包
      },
      stopHeartbeat() {
        if (this.heartbeatInterval) {
          clearInterval(this.heartbeatInterval);
          this.heartbeatInterval = null;
        }
      },
      startReconnect() {
        if (!this.reconnectInterval) {
          this.reconnectInterval = setInterval(() => {
            console.log('尝试重新连接 WebSocket...');
            this.connectWebSocket();
          }, 5000); // 每 5 秒尝试重新连接一次
        }
      },
      stopReconnect() {
        if (this.reconnectInterval) {
          clearInterval(this.reconnectInterval);
          this.reconnectInterval = null;
        }
      }
    }
  }
</script>

总结

通过上述步骤,我们就解决了两个客户端通信的问题。当然,除了websocket,你也可以使用java.net.ServerSocketNettySocketRabbitMQ等等技术,但是无论如何两个客户端中间都是得有一个服务端才能完成通信的。

相关推荐
ss2733 分钟前
基于Springboot + vue实现的中医院问诊系统
java·spring boot·后端
左灯右行的爱情1 小时前
Redis 缓存并发问题深度解析:击穿、雪崩与穿透防治指南
java·数据库·redis·后端·缓存
南玖yy1 小时前
C++ 成员变量缺省值:引用、const 与自定义类型的初始化规则详解,引用类型和const类型的成员变量自定义类型成员是否可以用缺省值?
c语言·开发语言·c++·后端·架构·c++基础语法
不爱总结的麦穗2 小时前
面试常问!Spring七种事务传播行为一文通关
后端·spring·面试
小虚竹2 小时前
claude 3.7,极为均衡的“全能型战士”大模型,国内直接使用
开发语言·后端·claude·claude3.7
苹果酱05672 小时前
python3语言基础语法整理
java·vue.js·spring boot·mysql·课程设计
bcbnb2 小时前
iOS 性能调优实战:三款工具横向对比实测(含 Instruments、KeyMob、Xlog)
后端
极客智谷2 小时前
Spring AI应用系列——基于ARK实现多模态模型应用
人工智能·后端
radient2 小时前
Java/Go双修 - Go并发Goroutine与Java对比
java·后端·go