已删除用户
发布于 2023-09-18 / 1 阅读 / 0 评论 / 0 点赞

解决基于MQTT协议通信在Webgl平台不兼容的问题

真机打包Webgl平台

报错

实测发现真机打包PC平台,可以直接调用mqtt基于u3d的类库,进行连接、订阅、发送等操作。

而真机打包Webgl平台,发现连接Mqtt会报错,无法连接,具体报错如下

Exception connecting to the broker , ex:System.Net.Sockets.SocketException (0x80004005): Success
  at System.Net.Sockets.Socket..ctor (System.Net.Sockets.AddressFamily addressFamily, System.Net.Sockets.SocketType socketType, System.Net.Sockets.ProtocolType protocolType) [0x00000] in <00000000000000000000000000000000>:0 
b50e31cb-83d9-46d1-85b8-b4f05b6e2d2b:3:35047
InvalidOperationException: Operation is not valid due to the current state of the object.
  at System.Runtime.InteropServices.SafeHandle.DangerousAddRef (System.Boolean& success) [0x00000] in <00000000000000000000000000000000>:0 

错位位置,mqtt基于u3d的类库中的MqttClient.cs 449 => Connect(xxx)


解决方案

根据查阅资料得知,Unity打包Webgl想要调用Mqtt通信,无法使用上述基于u3d的类库,需要在h5中具体实现mqtt连接、订阅、发送、取消订阅、断开方法,而Unity中通过.jslib文件调用前者h5各个方法

具体流程

在Unity编辑器预先创建xxx.jslib类库,声明mqtt的连接、订阅、发送、取消订阅、断开方法,在各个方法体中调用指定方法,在打包后的h5页面(index.html)中具体实现前者的指定方法(连接、订阅、发送、取消订阅、断开逻辑),h5完成具体逻辑后回调给Unity内部,其中会涉及到Webgl平台Unity与js通信

解决方案参考

Unity3D打包WebGL并使用MQTT(一)_unity3d webgl-CSDN博客

UnityWeb端和Js互调(MQTT通讯篇)unity与web端通信北特的博客-CSDN博客

Unity 导出WebGL 嵌入网页并通信_unity导出webgl-CSDN博客

Webgl平台Unity与js通信

Interaction with browser scripting - Unity 手册

h5(index.html)调用Unity代码

1.在Unity目录下导入依赖包 WebGLSupport

2.打包webgl包后,手动修改index.html文件,使用api,unityInstance.SendMessage('场景对象名', '对象所挂载的脚本的方法名', '方法参数可空');

Unity调用h5代码(index.html)

1.在Unity目录下新建并配置 Assets/Plugins/xxx.jslab文件,在文件中创建方法调用h5中函数

2.在unity脚本中调用xxx.jslab文件中的方法

3.打包webgl包后,手动修改index.html文件,实现/xxx.jslab文件中的方法

具体可参考WebglUseJslibToJs项目

Unity客户端配置

创建 xxx.jslib

xxx.jslib放置在Plugins 文件夹下,Plugins可以不放置在Assets根目录下

mergeInto(LibraryManager.library, {
  //Hello: function () {
  //  window.alert("测试Unity的Webgl平台通过H5调用MQTT通信");
  //},
​
  Jslib_Connect: function (host, port, clientId, username, password, destination) {
    mqttConnect(UTF8ToString(host), UTF8ToString(port), UTF8ToString(clientId), UTF8ToString(username), UTF8ToString(password), UTF8ToString(destination));
  },
​
  Jslib_Subscribe: function (topic) {
    mqttSubscribe(UTF8ToString(topic))
  },
​
  Jslib_Publish: function (topic, payload) {
    mqttSend(UTF8ToString(topic), UTF8ToString(payload))
  },
​
  Jslib_Unsubscribe: function(topic) {
    mqttUnsubscribe(UTF8ToString(topic));
  },
​
  Jslib_Disconnect: function() {
    mqttDisconnect();
  }
});

导入WebGLSupport到Unity工程目录

用于index.html中基于js使用unity API调用unity中的方法

API:unityInstance.SendMessage('场景对象名', '对象所挂载的脚本的方法名', '方法参数可空');

调用xxx.jslib方法

