首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

.NET Remoting 實作聊天伺服器與客戶端:HTTP 通道、單例模式與雙向回呼

     編輯:關於C語言

.NET Remoting 架構概覽

.NET Remoting 是 .NET Framework 早期的分散式物件通訊技術,支援:

  • 通道:HTTP (SOAP)、TCP (二進位)、IPC (共享記憶體)
  • 格式器:SoapFormatter、BinaryFormatter
  • 啟動模式:SingleCall (每呼叫一個實例)、Singleton (共用單一實例)、ClientActivated (客戶端啟動)
  • 遠端物件:繼承 MarshalByRefObject,透過 Proxy 存取
注意:.NET Remoting 已被 WCF (Windows Communication Foundation) 取代,並在 .NET Core/.NET 5+ 中移除。本文僅供維護舊系統或學習分散式概念參考。

伺服器端實作:Topic 類別 (Singleton)

using System;
using System.Collections;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using System.Windows.Forms; // 需參考 System.Windows.Forms.dll

namespace ChatServer.Remoting
{
    /// 
    /// 聊天室主題:管理用戶清單、訊息廣播、線上狀態
    /// 
    public class ChatTopic : MarshalByRefObject
    {
        private readonly ArrayList _users = new ArrayList();           // 用戶名稱
        private readonly ArrayList _textBoxes = new ArrayList();       // 客戶端 RichTextBox 參考
        private readonly ArrayList _listBoxes = new ArrayList();       // 客戶端 ListBox 參考

        /// 
        /// 用戶加入聊天室
        /// 
        /// true=成功加入,false=暱稱重複
        public bool Join(string userName, ref RichTextBox textBox, ref ListBox listBox)
        {
            lock (_users)
            {
                if (_users.Contains(userName))
                {
                    textBox.AppendText($"暱稱「{userName}」已被使用,請更換。\n");
                    return false;
                }

                // 註冊用戶
                _users.Add(userName);
                _textBoxes.Add(textBox);
                _listBoxes.Add(listBox);

                // 通知現有用戶更新線上清單
                foreach (ListBox lb in _listBoxes)
                {
                    lb.Items.Add(userName);
                }

                // 歡迎訊息僅發給新用戶
                textBox.AppendText("已連線至伺服器。\n");

                // 廣播加入訊息
                Broadcast($"{userName} 加入聊天室");
                
                // 將現有用戶加入新用戶清單
                foreach (string existingUser in _users)
                {
                    listBox.Items.Add(existingUser);
                }

                return true;
            }
        }

        /// 
        /// 用戶離開聊天室
        /// 
        public void Leave(string userName)
        {
            lock (_users)
            {
                int index = _users.IndexOf(userName);
                if (index < 0) return;

                _users.RemoveAt(index);
                _textBoxes.RemoveAt(index);
                _listBoxes.RemoveAt(index);

                // 廣播離開訊息
                Broadcast($"{userName} 離開聊天室");

                // 更新所有用戶線上清單
                foreach (ListBox lb in _listBoxes)
                {
                    lb.Items.Remove(userName);
                }
            }
        }

        /// 
        /// 廣播訊息給所有連線用戶
        /// 
        public void Broadcast(string message)
        {
            lock (_users)
            {
                Console.WriteLine($"[廣播] {message}");
                foreach (RichTextBox rtb in _textBoxes)
                {
                    try
                    {
                        rtb.AppendText(message + "\n");
                    }
                    catch
                    {
                        // 忽略個別用戶端 UI 存取失敗
                    }
                }
            }
        }

        /// 
        /// 無限期存活 (Singleton 模式必須)
        /// 
        public override object InitializeLifetimeService() => null;
    }

    /// 
    /// 伺服器啟動程式
    /// 
    public class ServerProgram
    {
        public static void Main()
        {
            const int Port = 1099;
            
            // 1. 建立 HTTP 通道 (SOAP 格式)
            var channel = new HttpChannel(Port);
            ChannelServices.RegisterChannel(channel, false); // false = 不確保安全性 (簡化測試)

            // 2. 註冊 Singleton 遠端物件
            RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ChatTopic),
                "ChatTopic.soap",           // URI 尾段
                WellKnownObjectMode.Singleton);

            Console.WriteLine($"聊天伺服器已啟動,監聽埠 {Port} (HTTP/SOAP)");
            Console.WriteLine("物件 URI: https://localhost:1099/ChatTopic.soap");
            Console.WriteLine("按 Enter 鍵停止伺服器...");
            Console.ReadLine();
        }
    }
}

客戶端實作:WinForms UI 與遠端呼叫

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;
using ChatServer.Remoting; // 參考伺服器組件或共用介面組件

namespace ChatClient
{
    public class ChatForm : Form
    {
        private readonly TextBox _nicknameBox = new TextBox { Left = 80, Top = 360, Width = 150 };
        private readonly Button _connectBtn = new Button { Text = "連線", Left = 16, Top = 360, Width = 60 };
        private readonly Button _disconnectBtn = new Button { Text = "離開", Left = 344, Top = 336, Width = 96, Enabled = false };
        private readonly Button _sendBtn = new Button { Text = "發送", Left = 136, Top = 64, Width = 72 };
        private readonly TextBox _messageBox = new TextBox { Left = 72, Top = 32, Width = 160 };
        private readonly RichTextBox _chatArea = new RichTextBox 
        { 
            Left = 24, Top = 8, Width = 288, Height = 208, 
            ReadOnly = true, BackColor = SystemColors.Info, Multiline = true 
        };
        private readonly ListBox _userList = new ListBox 
        { 
            Left = 320, Top = 0, Width = 152, Height = 329 
        };
        private readonly GroupBox _inputGroup = new GroupBox 
        { 
            Text = "發送訊息", Left = 16, Top = 232, Width = 240, Height = 104 
        };

        private HttpChannel _channel;
        private ChatTopic _topic;
        private string _userName;
        private bool _connected;

        public ChatForm()
        {
            Text = "聊天客戶端";
            ClientSize = new Size(496, 425);
            
            // 版面配置
            _inputGroup.Controls.AddRange(new Control[] { _sendBtn, _messageBox, new Label { Text = "訊息", Left = 16, Top = 32 } });
            Controls.AddRange(new Control[] 
            { 
                _nicknameBox, _connectBtn, _disconnectBtn, _sendBtn, 
                _messageBox, _chatArea, _userList, _inputGroup,
                new Label { Text = "暱稱", Left = 16, Top = 360 }
            });

            // 事件繫結
            _connectBtn.Click += ConnectBtn_Click;
            _disconnectBtn.Click += DisconnectBtn_Click;
            _sendBtn.Click += SendBtn_Click;
            FormClosing += ChatForm_FormClosing;
        }

        private void ConnectBtn_Click(object sender, EventArgs e)
        {
            if (_connected) return;
            
            string nickname = _nicknameBox.Text.Trim();
            if (string.IsNullOrEmpty(nickname))
            {
                MessageBox.Show("請輸入暱稱");
                return;
            }

            try
            {
                // 1. 建立客戶端回呼通道 (埠 0 = OS 自動指派)
                _channel = new HttpChannel(0);
                ChannelServices.RegisterChannel(_channel, false);

                // 2. 取得遠端物件 Proxy
                _topic = (ChatTopic)Activator.GetObject(
                    typeof(ChatTopic),
                    "https://localhost:1099/ChatTopic.soap");

                // 3. 呼叫遠端 Join 方法 (傳遞 UI 控制項參考)
                bool success = _topic.Join(nickname, ref _chatArea, ref _userList);

                if (!success)
                {
                    CleanupConnection();
                    return;
                }

                // 4. 連線成功,更新 UI 狀態
                _connected = true;
                _userName = nickname;
                _nicknameBox.Enabled = false;
                _connectBtn.Enabled = false;
                _disconnectBtn.Enabled = true;
                _sendBtn.Enabled = true;
                _messageBox.Enabled = true;
                Text = $"聊天客戶端 - {nickname} (已連線)";
            }
            catch (Exception ex)
            {
                MessageBox.Show($"連線失敗: {ex.Message}");
                CleanupConnection();
            }
        }

        private void DisconnectBtn_Click(object sender, EventArgs e) => Disconnect();

        private void SendBtn_Click(object sender, EventArgs e)
        {
            if (!_connected || string.IsNullOrWhiteSpace(_messageBox.Text)) return;
            
            try
            {
                _topic.Broadcast($"{_userName}: {_messageBox.Text}");
                _messageBox.Clear();
            }
            catch (Exception ex)
            {
                MessageBox.Show($"發送失敗: {ex.Message}");
                Disconnect();
            }
        }

        private void ChatForm_FormClosing(object sender, FormClosingEventArgs e) => Disconnect();

        private void Disconnect()
        {
            if (!_connected) return;
            
            try
            {
                _topic?.Leave(_userName);
            }
            catch { /* 忽略離開時錯誤 */ }
            finally
            {
                CleanupConnection();
            }
        }

        private void CleanupConnection()
        {
            _connected = false;
            _topic = null;
            _userName = null;
            
            _nicknameBox.Enabled = true;
            _connectBtn.Enabled = true;
            _disconnectBtn.Enabled = false;
            _sendBtn.Enabled = false;
            _messageBox.Enabled = false;
            _nicknameBox.Clear();
            _chatArea.Clear();
            _userList.Items.Clear();
            Text = "聊天客戶端";

            try { ChannelServices.UnregisterChannel(_channel); } catch { }
            _channel = null;
        }

        [STAThread]
        public static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new ChatForm());
        }
    }
}

關鍵技術點解析

1. MarshalByRefObject:遠端物件基底類別

  • 繼承此類別的物件不跨越應用程式域界限序列化,而是建立透明 Proxy
  • 所有方法呼叫透過 Proxy 封送至伺服器端真實物件執行
  • 回傳值、out/ref 參數自動封送回客戶端

2. Singleton 模式:共用單一伺服器實例

RemotingConfiguration.RegisterWellKnownServiceType(
    typeof(ChatTopic),
    "ChatTopic.soap",
    WellKnownObjectMode.Singleton); // 關鍵:所有客戶端共用同一實例

// 伺服器端物件必須覆寫 InitializeLifetimeService 回傳 null
public override object InitializeLifetimeService() => null; // 永不租約過期

3. 客戶端回呼通道

  • 客戶端同樣建立 HttpChannel(0) (埠 0 讓 OS 指派可用埠)
  • 此通道用於伺服器「呼叫回」客戶端 UI 控制項 (RichTextBox.AppendText, ListBox.Items.Add)
  • 本質上是雙向 Remoting:客戶端也是伺服器

4. UI 控制項跨應用程式域傳遞

// 客戶端將 UI 控制項傳址給伺服器
_topic.Join(nickname, ref _chatArea, ref _userList);

// 伺服器端直接操作客戶端 UI (透過 Proxy)
textBox.AppendText("訊息"); // 實際發生跨程序/跨機器呼叫
警告:此做法緊耦合伺服器與客戶端 UI 技術 (WinForms),違反分層架構。生產環境應定義純資料介面 (如 IChatClientCallback),客戶端實作介面並透過事件更新 UI。

5. 執行緒安全

  • 伺服器方法使用 lock (_users) 保護共用集合
  • WinForms 控制項非執行緒安全,跨執行緒存取需用 Invoke/BeginInvoke
  • Remoting 預設使用 ThreadPool 執行緒,直接操作 UI 會導致競態條件
  • 改進:伺服器僅管理資料,客戶端透過事件/輪詢更新 UI

建置與執行步驟

  1. 建立方案,包含三個專案:
    • ChatCommon (類別庫):定義介面/共用型別 (可選,簡化則直接參考伺服器組件)
    • ChatServer (主控台應用程式):參考 ChatCommon、System.Runtime.Remoting、System.Windows.Forms
    • ChatClient (Windows Forms 應用程式):參考 ChatCommon、System.Runtime.Remoting
  2. 建置方案
  3. 先執行 ChatServer.exe,確認主控台顯示「聊天伺服器已啟動」
  4. 執行多個 ChatClient.exe 實例,輸入不同暱稱連線
  5. 在任一客戶端發送訊息,觀察所有客戶端即時接收

現代替代方案

技術優點適用場景
SignalR (ASP.NET Core)WebSocket/長輪詢自動切換、支援 Scale-out、JS/.NET 客戶端即時 Web 應用、聊天室、儀表板
gRPCHTTP/2、Protobuf 序列化、強型別契約、雙向串流微服務通訊、高效能內部 API
WebSocket (System.Net.WebSockets)標準雙向通訊、低延遲自訂即時協定
WCF (NetTcpBinding / WSDualHttpBinding)企業級功能、安全性、交易、可靠傳訊舊系統遷移、企業整合

總結

.NET Remoting 透過 MarshalByRefObject、HTTP 通道、Singleton 模式實現分散式聊天室。核心流程:伺服器註冊遠端物件 → 客戶端建立回呼通道 → 取得 Proxy → 呼叫 Join 註冊 UI → 伺服器廣播訊息回呼客戶端 UI。雖已淘汰,但其 Proxy/Stub、封送、租約、通道等概念為理解現代 gRPC、SignalR、微服務通訊奠定基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved