[C#.NET][TCP Socket][Thread] 建立非同步模型( EAP / APM),利用 Socket 類別 控制 Alien Reader 為例

[C#.NET][TCP Socket][Thread] 建立非同步模型( EAP / APM),利用 Socket 類別 控制 Alien Reader 為例

非同步模組有兩種

1.EAP(Event-based Asynchronous Pattern):需要用+=來進行註冊,利如:this.button1.Click += new System.EventHandler(this.button1_Click);

2.APM(Asynchronous Programming Model):前綴帶有Beginxxx,Endxxx,利如:FileStream.BeginRead

 

沒有買書的人,可參考對岸的 CLR Via C# 第3版 筆記

http://www.cnblogs.com/wang_yb/archive/2011/12/01/2270792.html

image

http://www.cnblogs.com/wang_yb/archive/2011/11/29/2267790.html

 



利用上篇稍做修改 [C#.NET] 利用 Socket 同步方法 控制 Alien Reader,完成以下:

這是同步模型:


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SocketClient
{
    public class Alien
    {
        //fields

        private Socket _socketCliect = null;
        private Encoding _encode = Encoding.ASCII;
        private bool _isConnected = false;
        private AlienConfig _config;

        //properites
        public Encoding Encode
        {
            get { return _encode; }
            set { _encode = value; }
        }

        public bool IsConnected
        {
            get { return _isConnected; }
            private set { _isConnected = value; }
        }

        public AlienConfig Config
        {
            get { return _config; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException();
                }
                _config = value;
            }
        }

        //constructor
        public Alien(AlienConfig config)
        {
            this.Config = config;
        }

        //public method
        public bool Connect()
        {
            if (this.Config == null)
            {
                return false;
            }
            var ipAddress = IPAddress.Parse(this.Config.IpAddress);
            var iPEndPoint = new IPEndPoint(ipAddress, this.Config.Port);

            //Create a TCP/IP  socket.
            this._socketCliect = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            EndPoint endPoint = (EndPoint)iPEndPoint;

            this._socketCliect.Connect(endPoint);
            var connectReceive = receive();

            if (connectReceive == "***********************************************\r\n*\r\n* Alien Technology : RFID Reader \r\n*\r\n***********************************************\r\n\r\nUsername>\0")
            {
                //login
                var sendUser = this.Send(this.Config.UserName);
                var sendPassword = this.Send(this.Config.Password);
                if (sendPassword == "********\r\n\r\n\r\nAlien>\0")
                {
                    this.IsConnected = true;
                }
            }

            return this.IsConnected;
        }

        public string Send(string Data)
        {
            if (this.Config == null)
            {
                return null;
            }
            if (this.IsConnected)
            {
                return null;
            }

            var command = string.Concat(Data, "\r\n");
            while (true)
            {
                SocketError error = new SocketError();
                var sendData = this.Encode.GetBytes(command);
                this._socketCliect.Send(sendData, 0, sendData.Length, SocketFlags.None, out error);
                if (error == SocketError.Success)
                {
                    break;
                }
            }

            return receive();
        }

        private string receive()
        {
            if (this._socketCliect == null && !this._socketCliect.Connected)
            {
                return null;
            }

            var tempTimeOut = this._socketCliect.ReceiveTimeout;
            this._socketCliect.ReceiveTimeout = 1000;

            var sb = new StringBuilder();
            var buffer = new byte[1024];

            while (true)
            {
                var socketError = new SocketError();
                var receiveCount = this._socketCliect.Receive(buffer, 0, buffer.Length, SocketFlags.None, out socketError);
                Thread.Sleep(100);
                if (receiveCount == 0 || socketError != SocketError.Success)
                {
                    break;
                }
                else
                {
                    var receive = this.Encode.GetString(buffer, 0, receiveCount);
                    sb.Append(receive);
                    if (this._socketCliect.Available == 0)
                    {
                        break;
                    }
                }
            }

            this._socketCliect.ReceiveTimeout = tempTimeOut;
            var result = sb.ToString();

            return result;
        }

    }

    public class AlienConfig
    {
        private string _userName = "";//自訂
        private string _password = "";//自訂
        private string _ipAddress = "192.168.1.100";
        private int _port = 23;

        public string IpAddress
        {
            get { return _ipAddress; }
            set
            {
                IPAddress ip = null;
                if (IPAddress.TryParse(value, out ip))
                {
                    _ipAddress = value;
                }
                else
                {
                    throw new FormatException("IpAddress");
                }
            }
        }

        public int Port
        {
            get { return _port; }
            set { _port = value; }
        }

        public string UserName
        {
            get { return _userName; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("UserName");
                }
                _userName = value;
            }
        }

        public string Password
        {
            get { return _password; }
            set
            {
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("UserName");
                }
                _password = value;
            }
        }
    }
}


實現 APM 非同步模組,以 Connect 方法為例:

Step1. @Alien class 建立 BeginConnect 方法:

Step2. @Alien class 建立 EndConnect 方法:

 

在這裡我利用FCL內建的Func<>定義 Delegate,在 Alien 類別加入以下程式碼:

 


public virtual IAsyncResult BeginConnect(AsyncCallback Callback)
{
    Func<bool> connectDelegate = Connect;
    var asyncResult = (AsyncResult)connectDelegate.BeginInvoke(Callback, connectDelegate);
    return asyncResult;
}

