首頁

目前文章總數:157 篇

  

最後更新:2024年 12月 07日

0057. .Net Core WebSite網站 - 實現 gRPC聊天室

日期:2023年 12月 03日

標籤: C# Asp.net Core Web MVC Web gRPC CentOS Chat Room

摘要:C# 學習筆記


應用所需:1. Visual Studio 2022 以上,支援.net Core 6
範例檔案:範例檔案
解決問題:1. 實現基於 gRPC 通訊的.Net Core WebSite 聊天室範例
基本介紹:本篇分為二大部分。
第一部分:專案代碼實現說明
第二部分:聊天室Demo






第一部分:第一部分:專案代碼實現說明

Step 1:範例專案架構


1. Proto檔 定義gRPC的通訊方法,包含訂閱、發送訊息
2. 商務邏輯 實現聊天室訂閱、通訊方式、推播訊息
3. 控制器 .Cshtml 頁面上互動的基本邏輯、提供API接口
4. 檢視頁面 實現發送按鈕、渲染推播後的訊息接收與Dom元件互動
5. 初始化配置 配置gRPC端口、啟用方式、基於Web Server 與 gRPC 同接口下的重定向處理




Step 2:Proto檔 - 創建

我們在proto資料夾下建立一個 chat.proto 的 gRPC 通訊檔案,定義了:

1. SendMessage 傳送訊息方法                         
2. Subscribe 訂閱方法                         
syntax = "proto3";

option csharp_namespace = "ChatApp";

package chat;
import "google/protobuf/empty.proto";

message MessageRequest {
  string username = 1;
  string message = 2;
}

message MessageResponse {
  string username = 1;
  string message = 2;
}

service ChatService {
  rpc SendMessage(MessageRequest) returns (MessageResponse);
  rpc Subscribe(stream SubscribeRequest) returns (stream Message);  
}

message SubscribeRequest {
  string subscriber_name = 1;
}

message Message {
  string content = 1;
}



Step 3:Proto檔 - 加入參考

接著從專案中 -> 加入 -> 服務參考 -> 將 chat.proto 設為伺服器與客戶端,完成ChatServiceBase代碼


Step 4:商務邏輯

新建一個 ChatService.cs 類別,繼承於 Step3. 產生的 ChatServiceBase 代碼
實現[訂閱]、[發送訊息+推播]


    public class ChatService : ChatServiceBase
    {
        /// <summary>
        /// 訂閱者
        /// </summary>
        private static readonly List<IServerStreamWriter<Message>> Subscribers = new List<IServerStreamWriter<Message>>();

        /// <summary>
        /// 將訂閱者加入
        /// </summary>        
        public override async Task Subscribe(IAsyncStreamReader<SubscribeRequest> requestStream, IServerStreamWriter<Message> responseStream, ServerCallContext context)
        {
            var subscriberName = "";
            await foreach (var request in requestStream.ReadAllAsync())
            {
                //1. 加入訂閱
                subscriberName = request.SubscriberName;
                Subscribers.Add(responseStream);
            }

            // 2. 等待客戶端斷開連線            
            while (!context.CancellationToken.IsCancellationRequested)
            {
                await Task.Delay(1000);  // 或使用其他適當的延遲時間
            }

            // 3. 客戶端斷開連線後,移除訂閱者
            Subscribers.RemoveAll(s => s == responseStream);           
        }

        /// <summary>
        /// 發送訊息 + 推播
        /// </summary>                
        public override async Task<MessageResponse> SendMessage(MessageRequest request, ServerCallContext context)
        {
            var response = new MessageResponse
            {
                Username = request.Username,
                Message = request.Message
            };

            var pushMessage = new Message() { 
                Content = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + " => " + response.Username +":" + response.Message + Environment.NewLine };
            await BroadcastMessageAsync(pushMessage);

            return response;
        }

        /// <summary>
        /// 推播訊息
        /// </summary>                
        public static async Task BroadcastMessageAsync(Message message)
        {
            foreach (var subscriber in Subscribers.ToList())
            {
                try
                {
                    await subscriber.WriteAsync(message);
                }
                catch (Exception ex)
                {                    
                    Console.WriteLine($"Error broadcasting message to a subscriber: {ex.Message}");
                    Subscribers.Remove(subscriber);
                }
            }
        }
    }



Step 5:控制器

新建一個控制器 ChatController.cs ,處理聊天室檢視頁面與訂閱、推播、取得當前訊息功能


/// <summary>
/// 全域配置
/// </summary>
public class GlobalConst {
    /// <summary>
    /// 加入聊天室的用戶會記錄於此
    /// </summary>
    public static Dictionary<string, string> DicMessages = new Dictionary<string, string>();
    public static string Self_GRPC_URL = "";
}

public class ChatController : Controller
{
    private readonly ChatApp.ChatService.ChatServiceClient _grpcClient;
    public ChatController(ChatApp.ChatService.ChatServiceClient grpcClient)
    {
        _grpcClient = grpcClient;
    }
    public async Task<IActionResult> Index()
    {
        return View();
    }
    /// <summary>
    /// 傳送訊息 + 推播給所有訂閱用戶
    /// </summary>        
    [HttpGet]
    public async Task<IActionResult> SendMessage(string user, string message)
    {
        // 1. 訂閱消息 - 簡單用 Static 做為當前Server訂閱(加入聊天室)的人
        if (!GlobalConst.DicMessages.ContainsKey(user))
        {
            GlobalConst.DicMessages.Add(user, "");
            Task.Run(() => SubscribeToMessages(user));
        }
        //2. 組成返回資訊
        var resultMessage = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + ":";
        var messageResponse = new MessageResponse();
        var request = new MessageRequest { Username = user, Message = message };
        using (var call = _grpcClient.SendMessageAsync(request))
        {
            messageResponse = await call.ResponseAsync;
        }
        //3. 回傳訊息給叫用API
        return Ok(new {
            time = resultMessage,
            response = messageResponse
        });
    }

    /// <summary>
    /// 取得當前訂閱內容
    /// </summary>        
    [HttpGet]
    public string GetMessage(string user)
    {
        if (user == null)
            return "";
        return GlobalConst.DicMessages.ContainsKey(user) ? GlobalConst.DicMessages[user] : "";
    }

    /// <summary>
    /// 訂閱
    /// </summary>        
    private static async Task SubscribeToMessages(string username)
    {
        using (var channel = GrpcChannel.ForAddress(GlobalConst.Self_GRPC_URL))
        {
            var client = new ChatApp.ChatService.ChatServiceClient(channel);
            // 訂閱消息
            using (var subscribeCall = client.Subscribe())
            {
                var subscribeRequest = new SubscribeRequest { SubscriberName = username };
                await subscribeCall.RequestStream.WriteAsync(subscribeRequest);
                // 接收消息
                await foreach (var message in subscribeCall.ResponseStream.ReadAllAsync())
                {
                    GlobalConst.DicMessages[username] += message.Content;
                }
            }
        }
    }
}



Step 6:檢視頁面

新建一個 Chat 資料夾,新建 Index.cshtml 作為聊天室的頁面檢視功能
實現3個功能,畫面檢視、發送訊息(訂閱)、輪詢資訊。


@using ChatApp
@using Grpc.Core
@using Grpc.Net.Client
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime

@{
    ViewData["Title"] = "gRPC聊天室";
}
<input type="text" id="usernameInput" placeholder="姓名">
<input type="text" id="messageInput" placeholder="訊息">
<button onclick="SendMessage()">傳送訊息</button>
<br/>
<br/>

<textarea id="outputTextBox" rows="15" cols="100" readonly></textarea>

@section scripts {
    <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
    <script>
        //1. 傳送訊息 + 訂閱
        function SendMessage()
        {
            const username = document.getElementById("usernameInput").value;
            const message = document.getElementById("messageInput").value;
            var pushData = {
               user: username, 
               message: message 
            };
            $.get("/Chat/SendMessage", pushData, function(data) { 
                console.log(data);
            });

        }

        //2-1. 取得訊息的輪巡
        async function updateMessage() {
            var outputTextBox = document.getElementById('outputTextBox');
            var user = document.getElementById("usernameInput").value;
            var response = await fetch('/Chat/GetMessage?user=' + user);
            var myPageMessage = await response.text();
            outputTextBox.value = myPageMessage + '\n';
        }
     
        //2-2. 設定間隔 500ms
        setInterval(updateMessage, 500);
    </script>
}




Step 7:初始化配置

打開 program.cs 初始配置檔案,我們需要運行所需的配置。
包括啟動gRPC、取得當前url配置、重定向功能

using ChatApp;

var builder = WebApplication.CreateBuilder(args);

// 1. 添加gRPC 
builder.Services.AddGrpc();
builder.Services.AddGrpcClient<ChatApp.ChatService.ChatServiceClient>();

// 2. 取得當前gRPC Https連線配置
IConfigurationRoot baseBuilderData = new ConfigurationBuilder()    
    .AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "Properties", "launchSettings.json"), optional: true, reloadOnChange: true)
    .Build();

// 3. 設定當前Grpc連結到設定執中
string apiUrls = baseBuilderData["profiles:NetCoreGRPCChattingRoomExample:applicationUrl"];
if (apiUrls != null)
{
    var splitApi = apiUrls.Split(';').Where(item => item.ToLower().Contains("https://")).Select(item => item).FirstOrDefault();
    NetCoreGRPCChattingRoomExample.Controllers.GlobalConst.Self_GRPC_URL = splitApi ?? "";

    builder.Services.AddGrpcClient<ChatService.ChatServiceClient>(options =>
    {
        options.Address = new Uri(NetCoreGRPCChattingRoomExample.Controllers.GlobalConst.Self_GRPC_URL); // Your gRPC server address
    });
}


\\配置其他Service......



var app = builder.Build();

// 4-1. 實現重定向,讓gRPC Server 與 Web Server 走同個port
app.Use(async (context, next) =>
{
    if (context.Request.Headers.ContainsKey("content-type") &&
        context.Request.Headers["content-type"].ToString().StartsWith("application/grpc", StringComparison.OrdinalIgnoreCase)
        )
    {
        // 4-2. 如果不是 gRPC 請求,應用 HTTPS 重定向
        context.Request.Scheme = "https";
        await next();
    }
    else
    {
        await next();
    }
});

\\配置其他app configure......






第二部分:聊天室Demo

Step 1:執行程式

我們啟動專案,可以利用Debug模式啟動,選擇 gRPC聊天室


Step 2:開啟分頁 - 傳送訊息

我們開啟分頁,左右兩個,並且在左邊輸入 名字 + 訊息 => 傳送訊息
因為發送訊息後才會視為加入聊天室,右邊的分頁不在聊天室中


Step 3:執行另個分頁 - 完成聊天室

對右邊分頁,輸入 名字 + 訊息 => 傳送訊息
可以發現在同個Server下,兩邊會互相傳送訊息,基於 gRPC 訂閱後推播資料