[DllImport("__Internal")]
    private static extern void Jslib_Connect(string host, string port, string clientId, string username, string password, string destination);
    [DllImport("__Internal")]
    private static extern void Jslib_Subscribe(string topic);
    [DllImport("__Internal")]
    private static extern void Jslib_Publish(string topic, string payload);
    [DllImport("__Internal")]
    private static extern void Jslib_Unsubscribe(string topic);
    [DllImport("__Internal")]
    private static extern void Jslib_Disconnect();
​
    private MqttRecvMsgCallback m_RecvMsgCallback;
​
    public void Connect(string clientIP, int clientPort, string clientId, string username, string password, string destination = "Unity_Test_Destination")
    {
        Jslib_Connect(clientIP, clientPort.ToString(), clientId, username, password, destination);
    }
​
    /// <summary>
    /// 订阅消息,为Unity提供
    /// </summary>
    /// <param name="topics"></param>
    public void Subscribe(params string[] topics)
    {
        foreach (string topic in topics)
        {
            Jslib_Subscribe(topic);
        }
    }
​
    /// <summary>
    /// 取消订消息,为Unity提供
    /// </summary>
    public void Unsubscribe(params string[] topics)
    {
        foreach (string topic in topics)
        {
            Jslib_Unsubscribe(topic);
        }
    }
​
    /// <summary>
    /// 监听订阅过的消息,为Unity提供
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="msg"></param>
    public void AddListenerSubscribe(MqttRecvMsgCallback mqttRecvMsgCallback)
    {
        m_RecvMsgCallback += mqttRecvMsgCallback;
    }
​
    /// <summary>
    /// 监听订阅过的消息,为Unity提供
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="msg"></param>
    public void RemoveListenerSubscribe(MqttRecvMsgCallback mqttRecvMsgCallback)
    {
        m_RecvMsgCallback -= mqttRecvMsgCallback;
    }
​
    /// <summary>
    /// 发送消息
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="msg"></param>
    public void Publish(string topic, string msg)
    {
        Jslib_Publish(topic, msg);
    }
​
    /// <summary>
    /// 断开连接
    /// </summary>
    public void DisConnect()
    {
        Jslib_Disconnect();
    }
​
    /// <summary>
    /// 接收订阅的消息,为外部h5提供api,晚点测试使用private能否调用
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="msg"></param>
     void RecvMsg(string jsonStr)
    {
        string topic = jsonStr.Split('|')[0];
        string msg = jsonStr.Split('|')[1];
        Debug.Log("[Unity] RecvMsg,topic:" + topic + ",msg:" + msg);
        m_RecvMsgCallback?.Invoke(topic, msg);
    }
​
    /// <summary>
    /// mqtt连接成功后回调
    /// </summary>
    public void ConnSuc()
    {
        Debug.Log("[Unity] ConnSuc");
        NetworkMqtt.GetInstance. ConnSucCallbackHandle?.Invoke();
    }

打包后配置

修改index.html

需要在打包webgl包后,手动修改index.html文件,添加xxx.jslib中调用的方法

在<body>标签中新增以下代码

 <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
  <script>
    //#region MQTT 连接、断开、订阅监听消息、发送消息
    /*
    MQTT类库https://unpkg.com/mqtt/dist/mqtt.min.js  
    API文档:https://www.emqx.com/zh/blog/mqtt-js-tutorial
    */
    var client
    var gameUnityInstance
    var subscribeIdObj = {}
    const options = {
      Name: 'Name_Test_MQTT_WebGl',
      connectTimeout: 40000,
      clientId: '',
      username: '',
      password: '',
      cleanSession: false,
      keepAlive: 60
    }
​
    //ArrayBuffer二进制转字符串
    function ab2str(message) {
      const utf8decoder = new TextDecoder()
      if (!message?.length) return null
      const type = Object.prototype.toString.call(message)
      let res = null
      if (type === '[object Uint8Array]') {
        // @ts-ignore
        res = utf8decoder.decode(new Uint8Array(message))
      } else {
        res = JSON.stringify(message)
      }
      if (Object.prototype.toString.call(res) !== '[object String]') {
        res = JSON.parse(res)
      }
      return res
    }
​
    //连接
    function mqttConnect(host, port, clientId, username, password, destination) {
      let url = 'ws://' + host + ':' + port + '/mqtt'
      //创建一个client实例
      options.clientId = clientId
      options.username = username
      options.password = password
​
      //Test
      url = 'ws://10.5.24.27:8083/mqtt'
​
      client = mqtt.connect(url, options)
      log("尝试链接mqtt IP:" + host + ",port:" + port + ",username:" + options.username + "clientId: " + options.clientId);
​
      var firstConnSuc = false;
      client.on('connect', function (connack) {
        if (!firstConnSuc) {
          firstConnSuc = true
          log("mqtt首次连接成功! username :" + options.username + "clientId: " + options.clientId, true);
          gameUnityInstance.SendMessage('[UnityObjectForWebglMsg]', 'ConnSuc')
        }
      })
    }
​
    //订阅监听消息
    function mqttSubscribe(topic) {
      log("尝试订阅监听消息 topic:" + topic);
​
      //topic: 可传入一个字符串,或者一个字符串数组,也可以是一个 topic 对象,{'test1': {qos: 0}, 'test2': {qos: 1}}
      //options: 可选值,订阅 Topic 时的配置信息,主要是填写订阅的 Topic 的 QoS 等级的
      //callback: 订阅 Topic 后的回调函数,参数为 error 和 granted,当订阅失败时 error 参数才存在, granted 是一个 { topic, qos } 的数组,其中 topic 是一个被订阅的主题,qos 是 Topic 是被授予的 QoS 等级
      subscribeIdObj[topic] = client.subscribe(topic, { qos: 0 }, function (error, granted) {
        if (error) {
          error("订阅监听消息失败 error:" + error)
        } else {
          if (granted.length > 0) {
            log(`订阅监听消息成功 topic: ${granted[0].topic}`)
          }
          else {
            error("订阅监听消息失败,当前标题已订阅 topic:" + topic)
          }
        }
      })
​
      client.on('message', function (_topic, message) {
        //二进制ArrayBuffer消息转字符串
        var msgStr = ab2str(message)
        log("接收到消息,msg:" + msgStr);
        gameUnityInstance.SendMessage('[UnityObjectForWebglMsg]', 'RecvMsg', _topic + "|" + message)
      })
    }
​
    //发送消息
    function publish(topic, payload) {
      log("尝试发送消息 , topic:" + topic + ", msg:" + payload);
      // 发布消息
      client.publish(topic, payload, { qos: 0, retain: false }, function (error) {
        if (error) {
          error("发送消息失败 error:" + error + ',topic:' + topic + ',msg:' + payload)
        } else {
          log('发送消息成功 topic:' + topic + ',msg:' + payload)
        }
      })
    }
​
    //取消消息订阅
    function mqttUnsubscribe(topic) {
      log('尝试取消消息订阅  topic:' + topic)
      client.unsubscribe(topic, function (error) {
        if (error) {
          log('取消消息订阅失败 topic:' + topic)
        } else {
          log('取消消息订阅成功 topic:' + topic)
        }
      })
    }
​
    // 断开连接
    function mqttDisconnect() {
      log("尝试断开mqtt链接");
      client.end(true, null, () => {
        log('已断开mqtt链接')
      })
    }
    //封装js日志打印
    function log(msg, isShow = true) {
      if (isShow) {
        console.log("[html]:" + msg);
      }
    }
    function error(msg, isShow = true) {
      if (isShow) {
        console.error("[html]:" + msg);
      }
    }
    //#endregion MQTT
  </script>

找到此代码段,只需新增一行代码

var script = document.createElement("script");
    script.src = loaderUrl;
    script.onload = () => {
      createUnityInstance(canvas, config, (progress) => {
        progressBarFull.style.width = 100 * progress + "%";
      }).then((unityInstance) => {
        gameUnityInstance = unityInstance    //当前代码端仅仅新增此行,当前代码端仅仅新增此行,当前代码端仅仅新增此行
        loadingBar.style.display = "none";
        fullscreenButton.onclick = () => {
          unityInstance.SetFullscreen(1);
        };
      }).catch((message) => {
        alert(message);
      });
    };
    document.body.appendChild(script);

注意

1.xxx.jslib父目录Plugins可以不放在Assets/根目录下



评论