[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
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!";
}
沒意外的話應該會跳出跨執行緒的問題。
非同步模組應該如何改變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 說第四版下個月就要出了。
範例下載:
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET