You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Maui SignalR聊天应用一对一消息功能实现求助

问题描述

基于SignalR开发的.NET Maui聊天应用当前存在消息广播问题——发送的消息会被所有客户端接收。需要实现特定用户间的一对一消息功能,并在两个模拟器中完成测试。此前尝试通过连接ID实现该功能,但出现错误,寻求技术支持。

客户端代码

using Microsoft.Maui.Devices;
using Microsoft.Maui.Media;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Maui.Media;
using Microsoft.Maui.ApplicationModel;
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRClient
{
    public partial class MainPage : ContentPage
    {
        private readonly HubConnection hubConnection;
        private IEnumerable<Locale> locales;
        private CancellationTokenSource cancellationTokenSource;

        public MainPage()
        {
            InitializeComponent();

            string baseUrl = DeviceInfo.Current.Platform == DevicePlatform.Android ? "http://10.0.2.2" : "http://localhost";
            hubConnection = new HubConnectionBuilder()
                .WithUrl($"{baseUrl}:5127/chatHub")
                .Build();

            hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
            {
                this.Dispatcher.DispatchAsync(() =>
                {
                    lblChat.Text += $"<b>{user}</b>: {message}<br/>";
                });
            });

            _ = ConnectToHubAsync();
            cancellationTokenSource = new CancellationTokenSource();
        }

        protected override async void OnAppearing()
        {
            base.OnAppearing();
            locales = await TextToSpeech.GetLocalesAsync();

            foreach (var locale in locales)
            {
                Languages.Items.Add(locale.Language);
            }
            Languages.SelectedIndex = 10;
            Languages.IsVisible = true;

            await FirstStartListening(); // 新增调用
        }

        private async Task ConnectToHubAsync()
        {
            try
            {
                await hubConnection.StartAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"连接Hub错误: {ex.Message}");
            }
        }

        private async Task StartListening()
        {
            var isGranted = await Permissions.CheckStatusAsync<Permissions.Microphone>();
            if (isGranted != PermissionStatus.Granted)
            {
                isGranted = await Permissions.RequestAsync<Permissions.Microphone>();
            }

            if (isGranted == PermissionStatus.Granted)
            {
                var cultureInfo = new CultureInfo("tr-TR");
                cancellationTokenSource = new CancellationTokenSource();
                var cancellationToken = cancellationTokenSource.Token;

                try
                {
                    UpdateUI("语音识别启动中...");

                    var recognitionResult = await SpeechToText.Default.ListenAsync(
                        cultureInfo,
                        new Progress<string>(partialText => UpdateUI(partialText)),
                        cancellationToken);

                    if (recognitionResult != null && recognitionResult.IsSuccessful)
                    {
                        var usertext = recognitionResult.Text.ToLowerInvariant();

                        this.Dispatcher.DispatchAsync(() =>
                        {
                            txtMessage.Text = usertext; // 将识别的文本写入消息输入框
                        });

                        if (usertext.Contains("sesli mesaj gönder"))
                        {
                            btnRecord_Clicked(this, EventArgs.Empty);
                        }
                        else if (usertext.Contains("mesaj gönder"))
                        {
                            btnSend_Clicked(this, EventArgs.Empty);
                        }
                    }
                    else
                    {
                        UpdateUI("请重试。");
                    }
                }
                catch (OperationCanceledException)
                {
                    UpdateUI("语音识别已取消。");
                }
                catch (Exception ex)
                {
                    UpdateUI($"错误: {ex.Message}");
                }
            }
            else
            {
                UpdateUI("未授予麦克风权限。");
            }
        }

        private void UpdateUI(string message)
        {
            this.Dispatcher.DispatchAsync(() =>
            {
                txtMessage.Text = message;
            });
        }

        private async void btnSend_Clicked(object sender, EventArgs e)
        {
            await SendAndSpeakAsync();
        }

        private async Task SendAndSpeakAsync()
        {
            try
            {
                await hubConnection.InvokeAsync("SendMessageToAll", txtUsername.Text, txtMessage.Text);

                if (Languages.SelectedIndex >= 0)
                {
                    var selectedLocale = locales.ElementAt(Languages.SelectedIndex);
                    await TextToSpeech.SpeakAsync(txtMessage.Text, new SpeechOptions { Locale = selectedLocale });
                }

                txtMessage.Text = string.Empty;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"发送消息错误: {ex.Message}");
            }
        }

        private async Task FirstStartListening()
        {
            var isGranted = await CheckAndRequestMicrophonePermission();
            if (!isGranted)
            {
                UpdateUI("未授予麦克风权限。");
                return;
            }

            var cultureInfo = new CultureInfo("tr-TR");

            while (true)
            {
                if (cancellationTokenSource.Token.IsCancellationRequested)
                {
                    break;
                }

                var recognitionResult = await SpeechToText.Default.ListenAsync(
                    cultureInfo,
                    new Progress<string>(partialText => { }),
                    cancellationTokenSource.Token);

                if (recognitionResult.IsSuccessful)
                {
                    var usertext = recognitionResult.Text.ToLowerInvariant();

                      this.Dispatcher.DispatchAsync(() =>
                    {
                        txtMessage.Text = usertext; // 将识别的文本写入消息输入框
                    });

                    if (usertext.Contains("sesli mesaj gönder"))
                    {
                        btnRecord_Clicked(this, EventArgs.Empty);
                    }
                    else if (usertext.Contains("mesaj gönder")) 

                    {
                        btnSend_Clicked(this, EventArgs.Empty);
                    }
                }
            }
        }

        private static async Task<bool> CheckAndRequestMicrophonePermission()
        {
            var status = await Permissions.CheckStatusAsync<Permissions.Microphone>();
            if (status != PermissionStatus.Granted)
            {
                status = await Permissions.RequestAsync<Permissions.Microphone>();
            }

            return status == PermissionStatus.Granted;
        }

        // 录音按钮点击事件
        private async void btnRecord_Clicked(object sender, EventArgs e)
        {
            cancellationTokenSource?.Cancel();
            cancellationTokenSource = new CancellationTokenSource();

            await StartListening();
        }
    }
}

服务端Hub代码

using Microsoft.AspNetCore.SignalR;

namespace SignalRServer.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessageToAll(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}

服务端启动代码

using SignalRServer.Hubs;

var builder = WebApplication.CreateBuilder(args);

// 添加服务到容器
builder.Services.AddControllersWithViews();

builder.Services.AddSignalR();

var app = builder.Build();

// 配置HTTP请求管道
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // 默认HSTS值为30天,生产环境可调整
    app.UseHsts();

    app.UseHttpsRedirection();
}


app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapHub<ChatHub>("/chatHub");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=ChatDemo}/{id?}");

app.Run();

解决方案

核心思路

实现一对一消息的关键是建立用户名与SignalR连接ID的映射关系:每个客户端连接SignalR时会获得唯一连接ID,但该ID会随连接断开重连变化,因此需要在客户端连接时将用户名和当前连接ID注册到服务端,服务端通过用户名找到对应连接ID,将消息发送给指定客户端。

步骤1:修改服务端Hub

使用线程安全的ConcurrentDictionary存储用户名-连接ID映射,新增用户注册和一对一消息发送方法:

using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;

namespace SignalRServer.Hubs
{
    public class ChatHub : Hub
    {
        // 线程安全的用户名-连接ID映射存储
        private static readonly ConcurrentDictionary<string, string> _userConnections = new ConcurrentDictionary<string, string>();

        // 用户连接时注册映射关系
        public override async Task OnConnectedAsync()
        {
            var username = Context.GetHttpContext().Request.Query["username"];
            if (!string.IsNullOrEmpty(username))
            {
                _userConnections.TryAdd(username, Context.ConnectionId);
            }
            await base.OnConnectedAsync();
        }

        // 用户断开时移除映射
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            var user = _userConnections.FirstOrDefault(x => x.Value == Context.ConnectionId).Key;
            if (!string.IsNullOrEmpty(user))
            {
                _userConnections.TryRemove(user, out _);
            }
            await base.OnDisconnectedAsync(exception);
        }

        // 一对一消息发送方法
        public async Task SendMessageToUser(string fromUser, string toUser, string message)
        {
            if (_userConnections.TryGetValue(toUser, out var targetConnectionId))
            {
                // 发送给目标用户
                await Clients.Client(targetConnectionId).SendAsync("ReceiveMessage", fromUser, message);
                // 可选:给自己发送消息,显示已发送反馈
                await Clients.Caller.SendAsync("ReceiveMessage", fromUser, message);
            }
            else
            {
                // 通知发送方目标用户未在线
                await Clients.Caller.SendAsync("ReceiveMessage", "系统", $"用户 {toUser} 未在线");
            }
        }

        // 保留原广播方法(可选)
        public async Task SendMessageToAll(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}

步骤2:修改客户端代码

  1. UI新增目标用户输入框:在XAML中添加命名为txtTargetUser的输入框,用于输入接收消息的目标用户名。
  2. 修改Hub连接逻辑:连接时将当前用户名作为查询参数传入:
// 修改MainPage构造函数中的Hub连接代码
string username = txtUsername.Text; // 假设txtUsername是当前用户的输入框
hubConnection = new HubConnectionBuilder()
    .WithUrl($"{baseUrl}:5127/chatHub", options =>
    {
        options.QueryParameters.Add("username", username);
    })
    .Build();
  1. 修改消息发送方法:调用SendMessageToUser替代原SendMessageToAll:
private async Task SendAndSpeakAsync()
{
    try
    {
        var fromUser = txtUsername.Text;
        var toUser = txtTargetUser.Text;
        var message = txtMessage.Text;

        if (string.IsNullOrEmpty(fromUser) || string.IsNullOrEmpty(toUser) || string.IsNullOrEmpty(message))
        {
            Console.WriteLine("用户名、目标用户或消息不能为空");
            return;
        }

        await hubConnection.InvokeAsync("SendMessageToUser", fromUser, toUser, message);

        if (Languages.SelectedIndex >= 0)
        {
            var selectedLocale = locales.ElementAt(Languages.SelectedIndex);
            await TextToSpeech.SpeakAsync(message, new SpeechOptions { Locale = selectedLocale });
        }

        txtMessage.Text = string.Empty;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"发送消息错误: {ex.Message}");
    }
}

步骤3:测试流程

  1. 启动服务端。
  2. 打开两个.NET Maui模拟器,分别输入不同用户名(如A和B)。
  3. 在模拟器A中输入目标用户B和消息,点击发送;模拟器B应收到消息,模拟器A也会显示自己发送的消息(可选反馈)。
  4. 在模拟器B中输入目标用户A和消息,点击发送;模拟器A应收到消息,验证一对一功能生效。

内容的提问来源于stack exchange,提问作者begums

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 10:20:54