.NET 高级 TCP 套接字编程






4.99/5 (64投票s)
使用 .NET 框架和 C# 开发健壮的客户端-服务器应用程序
引言
在本文中,我将尝试演示我多年来在开发客户端-服务器应用程序时使用的一些技术。
背景
我写这篇文章的原因是我多次遇到网上非常糟糕的示例和教程,它们只演示了套接字编程的最基本内容,而没有深入探讨面向对象和类型安全通信等概念。
请看下面的代码示例
private void StartServer()
{
TcpListener server = new TcpListener(System.Net.IPAddress.Any, 8888);
//Start the server
server.Start();
Console.WriteLine("Server started. Waiting for connection...");
//Block execution until a new client is connected.
TcpClient newClient = server.AcceptTcpClient();
Console.WriteLine("New client connected!");
//Checking if new data is available to be read on the network stream
if (newClient.Available > 0)
{
//Initializing a new byte array the size of the available bytes on the network stream
byte[] readBytes = new byte[newClient.Available];
//Reading data from the stream
newClient.GetStream().Read(readBytes, 0, newClient.Available);
//Converting the byte array to string
String str = System.Text.Encoding.ASCII.GetString(readBytes);
//This should output "Hello world" to the console window
Console.WriteLine(str);
}
}
private void StartClient()
{
TcpClient client = new TcpClient();
//Connect to the server
client.Connect("localhost", 8888);
String str = "Hello world";
//Get the network stream
NetworkStream stream = client.GetStream();
//Converting string to byte array
byte[] bytesToSend = System.Text.Encoding.ASCII.GetBytes(str);
//Sending the byte array to the server
client.Client.Send(bytesToSend);
}
}
这大概就是你在网上找代码示例能得到的全部内容了。那个示例没什么问题,但在现实世界中,我希望我的应用程序有一个比在客户端和服务器之间传递一些 string
更高级的协议。例如:
- 创建客户端之间的会话
- 传递复杂对象
- 客户端的身份验证和授权
- 传输大文件
- 实时客户端连接通知
- 使用回调接收有关远程客户端某个过程进度的信息,或返回结果
解决方案结构
Sirilix.AdvancedTCP.Server
(包含服务器相关类)Sirilix.AdvancedTCP.Client
(包含客户端相关类)Sirilix.AdvancedTCP.Shared
(包含程序集之间的共享类)Sirilix.AdvancedTCP.Server.UI
(一个演示如何使用 Server 项目的 WinForms 应用程序)Sirilix.AdvancedTCP.Client.UI
(一个演示如何使用 Client 项目的 WinForms 应用程序)
客户端-服务器模型
为了在机器之间创建连接,其中一台机器必须在一个特定的端口号上监听传入的连接,这个监听例程是用 TcpListener
对象完成的。
TcpListener
可以接受连接,并返回一个 TcpClient
套接字。
TcpClient newClient = listener.AcceptTcpClient();
一旦获得了新的 TcpClient
套接字,我就可以开始在两个客户端之间发送和接收数据了。
这意味着,如果我想在两个应用程序之间通信,我只需要其中一个监听传入连接,另一个则发起连接。这是真的,但为了监听传入连接,我的应用程序将需要端口转发和防火墙异常,这对普通用户来说过于复杂。不仅如此,发起连接的应用程序还需要知道远程机器的 IP 地址或 DNS 名称。
这种方法在某些情况下可能有用,但在大多数情况下,我希望我的客户端能够通过用户名或电子邮件地址相互连接,就像 Skype 或 Team Viewer 一样。这就是为什么我需要一个中心单元充当客户端之间的桥梁。
客户端-服务器-客户端模型
这种方法要求服务器在一个列表中保存每个新客户端,此外,服务器必须知道每个消息传递的来源和目的地,以便将消息传递给正确的客户端。为了实现这一点,我们需要将每个客户端封装在一个我称之为 Receiver
的处理程序中。
Receiver
应该处理其封装客户端的所有传入和传出消息。Receiver
还应该能够将传入的消息传输到列表中的任何其他接收器,并指示其将该消息发送给其封装的客户端。Receiver
应该拥有一个唯一的 ID,例如与客户端关联的电子邮件地址或用户名,以便其他接收器可以将消息寻址到正确的接收器(客户端)。
这为我们提供了机会,不仅可以对每个客户端进行身份验证,还可以处理客户端之间所有进出数据。例如,我们可以测量每个客户端的总字节使用量,或者根据消息内容阻止特定消息。
让我们看一些 Receiver
类的重要部分。
接收器 (Receiver)
public Receiver(TcpClient client, Server server)
: this()
{
Server = server;
Client = client;
Client.ReceiveBufferSize = 1024;
Client.SendBufferSize = 1024;
}
public void Start()
{
receivingThread = new Thread(ReceivingMethod);
receivingThread.IsBackground = true;
receivingThread.Start();
sendingThread = new Thread(SendingMethod);
sendingThread.IsBackground = true;
sendingThread.Start();
}
public void SendMessage(MessageBase message)
{
MessageQueue.Add(message);
}
private void SendingMethod()
{
while (Status != StatusEnum.Disconnected)
{
if (MessageQueue.Count > 0)
{
var message = MessageQueue[0];
try
{
BinaryFormatter f = new BinaryFormatter();
f.Serialize(Client.GetStream(), message);
}
catch
{
Disconnect();
}
finally
{
MessageQueue.Remove(message);
}
}
Thread.Sleep(30);
}
}
private void ReceivingMethod()
{
while (Status != StatusEnum.Disconnected)
{
if (Client.Available > 0)
{
TotalBytesUsage += Client.Available;
try
{
BinaryFormatter f = new BinaryFormatter();
MessageBase msg = f.Deserialize(Client.GetStream()) as MessageBase;
OnMessageReceived(msg);
}
catch (Exception e)
{
Exception ex = new Exception("Unknown message received. Could not deserialize
the stream.", e);
Debug.WriteLine(ex.Message);
}
}
Thread.Sleep(30);
}
}
首先,您可以看到我在构造函数中传递了一个 TcpClient
。这个 TcpClient
是由 TcpListener
接受的。我还传递了包含所有其他接收器列表的 Server
实例,以便该接收器能够感知其同类并与它们通信。
Start
方法将启动两个线程,一个用于发送数据,另一个用于接收数据。只要接收器的 Status
保持连接状态,这些线程就会循环运行。
接收线程 (Receiving Thread)
同样,只要接收器已连接,此线程就会保持活动状态,检查 TCP Client
的 NetworkStream
是否有数据可用。然后,它将尝试将数据反序列化为 MessageBase
类型的对象,这是所有请求和响应消息的基类。稍后我们也会讨论这些消息。如果反序列化成功,它会将消息传递给 OnMessageReceived
方法。此方法将处理与接收器相关的消息。基本上,接收器只关心与协商过程相关的消息,例如对客户端进行身份验证和创建客户端之间的会话。其他消息将被旁路并直接传输到目标接收器。
* 您会注意到,在这种情况下,我使用 BinaryFormatter
来序列化和反序列化所有消息,但您也可以使用其他协议,如 SoapFormatter
或 Protobuf
,如果您需要开发一些跨平台解决方案。
发送线程 (Sending Thread)
此线程将负责发送 MessageQueue
中等待的 MessageBase
类型消息。使用队列的原因是为了确保消息不会混淆,并且一次只传递一条。
话虽如此,剩下要做的就是使用 SendMessage
方法。此方法仅将消息添加到队列,然后由 Sending Thread
实际序列化和发送消息。
现在我们已经了解了接收器的一些基本知识,让我们来看看 Server
类。
服务器
public Server(int port)
{
Receivers = new List<Receiver>();
Port = port;
}
public void Start()
{
if (!IsStarted)
{
Listener = new TcpListener(System.Net.IPAddress.Any, Port);
Listener.Start();
IsStarted = true;
WaitForConnection();
}
}
public void Stop()
{
if (IsStarted)
{
Listener.Stop();
IsStarted = false;
}
}
private void WaitForConnection()
{
Listener.BeginAcceptTcpClient(new AsyncCallback(ConnectionHandler), null);
}
private void ConnectionHandler(IAsyncResult ar)
{
lock (Receivers)
{
Receiver newClient = new Receiver(Listener.EndAcceptTcpClient(ar), this);
newClient.Start();
Receivers.Add(newClient);
OnClientConnected(newClient);
}
WaitForConnection();
}
正如您所见,服务器代码相当直接。
- 启动
Listener
。 - 等待传入连接。
- 接受连接。
- 使用新的
TcpClient
初始化一个新的Receiver
,并将其添加到Receivers
列表中。 - 启动接收器。
- 重复阶段 2。
注意:我在这里使用了 Begin/End Async 模式,因为它是处理传入连接的最佳方法。
客户端
Client
类与 Receiver
类非常相似,它也有一个发送线程、一个接收线程和一个消息队列。唯一的区别是,这个客户端是发起与监听器连接的客户端,它处理更多 MessageBase
类型的消息,并且负责向最终开发人员公开必要的方法,以防您正在开发某种 TCP 库,而我们在这个项目中就是这种情况。
让我们看看 Client
类中的一些重要部分。
public Client()
{
callBacks = new List<ResponseCallbackObject>();
MessageQueue = new List<MessageBase>();
Status = StatusEnum.Disconnected;
}
public void Connect(String address, int port)
{
Address = address;
Port = port;
TcpClient = new TcpClient();
TcpClient.Connect(Address, Port);
Status = StatusEnum.Connected;
TcpClient.ReceiveBufferSize = 1024;
TcpClient.SendBufferSize = 1024;
receivingThread = new Thread(ReceivingMethod);
receivingThread.IsBackground = true;
receivingThread.Start();
sendingThread = new Thread(SendingMethod);
sendingThread.IsBackground = true;
sendingThread.Start();
}
public void SendMessage(MessageBase message)
{
MessageQueue.Add(message);
}
private void SendingMethod()
{
while (Status != StatusEnum.Disconnected)
{
if (MessageQueue.Count > 0)
{
MessageBase m = MessageQueue[0];
BinaryFormatter f = new BinaryFormatter();
try
{
f.Serialize(TcpClient.GetStream(), m);
}
catch
{
Disconnect();
}
MessageQueue.Remove(m);
}
Thread.Sleep(30);
}
}
private void ReceivingMethod()
{
while (Status != StatusEnum.Disconnected)
{
if (TcpClient.Available > 0)
{
BinaryFormatter f = new BinaryFormatter();
MessageBase msg = f.Deserialize(TcpClient.GetStream()) as MessageBase;
OnMessageReceived(msg);
}
Thread.Sleep(30);
}
}
Connect
方法将初始化一个新的 TcpClient
对象,然后通过指定的 IP 地址或 DNS 名称和端口号与服务器建立连接。
* 注意,我为 ReceiveBufferSize 属性分配了 1024 的值。这个小调整将大大提高我们的发送和接收速度。
消息
我们的项目很酷的一点是,我们发送或接收的每一条消息本质上都是一个 C# 类,所以我们不需要解析任何复杂协议。BinaryFormatter
会处理所有编码的数据,并将我们的消息重构成相同的 C# 对象。
请记住,为了成功反序列化消息,消息类型需要位于与序列化消息起源相同的程序集中。因此,我们需要创建一个可以在客户端和接收器之间共享的类库。
为所有消息创建一个基类是个好主意。
- 它们将共享一些属性。
- 我们使用这个基类来反序列化流,并检测流上的数据是否与此基类或其任何子类不兼容。
让我们从创建 MessageBase
基类开始。
[Serializable]
public class MessageBase
{
public bool HasError { get; set; }
public Exception Exception { get; set; }
public MessageBase()
{
Exception = new Exception();
}
}
注意,我为类添加了 [Serializable
] 属性,这是 BinarryFormatter
序列化我们的类所必需的,否则它将抛出异常。
与此同时,不同消息之间共享的并不多,只是每条消息都可以返回发生了错误。
现在让我们为请求创建一个基类,也为响应创建一个基类。
[Serializable]
public class RequestMessageBase : MessageBase
{
}
[Serializable]
public class ResponseMessageBase : MessageBase
{
}
所以,我们基本上已经创建了开始做一些有趣事情所需的一切。让我们开始发送消息!第一条消息将是 ValidationRequest
消息,该消息将用于向服务器验证客户端。
[Serializable]
public class ValidationRequest : RequestMessageBase
{
public String Email { get; set; }
}
正如您所见,此消息派生自我们的请求基消息,其唯一属性是用户的电子邮件,通常我们还会添加密码属性。
现在,我们需要公开一个方法来创建一个此消息的新实例并将其添加到消息队列。让我们看看 Client
类中的 Login
方法。
public void Login(String email)
{
//Create a new validation request message
ValidationRequest request = new ValidationRequest();
request.Email = email;
//Send the message (Add it to the message queue)
SendMessage(request);
}
Login
方法会将 ValidationRequest
消息添加到消息队列,然后发送线程会拾取消息,将其序列化,并通过网络发送。然后,Receiver
会反序列化消息并将其传递给 ValidationRequestHandler
方法。
ValidationRequestHandler
方法将从服务器类中引发一个名为 OnClientValidating
的事件,这将强制前端开发人员调用 ClientValidatingEventArgs
中的一个方法。
Confirm
将发送一个 ValidationResponse
消息,并将 IsValid
属性设置为 true
。Refuse
将发送一个带有身份验证异常的 ValidationResponse
消息。
在接收器端接收到 ValidationRequest 消息
private void ReceivingMethod()
{
while (Status != StatusEnum.Disconnected)
{
if (Client.Available > 0)
{
TotalBytesUsage += Client.Available;
try
{
BinaryFormatter f = new BinaryFormatter();
MessageBase msg = f.Deserialize(Client.GetStream()) as MessageBase;
OnMessageReceived(msg);
}
catch (Exception e)
{
Exception ex = new Exception("Unknown message received. Could not deserialize
the stream.", e);
Debug.WriteLine(ex.Message);
}
}
Thread.Sleep(30);
}
}
private void OnMessageReceived(MessageBase msg)
{
Type type = msg.GetType();
if (type == typeof(ValidationRequest))
{
ValidationRequestHandler(msg as ValidationRequest);
}
}
private void ValidationRequestHandler(ValidationRequest request)
{
ValidationResponse response = new ValidationResponse(request);
EventArguments.ClientValidatingEventArgs args = new EventArguments.ClientValidatingEventArgs (() =>
{
//Confirm Action
Status = StatusEnum.Validated;
Email = request.Email;
response.IsValid = true;
SendMessage(response);
Server.OnClientValidated(this);
},
() =>
{
//Refuse Action
response.IsValid = false;
response.HasError = true;
response.Exception = new AuthenticationException("Login failed for user " + request.Emai l);
SendMessage(response);
});
args.Receiver = this;
args.Request = request;
Server.OnClientValidating(args);
}
等等!
如果 UI 开发人员正在使用 login
方法或任何其他方法,他将如何得知此消息的响应已收到?第一个想到的解决方案是从客户端类引发一个事件,但这意味着我们需要为每条消息创建一个事件。从 UI 开发者的角度来看,这也会使代码变得复杂。所以我找到了一个更优雅的解决方案来处理响应消息,使其与请求在同一个上下文中(就地)。
回调
我们都知道回调函数是如何工作的,但我们如何在远程客户端之间实现这种行为呢?
答案很简单,我们需要一种存储回调并响应收到时调用它们的方法,但同样,我们如何为给定的响应调用正确的回调?答案当然是 CallbackID
!
所以我认为我们需要进一步扩展我们的消息结构,这很明显。
让我们看看新的 MessageBase
类。
[Serializable]
public class MessageBase
{
public Guid CallbackID { get; set; }
public bool HasError { get; set; }
public Exception Exception { get; set; }
public MessageBase()
{
Exception = new Exception();
}
}
以及新的 ResponseMessageBase
。
[Serializable]
public class ResponseMessageBase : MessageBase
{
public bool DeleteCallbackAfterInvoke { get; set; }
public ResponseMessageBase(RequestMessageBase request)
{
DeleteCallbackAfterInvoke = true;
CallbackID = request.CallbackID;
}
}
现在,每条消息都有一个 CallbackID
属性,并且每条响应消息都有一个 DeleteCallbackAfterInvoke
属性。
当 DeleteCallbackAfterInvoke
设置为 false
时,回调在调用后不会从列表中删除,这将非常有用,如果我们想要创建多次响应的情况,例如分块上传大文件,或者创建远程桌面会话。
此外,ResponseMessageBase
构造函数需要一个 RequestMessageBase
,以便它可以将回调 ID 从请求复制到响应。
现在我们了解了如何实现回调,让我们看看这在 Client
类中是如何工作的。例如,我之前展示的 Login
方法现在看起来是这样的:
public void Login(String email, Action<Client, ValidationResponse> callback)
{
//Create a new validation request message
ValidationRequest request = new ValidationRequest();
request.Email = email;
//Add a callback before we send the message
AddCallback(callback, request);
//Send the message (Add it to the message queue)
SendMessage(request);
}
请注意,现在 Login
方法需要一个回调操作,并在发送消息之前调用 AddCallback
方法来添加给定的回调。这就是 AddCallback
方法。
private void AddCallback(Delegate callBack, MessageBase msg)
{
if (callBack != null)
{
Guid callbackID = Guid.NewGuid();
ResponseCallbackObject responseCallback = new ResponseCallbackObject()
{
ID = callbackID,
CallBack = callBack
};
msg.CallbackID = callbackID;
callBacks.Add(responseCallback);
}
}
AddCallback
方法需要一个 Delegate
类型和一个 MessageBase
,以便它可以构建一个新的 ResponseCallbackObject
并将其添加到回调列表中。它还会为给定的回调生成一个唯一的 ID,并将这个新 ID 附加到消息上。
现在,当这条消息在 Receiver
处收到时,我们需要检查请求的客户端是否已授权,并返回一个包含与 ValidationRequest
相同的回调 ID 的 ValidationResponse
。 这在之前看到的 ValidationRequestHandler
方法中完成。让我们看看当 ValidationResponseMessage
在 Client
类处收到时发生了什么。
private void OnMessageReceived(MessageBase msg)
{
Type type = msg.GetType();
if (msg is ResponseMessageBase)
{
InvokeMessageCallback(msg, (msg as ResponseMessageBase).DeleteCallbackAfterInvoke);
if (type == typeof(RemoteDesktopResponse))
{
RemoteDesktopResponse response = msg as RemoteDesktopResponse;
if (!response.Cancel)
{
RemoteDesktopRequest request = new RemoteDesktopRequest();
request.CallbackID = response.CallbackID;
SendMessage(request);
}
}
else if (type == typeof(FileUploadResponse))
{
FileUploadResponseHandler(msg as FileUploadResponse);
}
}
else
{
if (type == typeof(SessionRequest))
{
SessionRequestHandler(msg as SessionRequest);
}
else if (type == typeof(RemoteDesktopRequest))
{
RemoteDesktopRequestHandler(msg as RemoteDesktopRequest);
}
else if (type == typeof(TextMessageRequest))
{
TextMessageRequestHandler(msg as TextMessageRequest);
}
else if (type == typeof(FileUploadRequest))
{
FileUploadRequestHandler(msg as FileUploadRequest);
}
else if (type == typeof(DisconnectRequest))
{
OnSessionClientDisconnected();
}
}
}
我们可以看到,没有为 ValidationResponse
进行特殊处理,它只是直接传递并由 InvokeMessageCallback
方法处理。
这是 InvokeMessageCallback
方法。
private void InvokeMessageCallback(MessageBase msg, bool deleteCallback)
{
var callBackObject = callBacks.SingleOrDefault(x => x.ID == msg.CallbackID);
if (callBackObject != null)
{
if (deleteCallback)
{
callBacks.Remove(callBackObject);
}
callBackObject.CallBack.DynamicInvoke(this, msg);
}
}
此方法需要一条消息和一个确定是否在调用后删除回调的值。它将通过回调 ID 在回调列表中搜索回调。如果找到,它将使用 DynamicInvoke
调用它。DynamicInvoke
帮助我们使用多态性使用正确的响应消息类型调用回调。
现在让我们看看我们是如何从 UI 项目中使用这个架构的。
client.Login("myEmail", (senderClient, response) =>
{
if (response.IsValid)
{
Status("User Validated!");
this.InvokeUI(() =>
{
btnLogin.Enabled = false;
});
}
if (response.HasError)
{
Status(response.Exception.ToString());
}
});
通过使用 Lambda 表达式和 匿名函数,调用 login
方法并获得同一上下文中的响应非常简单。
在这个特定的项目中,我选择使用一对一会话方法。这意味着为了让客户端相互交互,其中一个必须首先发送会话请求消息,另一个必须确认请求。这两条消息是 SessionRequest
和 SessionResponse
。
会话
会话请求是一个特殊的请求,因为它需要接收器和客户端同时处理。接收器首先检查请求的客户端是否存在、已连接且未被另一个会话占用。只有这样,它才会将消息重定向到请求的客户端。然后客户端需要通过发送积极的 SessionResponse
消息来确认消息。然后,此消息将由接收器重定向到请求的客户端。整个过程就像客户端和接收器之间的握手。
让我们看看 RequestSession
方法。
public void RequestSession(String email, Action<Client, SessionResponse> callback)
{
SessionRequest request = new SessionRequest();
request.Email = email;
AddCallback(callback, request);
SendMessage(request);
}
我选择使用客户端的电子邮件地址作为服务器上的唯一标识符,此电子邮件在客户端登录服务器时注册,因此 RequestSession
方法需要请求客户端的电子邮件。
现在让我们从 UI 项目调用此方法。
client.RequestSession("client@email.com", (senderClient, args) =>
{
if (args.IsConfirmed)
{
Status("Session started with " + "client@email.com");
}
else
{
Status(args.Exception.ToString());
}
});
在消息到达接收器后,我们需要检查请求的客户端是否可用。
private void SessionRequestHandler(SessionRequest request)
{
foreach (var receiver in Server.Receivers.Where(x => x != this))
{
if (receiver.Email == request.Email)
{
if (receiver.Status == StatusEnum.Validated)
{
request.Email = this.Email;
receiver.SendMessage(request);
return;
}
}
}
SessionResponse response = new SessionResponse(request);
response.IsConfirmed = false;
response.HasError = true;
response.Exception = new Exception(request.Email +
" does not exists or not logged in or in session with another user.");
SendMessage(response);
}
一旦接收器遇到与电子邮件地址关联的另一个接收器,并且该接收器被认为是 Validated
(已登录且未被占用),它会将消息重定向到请求的客户端。然后,请求的客户端需要通知 UI 项目新的会话请求,并提供确认或拒绝会话的选项。
private void SessionRequestHandler(SessionRequest request)
{
SessionResponse response = new SessionResponse(request);
EventArguments.SessionRequestEventArguments args =
new EventArguments.SessionRequestEventArguments(() =>
{
//Confirm Session
response.IsConfirmed = true;
response.Email = request.Email;
SendMessage(response);
},
() =>
{
//Refuse Session
response.IsConfirmed = false;
response.Email = request.Email;
SendMessage(response);
});
args.Request = request;
OnSessionRequest(args);
}
protected virtual void OnSessionRequest(EventArguments.SessionRequestEventArguments args)
{
if (SessionRequest != null) SessionRequest(this, args);
}
正如您所见,我们正在引发一个带有两个方法的事件,一个用于确认,另一个用于拒绝请求。当然,我们需要在 UI 项目中注册此事件。
client.SessionRequest += client_SessionRequest; //Register for the event.
private void client_SessionRequest(Client client, EventArguments.SessionRequestEventArguments args)
{
this.InvokeUI(() =>
{
if (MessageBox.Show(this, "Session request from " +
args.Request.Email + ". Confirm request?",
this.Text, MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes)
{
args.Confirm();
Status("Session started with " + args.Request.Email);
}
else
{
args.Refuse();
}
});
}
private void InvokeUI(Action action)
{
this.Invoke(action);
}
请注意,我正在 UI 线程上执行代码,以避免在触发事件的接收线程与 UI 线程之间发生跨线程操作。
现在让我们看看当会话被确认时服务器上发生了什么。
private void SessionResponseHandler(SessionResponse response)
{
foreach (var receiver in Server.Receivers.Where(x => x != this))
{
if (receiver.Email == response.Email)
{
response.Email = this.Email;
if (response.IsConfirmed)
{
receiver.OtherSideReceiver = this;
this.OtherSideReceiver = receiver;
this.Status = StatusEnum.InSession;
receiver.Status = StatusEnum.InSession;
}
else
{
response.HasError = true;
response.Exception =
new Exception("The session request was refused by " + response.Email);
}
receiver.SendMessage(response);
return;
}
}
}
请注意,我为每个会话接收器分配了类型为 Receiver
的 OtherSideReceiver
属性,并将其状态更改为 InSession
。从此刻起,其中一个接收器发送的每条消息都将直接路由到其 OtherSideReceiver
。
因此,在我们了解了如何创建会话、发送消息和调用回调之后,我想演示一个多回调场景,例如样本解决方案中用于远程桌面查看器功能的场景。
多个回调 (Multiple Callbacks)
我所说的多回调,意思就是通过将 ResponseMessageBase
中的 DeleteCallbackAfterInvoke
属性设置为 false
,重复使用同一个回调,这将指示客户端在调用后不删除回调,并将其保留在回调列表中以备将来使用。
让我们看看这在远程桌面查看器中是如何工作的。
远程桌面请求 (Remote Desktop Request)
[Serializable]
public class RemoteDesktopRequest : RequestMessageBase
{
public int Quality { get; set; } //Quality of the captured frame.
public RemoteDesktopRequest()
{
Quality = 50;
}
}
远程桌面响应 (Remote Desktop Response)
[Serializable]
public class RemoteDesktopResponse : ResponseMessageBase
{
public RemoteDesktopResponse(RequestMessageBase request)
: base(request)
{
DeleteCallbackAfterInvoke = false; //Direct the client to keep the callback.
}
public MemoryStream FrameBytes { get; set; } //Current frame byte array.
public bool Cancel { get; set; } //When set to true will cancel the remote desktop session.
}
客户端类上的 RequestDesktop 方法
public void RequestDesktop(Action<Client, RemoteDesktopResponse> callback)
{
RemoteDesktopRequest request = new RemoteDesktopRequest();
AddCallback(callback, request);
SendMessage(request);
}
从 UI 项目调用 RequestDesktop 方法
(提供的回调方法应在每次收到新帧时调用,并更新预览面板。)
client.RequestDesktop((clientSender, response) =>
{
panelPreview.BackgroundImage = new Bitmap(response.FrameBytes); //Show the received frame.
response.FrameBytes.Dispose(); //Dispose the memory stream.
});
现在,让我们仔细看看当此请求发送到接收器端时发生了什么。
将消息重定向到 OtherSide 接收器
private void OnMessageReceived(MessageBase msg)
{
Type type = msg.GetType();
if (type == typeof(ValidationRequest))
{
ValidationRequestHandler(msg as ValidationRequest);
}
else if (type == typeof(SessionRequest))
{
SessionRequestHandler(msg as SessionRequest);
}
else if (type == typeof(SessionResponse))
{
SessionResponseHandler(msg as SessionResponse);
}
else if (type == typeof(DisconnectRequest))
{
DisconnectRequestHandler(msg as DisconnectRequest);
}
else if (OtherSideReceiver != null)
{
OtherSideReceiver.SendMessage(msg);
}
}
请注意,我们的 RemoteDesktopRequest
消息不符合任何接收器消息处理程序,因为它不需要任何服务器端处理,将被重定向到 OtherSideReceiver
(远程客户端接收器)。
现在,消息到达远程客户端后,我们需要捕获桌面,并发送一个 RemoteDesktopResponse
消息,其中包含新帧。
捕获桌面并发送帧
这个辅助类将帮助我们一次捕获一个桌面帧,按给定质量将其转换为 JPEG 格式并压缩,并返回一个包含压缩 JPEG 字节数组的 MemoryStream
。
public class RemoteDesktop
{
public static MemoryStream CaptureScreenToMemoryStream(int quality)
{
Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
Graphics g = Graphics.FromImage(bmp);
g.CopyFromScreen(new Point(0, 0), new Point(0, 0), bmp.Size);
g.Dispose();
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
ImageCodecInfo ici = null;
foreach (ImageCodecInfo codec in codecs)
{
if (codec.MimeType == "image/jpeg")
ici = codec;
}
var ep = new EncoderParameters();
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality);
MemoryStream ms = new MemoryStream();
bmp.Save(ms, ici, ep);
ms.Position = 0;
bmp.Dispose();
return ms;
}
}
这是远程客户端类上的 RemoteDesktopRequest
消息处理程序。
private void RemoteDesktopRequestHandler(RemoteDesktopRequest request)
{
RemoteDesktopResponse response = new RemoteDesktopResponse(request);
try
{
response.FrameBytes = Helpers.RemoteDesktop.CaptureScreenToMemoryStream(request.Quality);
}
catch (Exception e)
{
response.HasError = true;
response.Exception = e;
}
SendMessage(response);
}
现在,在发送 RemoteDesktopResponse
消息并被接收器重定向后,它会到达请求的客户端。
private void RemoteDesktopResponseHandler(RemoteDesktopResponse response)
{
if (!response.Cancel)
{
RemoteDesktopRequest request = new RemoteDesktopRequest();
request.CallbackID = response.CallbackID;
SendMessage(request);
}
else
{
callBacks.RemoveAll(x => x.ID == response.CallbackID);
}
}
RemoteDesktopResponse
消息处理程序只是将回调 ID 从响应复制到新的 RemoteDesktopRequest
消息,并再次将其发送到远程客户端。
请记住,RemoteDesktopResponse
消息的 DeleteCallbackAfterInvoke
属性在消息构造函数中自动设置为 false
。只有当 UI 项目中的回调方法将响应消息的 Cancel
属性设置为 true
时,回调才会被删除,例如:
client.RequestDesktop((clientSender, response) =>
{
panelPreview.BackgroundImage = new Bitmap(response.FrameBytes); //Show the received frame.
response.FrameBytes.Dispose(); //Dispose the memory stream.
response.Cancel = true; //Cancel the remote desktop session.
});
所以基本上,我们看到的是,我们可以使用相同的回调并调用它,使用类型相同的不同响应消息,比如 RemoteDesktopResponse
。非常棒!
我最后想谈的是扩展客户端类功能,提供创建和发送不属于 Sirilix.AdvancedTCP.Shared
项目的消息的能力。
使用通用消息扩展库
最后一个主题对我来说非常有趣,因为它带来了一些挑战。我希望 UI 项目能够创建和发送不属于后端库的消息。这样做最棘手的部分是,Receiver
类将不知道这些新消息,因此当它尝试反序列化它们时,BinaryFormatter
将抛出一个异常,告知它无法找到消息类型定义的程序集。这个问题的解决方案就是创建 GenericRequest
和 GenericResponse
消息。这些消息属于库,并用于封装任何派生自它们的类。
GenericRequest 消息
[Serializable]
public class GenericRequest : RequestMessageBase
{
internal MemoryStream InnerMessage { get; set; }
public GenericRequest()
{
InnerMessage = new MemoryStream();
}
public GenericRequest(RequestMessageBase request)
: this()
{
BinaryFormatter f = new BinaryFormatter();
f.Serialize(InnerMessage, request);
InnerMessage.Position = 0;
}
public GenericRequest ExtractInnerMessage()
{
BinaryFormatter f = new BinaryFormatter();
f.Binder = new AllowAllAssemblyVersionsDeserializationBinder();
return f.Deserialize(InnerMessage) as GenericRequest;
}
}
GenericResponse 消息
[Serializable]
public class GenericResponse : ResponseMessageBase
{
internal MemoryStream InnerMessage { get; set; }
public GenericResponse(GenericRequest request)
: base(request)
{
InnerMessage = new MemoryStream();
}
public GenericResponse(GenericResponse response)
: this(new GenericRequest())
{
CallbackID = response.CallbackID;
BinaryFormatter f = new BinaryFormatter();
f.Serialize(InnerMessage, response);
InnerMessage.Position = 0;
}
public GenericResponse ExtractInnerMessage()
{
BinaryFormatter f = new BinaryFormatter();
f.Binder = new AllowAllAssemblyVersionsDeserializationBinder();
return f.Deserialize(InnerMessage) as GenericResponse;
}
}
这两条消息与其他消息一样,只是它们可以通过将消息序列化到 InnerMessage
属性(类型为 MemoryStream
)来封装任何派生自它们的类。它们也可以通过调用 ExtractInnerMessage
方法来提取内部消息。此方法使用 BinaryFormatter
来反序列化内部消息,但您会注意到我将 Binder
属性设置为 AllowAllAssemblyVersionsDeserializationBinder
的新实例。之所以这样做,是因为通用消息的程序集不知道内部消息类型,也无法反序列化它。所以我找到了一种方法,通过替换默认的序列化绑定器来告知 BinaryFormatter
在哪里搜索消息类型。
用于在执行程序集(UI 项目)中定位类型的自定义序列化绑定器
public sealed class AllowAllAssemblyVersionsDeserializationBinder :
System.Runtime.Serialization.SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
Type typeToDeserialize = null;
String currentAssembly = Assembly.GetExecutingAssembly().FullName;
// In this case we are always using the current assembly
assemblyName = currentAssembly;
// Get the type using the typeName and assemblyName
typeToDeserialize = Type.GetType(String.Format("{0}, {1}",
typeName, assemblyName));
return typeToDeserialize;
}
}
为了使用通用消息,我必须对 Client
类进行一些调整。
首先,我添加了一个名为 SendGenericRequest
的新方法。
SendGenericRequest 方法
public void SendGenericRequest<T>(GenericRequest request, T callBack)
{
Guid guid = Guid.NewGuid();
request.CallbackID = guid;
GenericRequest genericRequest = new GenericRequest(request);
genericRequest.CallbackID = guid;
if (callBack != null) callBacks.Add(new ResponseCallbackObject()
{ CallBack = callBack as Delegate, ID = guid });
SendMessage(genericRequest);
}
此方法用于发送任何派生自 GenericRequestMessage
的消息,它所做的是创建一个新的 GenericRequestMessage
,并通过在消息构造函数中提供请求参数来封装“实际”消息(请参阅上面的 GenericRequest
消息结构)。我们稍后将确切了解如何使用此消息。接下来是 SendGenericResponseMessage
。
SendGenericResponse 方法
public void SendGenericResponse(GenericResponse response)
{
GenericResponse genericResponse = new GenericResponse(response);
SendMessage(genericResponse);
}
此方法提供大致相同的功能,只是它处理通用响应消息,并且不使用任何回调机制。
接下来要做的就是调整 Client
类来处理这些通用消息。需要做的第一件事是处理传入的通用请求消息,这需要一个新事件。
public event Action<Client, GenericRequest> GenericRequestReceived;
接下来,我们需要在收到通用消息时引发此事件。
收到通用请求时引发 GenericRequestReceived 事件
protected virtual void OnMessageReceived(MessageBase msg)
{
Type type = msg.GetType();
if (msg is ResponseMessageBase)
{
InvokeMessageCallback(msg, (msg as ResponseMessageBase).DeleteCallbackAfterInvoke);
if (type == typeof(RemoteDesktopResponse))
{
RemoteDesktopResponseHandler(msg as RemoteDesktopResponse);
}
else if (type == typeof(FileUploadResponse))
{
FileUploadResponseHandler(msg as FileUploadResponse);
}
}
else
{
if (type == typeof(SessionRequest))
{
SessionRequestHandler(msg as SessionRequest);
}
else if (type == typeof(RemoteDesktopRequest))
{
RemoteDesktopRequestHandler(msg as RemoteDesktopRequest);
}
else if (type == typeof(TextMessageRequest))
{
TextMessageRequestHandler(msg as TextMessageRequest);
}
else if (type == typeof(FileUploadRequest))
{
FileUploadRequestHandler(msg as FileUploadRequest);
}
else if (type == typeof(DisconnectRequest))
{
OnSessionClientDisconnected();
}
else if (type == typeof(GenericRequest))
{
OnGenericRequestReceived(msg as GenericRequest);
}
}
}
protected virtual void OnGenericRequestReceived(GenericRequest request)
{
if (GenericRequestReceived != null) GenericRequestReceived(this, request.ExtractInnerMessage());
}
请注意,我使用 ExtractInnerMessage
方法将“实际”消息作为事件参数引发事件。
当然,我们还需要处理任何收到的通用响应消息。
在调用 InvokeMessageCallback 方法之前,捕获通用响应消息并提取内部消息。
protected virtual void OnMessageReceived(MessageBase msg)
{
Type type = msg.GetType();
if (msg is ResponseMessageBase)
{
if (type == typeof(GenericResponse))
{
msg = (msg as GenericResponse).ExtractInnerMessage();
}
InvokeMessageCallback(msg, (msg as ResponseMessageBase).DeleteCallbackAfterInvoke);
if (type == typeof(RemoteDesktopResponse))
{
RemoteDesktopResponseHandler(msg as RemoteDesktopResponse);
}
else if (type == typeof(FileUploadResponse))
{
FileUploadResponseHandler(msg as FileUploadResponse);
}
}
else
{
if (type == typeof(SessionRequest))
{
SessionRequestHandler(msg as SessionRequest);
}
else if (type == typeof(RemoteDesktopRequest))
{
RemoteDesktopRequestHandler(msg as RemoteDesktopRequest);
}
else if (type == typeof(TextMessageRequest))
{
TextMessageRequestHandler(msg as TextMessageRequest);
}
else if (type == typeof(FileUploadRequest))
{
FileUploadRequestHandler(msg as FileUploadRequest);
}
else if (type == typeof(DisconnectRequest))
{
OnSessionClientDisconnected();
}
else if (type == typeof(GenericRequest))
{
OnGenericRequestReceived(msg as GenericRequest);
}
}
}
请注意,现在,我在调用回调机制之前提取响应的内部消息。这将导致回调被调用,同样,使用“实际”响应消息作为回调参数。
好的!现在让我们看看所有这些代码更改如何帮助我们扩展库的功能。
我所做的是在 UI 项目中创建了新的通用请求和响应消息,称为 CalcMessageRequest
和 CalcMessageResponse
。这两条消息只是一个示例,表示一个带有两个数字的请求,并期望得到这两个数字之和的响应。
CalcMessageRequest 派生自 GenericRequest 消息
[Serializable]
public class CalcMessageRequest : Shared.Messages.GenericRequest
{
public int A { get; set; }
public int B { get; set; }
}
CalcMessageResponse 派生自 GenericResponse 消息
[Serializable]
public class CalcMessageResponse : Shared.Messages.GenericResponse
{
public CalcMessageResponse(CalcMessageRequest request)
: base(request)
{
}
public int Result { get; set; }
}
现在,如果您查看 SendGenericRequest
方法,您会发现这是一个泛型方法,并且需要一个类型。此类型是调用回调时所需的委托类型,以便将正确的通用响应消息作为回调参数。因此,我们还需要创建一个委托,可以将其传递给 SendGenericRequest
方法。
用于调用请求回调的委托类型
public delegate void CalcMessageResponseDelegate(Client senderClient, CalcMessageResponse response);
最后,让我们从 UI 项目中使用这些消息。
发送 CalcMessageRequest,并将 CalcMessageResponseDelegate 作为回调类型提供
private void btnCalc_Click(object sender, EventArgs e)
{
MessagesExtensions.CalcMessageRequest request = new MessagesExtensions.CalcMessageRequest();
request.A = 10;
request.B = 5;
client.SendGenericRequest<MessagesExtensions.CalcMessageResponseDelegate>
(request, (clientSender,response) => {
InvokeUI(() => {
MessageBox.Show(response.Result.ToString());
});
});
}
消息已发送!现在,我们需要注册 GenericRequestReceived
事件才能处理不同的消息并发送回正确的响应,就像 Client
类一样。
注册 GenericRequestReceived 事件
client.GenericRequestReceived += client_GenericRequestReceived;
按消息类型过滤消息并返回响应
void client_GenericRequestReceived(Client client, Shared.Messages.GenericRequest msg)
{
if (msg.GetType() == typeof(MessagesExtensions.CalcMessageRequest))
{
MessagesExtensions.CalcMessageRequest request = msg as MessagesExtensions.CalcMessageRequest;
MessagesExtensions.CalcMessageResponse response =
new MessagesExtensions.CalcMessageResponse(request);
response.Result = request.A + request.B;
client.SendGenericResponse(response);
}
}
摘要
WCF(Windows Communication Foundation)框架为每个契约(接口)提供一个回调,与本文中所见的有所不同。我认为我们已经了解到,通过创建坚实的基础,然后无限制地向上构建,而无需任何复杂的框架,还有其他甚至更好的方法来开发可靠的通信应用程序。
希望您喜欢阅读。:)