Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I send message to a specificate client? #751

Open
philwu opened this issue May 23, 2024 · 19 comments
Open

How can I send message to a specificate client? #751

philwu opened this issue May 23, 2024 · 19 comments

Comments

@philwu
Copy link

philwu commented May 23, 2024

Let's see, I already had a running websocket server. How can I take the initiative to send a message to one of them?

Currently, I can only:

Wait a message from a client and make response, or
Make a broadcast so every client can receive the message.
But what I want is sending to a client its own message by some logic.

Anyone help, please.

@GholibjonMadiyarov
Copy link

`server = new WebSocketServer(IPAddress.Any, 8000, true);
server.SslConfiguration.ServerCertificate = new X509Certificate2("file", "password");
server.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Default | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12;

server.AddWebSocketService("/Web");
server.AddWebSocketService("/Desktop");
server.AddWebSocketService("/Mobile");

server.Log.File = AppDomain.CurrentDomain.BaseDirectory + "/ServerLog.txt";
server.KeepClean = false;

server.Start();

//Client id from db or file
string clientId = "9b0d327304bc433cb2c1968720cd2792";

//Message data
string message = "Hi!";

if (server.WebSocketServices["/Desktop"].Sessions.TryGetSession(clientId, out IWebSocketSession session))
{
//Client is open
if (session.State == WebSocketSharp.WebSocketState.Open)
{
server.WebSocketServices["/Desktop"].Sessions.SendTo(message, clientId);
//session.Context.WebSocket.Send(message);
}
}`

@lvqibin123
Copy link

wssv.WebSocketServices["/game"].Sessions.Broadcast
You access it like this.

@GholibjonMadiyarov
Copy link

This example is for sending messages to an individual client

@GholibjonMadiyarov
Copy link

your example to send message to all clients connected to module "/game"

@lvqibin123
Copy link

WriteException.cs
Used for writing logs!

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace Helper
{
   
    public class WriteException
    {
        #region 单例懒加载
        private static readonly Lazy<WriteException> lazy = new Lazy<WriteException>(() => new WriteException());
        public static WriteException Instance { get { return lazy.Value; } }
        #endregion

        public void WriteBlog(string strTile, string strContent)
        {
            try
            {
                StreamWriter writer = File.AppendText(AppDomain.CurrentDomain.BaseDirectory + "Blog.dll");
                writer.WriteLine("-------------------------------------------");
                writer.WriteLine(strTile);
                writer.WriteLine(DateTime.Now.ToString());
                writer.WriteLine(strContent);
                writer.WriteLine("-------------------------------------------");
                writer.Flush();
                writer.Close();
            }
            catch (Exception)
            {
            }
        }

        /// <summary>
        /// 写入日志
        /// </summary>
        /// <param name="message"></param>
        public  string WriteLog(string strTile, string strContent)
        {
            string result = "";
            try
            {
                string path = AppDomain.CurrentDomain.BaseDirectory;
                path += "\\" + DateTime.Now.ToString("yyyy") + "\\" + DateTime.Now.ToString("MM") + "\\" + DateTime.Now.ToString("dd");
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }
                string name = "czgl.log";

                DateTime dt = DateTime.Now;
                result = dt.ToString() + ":[" + strTile + "]" + strContent;

                StreamWriter writer = File.AppendText(path + "\\" + name);
                writer.WriteLine(result);
                writer.Flush();
                writer.Close();

                result = "[" + strTile + "]" + strContent + "   " + dt.ToString("yyyy-MM-dd HH:mm:ss ");
            }
            catch (Exception ex)
            {
                result = ex.Message;
            }
            return result;
        }

        /// <summary>
        /// 保存图片
        /// </summary>
        /// <param name="path"></param>
        /// <param name="data"></param>
        public void WriteImg(string path, ref byte[] data)
        {
            FileStream aFile = new FileStream(path, FileMode.Create);
            aFile.Seek(0, SeekOrigin.Begin);
            aFile.Write(data, 0, data.Length);
            aFile.Close();
        }


   
    }
}

CacheUtil.cs
Used for caching user information into file caching tool class!

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;

namespace Helper
{
    /// <summary>
    /// 缓存工具类
    /// </summary>
    public static class CacheUtil
    {
        #region 变量
        /// <summary>
        /// 缓存路径
        /// </summary>
        private static string folderPath = Application.StartupPath + "\\cache";
        /// <summary>
        /// 锁
        /// </summary>
        private static object _lock = new object();
        private static BinaryFormatter formatter = new BinaryFormatter();
        #endregion

        #region 构造函数
        static CacheUtil()
        {
            if (!Directory.Exists(folderPath))
            {
                Directory.CreateDirectory(folderPath);
            }
        }
        #endregion

        #region SetValue 保存键值对
        /// <summary>
        /// 保存键值对
        /// </summary>
        public static void SetValue(string key, object value, int expirationMinutes = 0)
        {
            CacheData data = new CacheData(key, value);
            data.updateTime = DateTime.Now;
            data.expirationMinutes = expirationMinutes;

            string keyMd5 = GetMD5(key);
            string path = folderPath + "\\" + keyMd5 + ".txt";
            lock (_lock)
            {
                using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
                {
                    fs.SetLength(0);
                    formatter.Serialize(fs, data);
                    fs.Close();
                }
            }
        }
        #endregion

        #region GetValue 获取键值对
        /// <summary>
        /// 获取键值对
        /// </summary>
        public static object GetValue(string key)
        {
            string keyMd5 = GetMD5(key);
            string path = folderPath + "\\" + keyMd5 + ".txt";
            if (File.Exists(path))
            {
                using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
                {
                    Console.WriteLine(" folderPath = {0}", folderPath);
                    Console.WriteLine(" path = {0}", path);
                    CacheData data = (CacheData)formatter.Deserialize(fs);
                    fs.Close();
                    if (data.expirationMinutes > 0 && DateTime.Now.Subtract(data.updateTime).TotalMinutes > data.expirationMinutes)
                    {
                        File.Delete(path);
                        return null;
                    }
                    return data.value;
                }
            }
            return null;
        }
        #endregion

        #region Delete 删除
        /// <summary>
        /// 删除
        /// </summary>
        public static void Delete(string key)
        {
            string keyMd5 = GetMD5(key);
            string path = folderPath + "\\" + keyMd5 + ".txt";
            if (File.Exists(path))
            {
                lock (_lock)
                {
                    File.Delete(path);
                }
            }
        }
        #endregion

        #region DeleteAll 全部删除
        /// <summary>
        /// 全部删除
        /// </summary>
        public static void DeleteAll()
        {
            string[] files = Directory.GetFiles(folderPath);
            foreach (string file in files)
            {
                File.Delete(file);
            }
        }
        #endregion

        #region 计算MD5值
        /// <summary>
        /// 计算MD5值
        /// </summary>
        private static string GetMD5(string value)
        {
            string base64 = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes(value)).Replace("/", "-");
            if (base64.Length > 200)
            {
                MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
                byte[] bArr = md5.ComputeHash(ASCIIEncoding.ASCII.GetBytes(value));
                StringBuilder sb = new StringBuilder();
                foreach (byte b in bArr)
                {
                    sb.Append(b.ToString("x2"));
                }
                return sb.ToString();
            }
            return base64;
        }
        #endregion
    }

    #region CacheData 缓存数据
    /// <summary>
    /// 缓存数据
    /// </summary>
    [Serializable]
    public class CacheData
    {
        /// <summary>
        /// 键
        /// </summary>
        public string key { get; set; }
        /// <summary>
        /// 值
        /// </summary>
        public object value { get; set; }
        /// <summary>
        /// 缓存更新时间
        /// </summary>
        public DateTime updateTime { get; set; }
        /// <summary>
        /// 过期时间(分钟),0表示永不过期
        /// </summary>
        public int expirationMinutes { get; set; }

        public CacheData(string key, object value)
        {
            this.key = key;
            this.value = value;
        }
    }
    #endregion

}

Chat.cs
Used to receive data from the front-end! Store the name and session ID passed by the frontend in the OnOpen method and save them to a file!

using Helper;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Threading;
using WebSocketSharp;
using WebSocketSharp.Server;

namespace HKHTTP
{
  public class Chat : WebSocketBehavior
  {
    private string     _name;
    private static int _number = 0;
    private string     _prefix;
    /// 锁
    /// </summary>
    private static object _lock = new object();

    public Chat ()
    {
      _prefix = "sinovine#";
    }

    public string Prefix {
      get {
        return _prefix;
      }

      set {
        _prefix = !value.IsNullOrEmpty () ? value : "sinovine#";
      }
    }

    private string getName ()
    {
      WriteException.Instance.WriteLog("Chat ", "getName");
      var name = QueryString["name"];
      WriteException.Instance.WriteLog("Chat name = ", name);
      return !name.IsNullOrEmpty () ? name : _prefix + getNumber ();
    }

    private static int getNumber ()
    {
      return Interlocked.Increment (ref _number);
    }

    protected override void OnClose (CloseEventArgs e)
    {
        WriteException.Instance.WriteLog("Chat ", "OnClose");
      if (_name == null)
        return;

      var fmt = "{0} got logged off...";
      var msg = String.Format (fmt, _name);
      WriteException.Instance.WriteLog("Chat OnClose fmt = ", fmt);
      WriteException.Instance.WriteLog("Chat OnClose msg = ", msg);
      Sessions.Broadcast (msg);
    }

    protected override void OnMessage (MessageEventArgs e)
    {
        WriteException.Instance.WriteLog("Chat ", "OnMessage");
      var fmt = "{0}: {1}";
      var msg = String.Format (fmt, _name, e.Data);
      WriteException.Instance.WriteLog("Chat OnOpen ID = ", ID);
      WriteException.Instance.WriteLog("Chat OnMessage fmt = ", fmt);
      WriteException.Instance.WriteLog("Chat OnMessage msg = ", msg);
      Sessions.Broadcast(msg);
      //if (_name == "admin")
      //{
      //    Sessions.SendToAsync(e.Data, ID, (bool b) =>
      //    {
      //        WriteException.Instance.WriteLog("WebSocket SendToAsync b= ", b.ToString());
      //    });
      //}
    }

    protected override void OnOpen ()
    {
        WriteException.Instance.WriteLog("Chat ", "OnOpen");
      _name = getName ();

      var fmt = "{0} has logged in!";
      var msg = String.Format (fmt, _name);
      //foreach (WebSocketSessionManager sessions in Sessions.Sessions)
      //{
      //    sessions.
      //}
      lock (_lock)
      {
          string webSocketUserString = CacheUtil.GetValue("webSocketUserString").ToString();
          try
          {
              WriteException.Instance.WriteLog("Chat OnOpen webSocketUserString = ", webSocketUserString);
              if (webSocketUserString != "")
              {
                  WriteException.Instance.WriteLog("Chat OnOpen newDataString 不为空 newDataString = ", "");
                  JArray data = JsonConvert.DeserializeObject<JArray>(webSocketUserString);
                  WriteException.Instance.WriteLog("Chat OnOpen data = ", JsonConvert.SerializeObject(data, Formatting.Indented));
                  JArray newData = new JArray();
                  foreach (JObject d in data)
                  {
                      string name = d["name"].ToString();
                      string id = d["id"].ToString();
                      WriteException.Instance.WriteLog("Chat OnOpen newDataString 不为空 data name = ", name);
                      WriteException.Instance.WriteLog("Chat OnOpen newDataString 不为空 data id = ", id);
                      newData.Add(d);
                      //if (name != _name)
                      //{
                      //    newData.Add(d);
                      //}
                      //else
                      //{
                      //    IEnumerable<string> activeIDs = Sessions.ActiveIDs;
                      //    foreach (string sessionId in activeIDs)
                      //    {
                      //        WriteException.Instance.WriteLog("Chat OnOpen newDataString 不为空 sessions sessionId = ", sessionId);
                      //        if (sessionId != ID)
                      //        {
                      //            IWebSocketSession session;
                      //            Sessions.TryGetSession(sessionId, out session);
                      //            session.WebSocket.Close();
                      //            WriteException.Instance.WriteLog("Chat OnOpen newDataString 不为空 sessions sessionId = ", sessionId + "session Close!");
                      //        }
                      //    }
                      //}
                  }
                  JObject newSion = new JObject();
                  newSion["name"] = _name;
                  newSion["id"] = ID;
                  newData.Add(newSion);
                  string newDataString = JsonConvert.SerializeObject(newData, Formatting.Indented);
                  WriteException.Instance.WriteLog("Chat OnOpen newDataString 不为空 newDataString = ", newDataString);
                  CacheUtil.SetValue("webSocketUserString", newDataString);
              }
              else
              {
                  WriteException.Instance.WriteLog("Chat OnOpen 获取缓存  webSocketUserString  为空 ", "");
                  //string data = "{{\"name\":\"" + _name + "\",\"id\":\"" + ID + "\"}}";
                  JArray newData = new JArray();
                  JObject newSion = new JObject();
                  newSion["name"] = _name;
                  newSion["id"] = ID;
                  newData.Add(newSion);
                  string newDataString = JsonConvert.SerializeObject(newData, Formatting.Indented);
                  WriteException.Instance.WriteLog("Chat OnOpen 获取缓存  webSocketUserString  为空 newDataString = ", newDataString);
                  CacheUtil.SetValue("webSocketUserString", newDataString);

              }
          }
          catch (Exception ex)
          {
              WriteException.Instance.WriteLog("Chat OnOpen  ex= ", ex.StackTrace);
          }
      }
      WriteException.Instance.WriteLog("Chat OnOpen ID = ", ID);
      WriteException.Instance.WriteLog("Chat OnOpen fmt = ", fmt);
      WriteException.Instance.WriteLog("Chat OnOpen msg = ", msg);
      Sessions.Broadcast (msg);
       
    }
  }
}

How to use it?

private HttpServer wssv = new HttpServer(4649);
WriteException.Instance.WriteLog("WebSocket", "启动");
wssv.AddWebSocketService<Chat>("/Chat");
wssv.Start();
WriteException.Instance.WriteLog("WebSocket", "测试WebSocket发送信息");
            wssv.WebSocketServices["/Chat"].Sessions.Broadcast("成功了,呵呵!");
            string data = "{\"ch\":\"云D6H311\",\"jpgname\":\"192.168.22.9_20241204133632696\",\"ipaddr\":\"192.168.22.9\",\"type\":\"过磅未完成\"}";
            // 将JSON字符串反序列化为C#对象
            //wssv.WebSocketServices["/Chat"].Sessions.Broadcast(data);
          try
          {
              string webSocketUserString = CacheUtil.GetValue("webSocketUserString").ToString();
              WriteException.Instance.WriteLog("测试WebSocket发送信息 = ", webSocketUserString);
              if (webSocketUserString != "")
              {
                  WriteException.Instance.WriteLog("测试WebSocket发送信息  webSocketUserString 不为空 ", "");
                  JArray webSocketUser = JsonConvert.DeserializeObject<JArray>(webSocketUserString);
                  WriteException.Instance.WriteLog("测试WebSocket发送信息 webSocketUserString 不为空 data = ", JsonConvert.SerializeObject(webSocketUser, Formatting.Indented));
                  foreach (JObject d in webSocketUser)
                  {
                      string name = d["name"].ToString();
                      string id = d["id"].ToString();
                      IEnumerable<string> activeIDs = wssv.WebSocketServices["/Chat"].Sessions.ActiveIDs;
                      foreach (string sessionId in activeIDs)
                      {
                          WriteException.Instance.WriteLog("测试WebSocket发送信息 webSocketUserString 不为空 sessions sessionId = ", sessionId);
                          if(name=="admin"){
                               if (sessionId == id)
                              {
                                  IWebSocketSession session;
                                  wssv.WebSocketServices["/Chat"].Sessions.SendTo(data,id);
                                  WriteException.Instance.WriteLog("测试WebSocket发送信息 webSocketUserString 不为空 sessions sessionId = ", sessionId + "发送消息");
                              }
                          }
                      }
                  }
              }
              else
              {
                  WriteException.Instance.WriteLog("测试WebSocket发送信息  webSocketUserString 为空 ", "");
              }
            }
            catch (Exception ex)
            {
                WriteException.Instance.WriteLog("测试WebSocket发送信息  ex= ", ex.StackTrace);
          }

Front end request address!

"ws://192.168.3.100:4649/Chat?name="+usId

@lvqibin123
Copy link

lvqibin123 commented Dec 11, 2024

Add a timer component to clear the task of closing WebSocket.

 private async void cleanWebSocketClosetimer_Tick(object sender, EventArgs e)
        {
            WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务   ", "》》》》》》》》》》》》》》》》》");
            try
            {
                bool k =  wssv.WebSocketServices["/Chat"].Sessions.KeepClean;
                if (k) { 
                    string webSocketUserString = CacheUtil.GetValue("webSocketUserString").ToString();
                    WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务 = ", webSocketUserString);
                    if (webSocketUserString != "")
                    {
                        JArray newData = new JArray();
                        WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务  webSocketUserString 不为空 ", "");
                        JArray webSocketUser = JsonConvert.DeserializeObject<JArray>(webSocketUserString);
                        WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务 webSocketUserString 不为空 data = ", JsonConvert.SerializeObject(webSocketUser, Formatting.Indented));
                        foreach (JObject d in webSocketUser)
                        {
                            string name = d["name"].ToString();
                            string id = d["id"].ToString();
                            IEnumerable<string> activeIDs = wssv.WebSocketServices["/Chat"].Sessions.ActiveIDs;
                            foreach (string sessionId in activeIDs)
                            {
                                if (sessionId == id)
                                {
                                    newData.Add(d);
                                }
                            }
                        }
                        string newDataString = JsonConvert.SerializeObject(newData, Formatting.Indented);
                        WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务 不为空 newDataString = ", newDataString);
                        CacheUtil.SetValue("webSocketUserString", newDataString);
                    }
                    else
                    {
                        WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务  webSocketUserString 为空 ", "");
                    }
                }
            }
            catch (Exception ex)
            {
                WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务  ex= ", ex.StackTrace);
            }
            WriteException.Instance.WriteLog("定时清除关闭WebSocket的任务   ", "》》》》》》》》》》》》》》》》》");

        }

@GholibjonMadiyarov
Copy link

Hi

@GholibjonMadiyarov
Copy link

  1. Why do you use HttpServer and not WebSocket() if you want to develop Chat?

@GholibjonMadiyarov
Copy link

I will try to prepare for you a scheme for the implementation of the Chat!

@lvqibin123
Copy link

The case I saw, WebSocket (), is used to send messages to clients, while HttpServer receives messages sent by clients. I only thought of this way to handle it!

@lvqibin123
Copy link

This plan can meet my current needs.

@GholibjonMadiyarov
Copy link

But I prefer to use only socket for both sides

@GholibjonMadiyarov
Copy link

Client:

@GholibjonMadiyarov
Copy link

var client = new WebSocket(ApplicationConfigurations.getConfig("websocket_host"));

client.SslConfiguration.ServerCertificateValidationCallback =
(s, certificate, chain, sslPolicyErrors) =>
{
// Do something to validate the server certificate.
return true; // If the server certificate is valid.
};

client.OnOpen += Client_OnOpen;
client.OnMessage += Client_OnMessage;
client.OnClose += Client_OnClose;
client.OnError += Client_OnError;

client.Compression = CompressionMethod.Deflate;

client.Log.File = AppDomain.CurrentDomain.BaseDirectory + "/WebSocketLog.txt";

client.Connect();

// Events
private void Client_OnOpen(object sender, EventArgs e)
{
Log.toFile("OpenLog.txt", "[Open]);
}

private void Client_OnMessage(object sender, MessageEventArgs e)
{
Log.toFile("MessageLog.txt", "[Message] Data:" + e.Data);
}

private void Client_OnClose(object sender, CloseEventArgs e)
{
Log.toFile("CloseLog.txt", "[Close] Code: " + e.Code + ", Reason:" + e.Reason + ", WasClean:" + e.WasClean);
}

private void Client_OnError(object sender, WebSocketSharp.ErrorEventArgs e)
{
Log.toFile("ErrorLog.txt", "[Error] Message:" + e.Message);
}

@GholibjonMadiyarov
Copy link

Server (Better implementation in Windows service):

@GholibjonMadiyarov
Copy link

server = new WebSocketServer(IPAddress.Any, ApplicationConfigurations.getConfig("port").toInt(), true);
server.SslConfiguration.ServerCertificate = new X509Certificate2(AppDomain.CurrentDomain.BaseDirectory + "/" + ApplicationConfigurations.getConfig("certificate_file"), ApplicationConfigurations.getConfig("certificate_password"));
server.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Default | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12;

server.AddWebSocketService("/Chat");

server.Log.File = AppDomain.CurrentDomain.BaseDirectory + "/ServerLog.txt";
server.KeepClean = false;

server.Start();

@GholibjonMadiyarov
Copy link

Chat Behavior:

@GholibjonMadiyarov
Copy link

class Chat : WebSocketBehavior
{
public Chat()
{

}

protected override void OnOpen()
{
	var IP = Context.UserEndPoint.Address.ToString();
	var port = Context.UserEndPoint.Port.ToString();
	var id = this.ID;

	Logs.toFile("ClientConnectedLog.txt", "[ClientConnected] IP:" + IP + ", port:" + port + "id:" + id);
}

protected override void OnMessage(MessageEventArgs e)
{
    try
    {
        JObject data = JObject.Parse(e.Data);

        if (data["identification"] != null)
        {
            if (ApplicationConfigurations.getConfig("key") == data["identification"]["key"].ToString())
            {
                switch (data["query"].ToString())
                {
                    case "handshake":
                        // Handshake Algorithms
						
						Send("Handshake Data");
                        break;

                    case "message":
                         // Message Actions
						
						//Send to this client
						Send("Message Data");
                        break;

                    case "key1":
                        //Actions by key1
                        break;

                    case "key2":
                        //Actions by key2
                        break;

                    default:
                        Logs.toFile("OnMessageLog.txt", "[Uncnown Key] Key:" + data["query"]);
                        break;
                }
            }
            else
            {
                Logs.toFile("IdentificationLog.txt", "[Invalid Identification] Key:" + data["identification"]["key"]);

                Context.WebSocket.CloseAsync(CloseStatusCode.Normal, "Closed due to incorrect identification");
            }
        }
        else
        {
            Logs.toFile("IdentificationLog.txt", "[Empty Identification] Message:Closed due to missing identification");

            Context.WebSocket.CloseAsync(CloseStatusCode.Normal, "Closed due to missing identification");
        }
    }
    catch (Exception exception)
    {
        Logs.toFile("MessageRecievedLog.txt", "Exception:" + exception.Message);
    }
}

protected override void OnError(ErrorEventArgs e)
{
    Logs.toFile("ErrorLog.txt", "[Exception] Message:" + e.Message);
}

protected override void OnClose(CloseEventArgs e)
{
	Logs.toFile("CloseLog.txt", " Code: " + e.Code + ", Reason:" + e.Reason + ", WasClean:" + e.WasClean);
}

}

@lvqibin123
Copy link

My usage scenario is: after the camera captures the license plate, it is written into the database, and then the information is sent to the web end, and then displayed on the web page. There are many users logging in on the web end, and only the administrator role can push it. Administrators can log in from multiple ends and need to push information to each administrator; The plan you gave me is not very suitable for my usage scenario. But I still want to thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants