You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 5 Next »

Blazor를 이용하여~ 그래픽 웹챗을 구현하는 변종 실험입니다.


GIT : https://github.com/psmon/BlazorChatApp


구현기능

  • 입장을 하면, 자신의 캐릭터(도형)가 랜덤생성됩니다.

실행 샘플

meta-chat.mp4



메시지 설계

웹소켓(브라우져)와 서버메시지(액터)의 정의를 통합할수 있습니다. - 블레이즈 특성

namespace BlazorChatApp.Shared
{
    public class ChatData
    {
    }

    public class UserInfo
    {
        public string Id { get; set; }
        public string Name { get; set; }
 
        public string Color { get; set; }
    }

    public class RoomInfo
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public class UpdateUserPos :UserInfo
    { 
        public double PosX{get; set; }
        public double PosY{get; set; }
        
    }


    public class ChatMessage
    { 
        public UserInfo From { get; set; }
        public string Message { get; set; }
    }

    public class JoinRoom
    {
        public RoomInfo RoomInfo { get; set; }
        public UserInfo UserInfo { get; set; }
    }

    public class SyncRoom
    {
        public RoomInfo RoomInfo { get; set; }
        public UserInfo UserInfo { get; set; }
    }

    public class LeaveRoom
    {
        public RoomInfo RoomInfo { get; set; }
        public UserInfo UserInfo { get; set; }
    }


    public class BaseCmd
    {
        public string Command {get;set; }
    }


    public class RoomCmd : BaseCmd
    {        
        public UserInfo UserInfo{ get; set; }
        public object Data{get;set; }
    }

}


웹소켓 처리코드

시그널 R이 이용되었으며, 일부 서버에서 상태처리를 해야하는경우 액터를 연결하여 처리가 가능합니다.

  • Cilent To Server : 브라우저에서 출발한 메시지를 서버에 전송합니다.
  • Server To Client : 서버에서 연산된 메시지를, 브라우저에게 전송합니다.
using System.Collections.Generic;
using System.Threading.Tasks;

using Akka.Actor;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR;

namespace BlazorChatApp.Server.Hubs
{
    public class ChatHub : Hub
    {
        private ActorSystem actorSystem;

        private ActorSelection roomActor;

        public ChatHub(ActorSystem _actorSystem)
        {
            actorSystem = _actorSystem;
            roomActor = actorSystem.ActorSelection("user/room1");
        }

        
        // Client To Server

        public async Task JoInRoom(JoinRoom joinRoom)
        {
            roomActor.Tell(joinRoom);
        }

        public async Task SyncRoom(SyncRoom syncRoom)
        {
            roomActor.Tell(syncRoom);
        }

        public async Task LeaveRoom(LeaveRoom leaveRoom)
        {
            roomActor.Tell(leaveRoom);
        }

        public async Task UpdateUserPos(UpdateUserPos updateUserPos)
        {
            roomActor.Tell(updateUserPos);
        }

        // Server To Client

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            await Clients.All.SendAsync("OnJoinRoom", user, roomInfo, updateUserPos);
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos)
        {
            await Clients.All.SendAsync("OnSyncRoom", user, updateUserPos);
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            await Clients.All.SendAsync("OnLeaveRoom", leaveRoom);
        }

        public async Task OnUpdateUserPos(UpdateUserPos updateUserPos)
        {
            await Clients.All.SendAsync("OnUpdateUserPos", updateUserPos);
        }

        
        public async Task SendMessage(string user, string message)       
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }

        public async Task SendRoomMessage(RoomCmd roomCmd)
        {
            roomActor.Tell(roomCmd);        
        }

    }
}


서버코드

웹소켓 이벤트가 주로 브라우저에서 발생하는 이벤트를 처리하면

서버내에서 처리해야하는 로직의 경우 액터를 활용하는것이 유연할수 있습니다.

여기서는 접속한 사용자의 좌표및 동기화를 관리합니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;

using Akka.Actor;
using Akka.Event;

using BlazorChatApp.Shared;

using Microsoft.AspNetCore.SignalR.Client;

namespace BlazorChatApp.Server.Hubs
{
    public class RoomActor : ReceiveActor
    {
        private readonly ILoggingAdapter log = Context.GetLogger();

        public Dictionary<string,UpdateUserPos> users = new Dictionary<string,UpdateUserPos>();

        private string roomName;

        private int userAutoNo = 0;

        public HubConnection hubConnection { get; set; }

        Random random= new Random();

        public RoomActor(string _roomName)
        {
            string baseUrl = "http://localhost:5000";
            var _hubUrl = baseUrl.TrimEnd('/') + "/chathub";
            hubConnection = new HubConnectionBuilder().WithUrl(_hubUrl).Build();

            hubConnection.StartAsync().Wait();

            roomName = _roomName;

            log.Info($"Create Room{roomName}");

            Receive<RoomCmd>(cmd => {
                log.Info("Received String message: {0}", cmd);
                //Sender.Tell(message);                
            });

            Receive<JoinRoom>(async cmd => {
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received JoinRoom message: {0}", jsonString);                
                string RandomColor =  string.Format("#{0:X6}", random.Next(0xFFFFFF));
                
                UserInfo userInfo = new UserInfo()
                { 
                    Id=cmd.UserInfo.Id,
                    Name=$"User-{userAutoNo}",
                    Color=RandomColor
                };

                UpdateUserPos updateUserPos= new UpdateUserPos()
                { 
                    Id=cmd.UserInfo.Id,
                    Name=$"User-{userAutoNo}",
                    PosX=random.NextDouble()*500,PosY=random.NextDouble()*500
                };

                users[cmd.UserInfo.Id] = updateUserPos;

                await OnJoinRoom(cmd.RoomInfo, userInfo, updateUserPos);
            });

            Receive<SyncRoom>(async cmd => {           
                userAutoNo++;
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);

                List<UpdateUserPos> updateUserPosList = users.Values.ToList();                
                //await hubConnection.SendAsync("OnSyncRoom", cmd.UserInfo, updateUserPosList);
                string RandomColor =  string.Format("#{0:X6}", random.Next(0xFFFFFF));

                UserInfo userInfo = new UserInfo()
                { 
                    Id=cmd.UserInfo.Id,
                    Name=$"User-{userAutoNo}",
                    Color=RandomColor
                };

                await OnSyncRoom(userInfo, users.Values.ToList());

            });

            Receive<UpdateUserPos>(async cmd => { 
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received SyncRoom message: {0}", jsonString);
                if(users.ContainsKey(cmd.Id))
                {
                    users[cmd.Id].PosX+=cmd.PosX;
                    users[cmd.Id].PosY+=cmd.PosY;
                }

                UpdateUserPos updateUserPos = new UpdateUserPos()
                {
                    Id = cmd.Id,
                    Name = cmd.Name,
                    PosX = cmd.PosX,
                    PosY = cmd.PosY
                };

                await OnUpdateUserPos(updateUserPos);

            });


            Receive<LeaveRoom>(async cmd => {                
                string jsonString = JsonSerializer.Serialize(cmd);
                log.Info("Received LeaveRoom message: {0}", jsonString);

                if(users.ContainsKey(cmd.UserInfo.Id))
                {
                    users.Remove(cmd.UserInfo.Id);
                    await OnLeaveRoom(cmd);
                }
            });

        }

        public async Task OnJoinRoom(RoomInfo roomInfo, UserInfo user, UpdateUserPos updateUserPos)
        {
            await hubConnection.SendAsync("OnJoinRoom", user, roomInfo, updateUserPos);
        }

        public async Task OnSyncRoom(UserInfo user, List<UpdateUserPos> updateUserPos )
        {
            await hubConnection.SendAsync("OnSyncRoom", user, updateUserPos);
        }

        public async Task OnLeaveRoom(LeaveRoom leaveRoom)
        {
            await hubConnection.SendAsync("OnLeaveRoom", leaveRoom);
        }

        public async Task OnUpdateUserPos(UpdateUserPos updatePos)
        {
            await hubConnection.SendAsync("OnUpdateUserPos", updatePos);
        }

    }
}



클라이언트 통신 코드

자바스크립트를 사용하지 않아도~ Blazor의 C#통합으로 

C#오브젝트를 변환기처리기를 작성할 필요없이 자연스럽게 브라우저내에 작동하는 프론트 코드 작성이 가능합니다.

ChatRoom.razor
    protected override async Task OnInitializedAsync()
    {

        LoginId = Guid.NewGuid().ToString();    //Fake Login ID

        hubConnection = new HubConnectionBuilder()
            .WithUrl(_navigationManager.ToAbsoluteUri("/chathub"))
            .Build();

        hubConnection.On<RoomInfo, UserInfo, UpdateUserPos>("OnJoinRoom", (room, user, pos) =>
        {
            Console.WriteLine($"WS - OnJoinRoom");
            if(user.Id == LoginId)
            {
                Name = user.Name;
                RoomName = room.Name;
                StateHasChanged();
            }
            else
            {
                BallField.AddUser(user.Id,user.Name, pos.PosX, pos.PosY);
            }
        });

        hubConnection.On<UserInfo,List<UpdateUserPos>>("OnSyncRoom", (user, updateUserPos) =>
        {
            Console.WriteLine($"WS - OnSyncRoom");
            if(user.Id == LoginId)
            {
                 foreach(var pos in updateUserPos)
                 {
                     BallField.AddUser(pos.Id, pos.Name, pos.PosX, pos.PosY);
                 }
            }
        });
        
        hubConnection.On<UpdateUserPos>("OnUpdateUserPos", (userPos) =>
        {
            BallField.UpdateUserPos(userPos);
        });

        hubConnection.On<LeaveRoom>("OnLeaveRoom", (room) =>
        {
            Console.WriteLine($"WS - OnLeaveRoom");
            BallField.RemoveUser(room.UserInfo.Id);
        });

        await hubConnection.StartAsync();

        JoinRoom sendMsg = new JoinRoom()
        {
            UserInfo = new UserInfo(){Name="user", Id= LoginId},
            RoomInfo = new RoomInfo(){Name="room1"}
        };

        SyncRoom syndMsg = new SyncRoom()
        {
            UserInfo = new UserInfo(){Name="user", Id= LoginId},
            RoomInfo = new RoomInfo(){Name="room1"}
        };

        await hubConnection.SendAsync("JoInRoom", sendMsg);

        await hubConnection.SendAsync("SyncRoom", syndMsg);

    }


애니메이션

Blazor 컴포넌트에서, Canvas(브라우저)를 제어할수 있으며

통합된 C#코드로 브라우저내의 그래픽요소를 렌더링 할수 있습니다.

ChatRoom.razor
@using Blazor.Extensions.Canvas
@using Blazor.Extensions.Canvas.Canvas2D;

    [JSInvokable]
    public async ValueTask RenderInBlazor(float timeStamp)
    {
        double fps = 1.0 / (DateTime.Now - LastRender).TotalSeconds;
        LastRender = DateTime.Now;

        await this.ctx.BeginBatchAsync();
        await this.ctx.ClearRectAsync(0, 0, BallField.Width, BallField.Height);
        await this.ctx.SetFillStyleAsync("#003366");
        await this.ctx.FillRectAsync(0, 0, BallField.Width, BallField.Height);
        await this.ctx.SetFontAsync("26px Segoe UI");
        await this.ctx.SetFillStyleAsync("#FFFFFF");
        await this.ctx.FillTextAsync("Blazor WebAssembly + HTML Canvas", 10, 30);
        await this.ctx.SetFontAsync("16px consolas");
        await this.ctx.FillTextAsync($"FPS: {fps:0.000}", 10, 50);
        await this.ctx.SetStrokeStyleAsync("#FFFFFF");

        await this.ctx.SetFontAsync("12px consolas");
        await this.ctx.SetStrokeStyleAsync("#FFFFFF");

        foreach (var ball in BallField.Balls)
        {
            await this.ctx.FillTextAsync($"{ball.Name}", ball.X -10, ball.Y -20);
            await this.ctx.BeginPathAsync();
            await this.ctx.ArcAsync(ball.X, ball.Y, ball.Radius, 0, 2 * Math.PI, false);
            await this.ctx.SetFillStyleAsync(ball.Color);
            await this.ctx.FillAsync();
            await this.ctx.StrokeAsync();
        }
        await this.ctx.EndBatchAsync();
    }







  • No labels