public virtual bool EndConnect(IAsyncResult AsyncResult)
{
    Func<bool> connectDelegate = (Func<bool>)AsyncResult.AsyncState;
    var result = connectDelegate.EndInvoke(AsyncResult);
    return result;
}

 

Step3.在 Winform 裡調用:

 


private Alien _alienClient = null;

private void Form1_Load(object sender, EventArgs e)
{
    AlienConfig config = new AlienConfig();
    this._alienClient = new Alien(config);
}

private void button1_Click(object sender, EventArgs e)
{
    this._alienClient.BeginConnect(new AsyncCallback(OnConnect));
}

private void OnConnect(IAsyncResult ar)
{
    var result = this._alienClient.EndConnect(ar);
    this.Text = result ? "Connect!" : "No Connect!";
}

 

沒意外的話應該會跳出跨執行緒的問題。

image

 

 

非同步模組應該如何改變UI的值呢? CLR Via C# 第3版作者 建議使用SynchronizationContext.Post方法

SynchronizationContext.Post 很像我們在用的 this.BeginInvoke ,SynchronizationContext.Send 則是 this.Invoke

Step4.處理跨執行緒錯誤的方法:

法一:利用 Invoke:

@Winform


private void OnConnect(IAsyncResult ar)
{
    var result = this._alienClient.EndConnect(ar);
    if (this.InvokeRequired)
    {
        this.Invoke((MethodInvoker)(() =>
        {
            this.Text = result ? "Connect!" : "No Connect!";
        }));
    }
    else
    {
        this.Text = result ? "Connect!" : "No Connect!";
    }
}


法二:利用 SyncContextCallback:

@Alien class

利用 SyncContextCallback.Post,在 Alien 類別裡加入 SyncContextCallback 方法

 

 


public virtual AsyncCallback SyncContextCallback(AsyncCallback callback)
{
    SynchronizationContext sc = SynchronizationContext.Current;
    if (sc == null)
        return callback;

    return asyncResult => sc.Post(result => callback((IAsyncResult)result), asyncResult);
}

 

 

在 BeginConnect 方法裡引用SyncContextCallback


public virtual IAsyncResult BeginConnect(AsyncCallback Callback)
{
    Func<bool> connectDelegate = Connect;
    var asyncResult = (AsyncResult)connectDelegate.BeginInvoke(this.SyncContextCallback(Callback), connectDelegate);
    return asyncResult;
}

 

這兩種方法都能解決 GUI 跨執行緒的問題。

 


實現 EAP 非同步模組,以 Connect 方法為例:請參考 [C#.NET] 建議 - 應該實現標準的事件模組寫法

 

Step1. @SocketClient Namespace 建立 ConnectedEventArgs 類別:


public class ConnectedEventArgs : EventArgs
{
    private bool _isConnected;

    public bool IsConnected
    {
        get { return _isConnected; }
        set { _isConnected = value; }
    }
}

 

Step2. @Alien class 定義 event:


public event EventHandler<ConnectedEventArgs> Connected;

 

 

PS.不過在我的專案裡我是用 EventHandlerList,選一個方法來用就好,EventHandlerList 據說比較彈性,但我目前還沒有享受到太大的好處,但以下的寫法會避免重覆註冊,我覺得挺好的。

 


private EventHandlerList _events = null;
private object _eventConnect = new object();

//protected properties
protected EventHandlerList Events
{
    get
    {
        if (_events == null)
        {
            _events = new EventHandlerList();
        }
        return _events;
    }
}
public event EventHandler<ConnectedEventArgs> Connected
{
    add
    {
        var handler = this.Events[this._eventConnect];
        if (handler == null)
        {
            this.Events.AddHandler(_eventConnect, value);
        }
    }
    remove { this.Events.RemoveHandler(_eventConnect, value); }
}

 

Step3. @Alien class  定義公開 ConnectAsync 方法:

Step4. @Alien class 定義保護 OnConnected 方法:

 

 

Func<> 與 SyncContextCallback 真的是太棒了,能讓處理 UI 端的人不需要擔心跨執行緒的問題。

 


public void ConnectAsync()
{
    Func<bool> connectDelegate = this.Connect;
    AsyncCallback callback = new AsyncCallback(this.OnConnected);
    connectDelegate.BeginInvoke(this.SyncContextCallback(callback), connectDelegate);
}
protected virtual void OnConnected(IAsyncResult asyncResult)
{
    Func<bool> connectDelegate = (Func<bool>)asyncResult.AsyncState;
    var result = connectDelegate.EndInvoke(asyncResult);

    if (this.Connected != null)
    {
        this.Connected.Invoke(this, new ConnectedEventArgs() { IsConnected = result });
    }
}

 

我以前這麼寫,這得自己處理跨執行緒的問題,現在我想我得拋棄這樣的寫法了

 


public void ConnectAsync1()
{
    ThreadPool.QueueUserWorkItem(o =>
    {
        var result = this.Connect();
        var e = new ConnectedEventArgs() { IsConnected = result };
        this.OnConnected1(e);
    });
}

protected virtual void OnConnected1(ConnectedEventArgs e)
{
    if (this.Connected != null)
    {
        this.Connected.Invoke(this, e);
    }
}

 



題外話:

jeffrey richter 說第四版下個月就要出了。

http://www.wintellect.com/cs/blogs/jeffreyr/archive/2012/10/19/what-s-new-in-clr-via-c-4th-edition-as-compared-to-the-3rd-edition.aspx

 

範例下載:

AlienSocketClient.zip

 

 

 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo