分享程式代碼相關筆記
目前文章總數:157 篇
最後更新:2024年 12月 07日
1. Proto檔 | : | 定義gRPC的通訊方法,包含訂閱、發送訊息 |
2. 商務邏輯 | : | 實現聊天室訂閱、通訊方式、推播訊息 |
3. 控制器 | : | .Cshtml 頁面上互動的基本邏輯、提供API接口 |
4. 檢視頁面 | : | 實現發送按鈕、渲染推播後的訊息接收與Dom元件互動 |
5. 初始化配置 | : | 配置gRPC端口、啟用方式、基於Web Server 與 gRPC 同接口下的重定向處理 |
我們在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;
}
接著從專案中 -> 加入 -> 服務參考 -> 將 chat.proto 設為伺服器與客戶端,完成ChatServiceBase代碼
新建一個 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);
}
}
}
}
新建一個控制器 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;
}
}
}
}
}
新建一個 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>
}
打開 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......
我們啟動專案,可以利用Debug模式啟動,選擇 gRPC聊天室
我們開啟分頁,左右兩個,並且在左邊輸入 名字 + 訊息 => 傳送訊息
因為發送訊息後才會視為加入聊天室,右邊的分頁不在聊天室中
對右邊分頁,輸入 名字 + 訊息 => 傳送訊息
可以發現在同個Server下,兩邊會互相傳送訊息,基於 gRPC 訂閱後推播資料