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

相关推荐
简诚16 分钟前
redis在spring boot中异常退出
spring boot·redis
PWRJOY1 小时前
Flask 路由装饰器:从 URL 到视图函数的优雅映射
后端·python·flask
Uranus^1 小时前
深入解析Spring Boot与Redis的缓存集成实践
java·spring boot·redis·缓存·性能优化
码农爱java2 小时前
Spring Boot 集成 Elasticsearch【实战】
大数据·spring boot·elasticsearch·全文检索·es
伍六星4 小时前
基于JDBC的信息管理系统,那么什么是JDBC呢?
java·数据库·后端·jdbc·数据库连接
声声codeGrandMaster7 小时前
Django之验证码功能
数据库·后端·python·django
sg_knight7 小时前
Docker网络全景解析:Overlay与Macvlan深度实践,直通Service Mesh集成核心
java·网络·spring boot·spring cloud·docker·容器·service_mesh
鬣主任8 小时前
JavaSenderMail发送邮件(QQ及OFFICE365)
java·spring boot·smtp/imap/tls
zwjapple9 小时前
RabbitMQ的基本使用
开发语言·后端·ruby
草明10 小时前
Brave 连接 Websocket 失败
网络·websocket·网络协议