.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:修改客户端代码
- UI新增目标用户输入框:在XAML中添加命名为
txtTargetUser的输入框,用于输入接收消息的目标用户名。 - 修改Hub连接逻辑:连接时将当前用户名作为查询参数传入:
// 修改MainPage构造函数中的Hub连接代码 string username = txtUsername.Text; // 假设txtUsername是当前用户的输入框 hubConnection = new HubConnectionBuilder() .WithUrl($"{baseUrl}:5127/chatHub", options => { options.QueryParameters.Add("username", username); }) .Build();
- 修改消息发送方法:调用
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:测试流程
- 启动服务端。
- 打开两个.NET Maui模拟器,分别输入不同用户名(如A和B)。
- 在模拟器A中输入目标用户B和消息,点击发送;模拟器B应收到消息,模拟器A也会显示自己发送的消息(可选反馈)。
- 在模拟器B中输入目标用户A和消息,点击发送;模拟器A应收到消息,验证一对一功能生效。
内容的提问来源于stack exchange,提问作者begums
相关产品推荐
相关产品推荐

