两个客户端如何通过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等等技术,但是无论如何两个客户端中间都是得有一个服务端才能完成通信的。

相关推荐
北城以北888818 分钟前
Spring定时任务与Spring MVC拦截器
spring boot·spring·mvc
Victor35626 分钟前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易26 分钟前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧33 分钟前
Range循环和切片
前端·后端·学习·golang
WizLC36 分钟前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Mr.朱鹏38 分钟前
SQL深度分页问题案例实战
java·数据库·spring boot·sql·spring·spring cloud·kafka
Victor35642 分钟前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法1 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长1 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈2 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端