双工 WCF 入门指南






4.74/5 (31投票s)
本文的目的是创建一个服务,该服务可以接受来自任何客户端的消息,并将这些消息重新分发给所有已订阅的客户端;同时创建一个客户端,该客户端可以订阅服务,向其发送消息,并从服务接收不相关的消息,无论发送多少消息。
目标
本文的目的是创建一个服务,该服务可以接受来自任何客户端的消息,并将这些消息重新分发给所有已订阅的客户端;同时创建一个客户端,该客户端可以订阅服务,向其发送消息,并从服务接收不相关的消息,无论发送多少消息。结果是使用 WCF(Windows 通信基础)的客户端/服务组合。
示例起源
该示例源于我正在开发的一个系统,其中采购和消费数据需要在不同安装之间实时共享。
这实际上是我有史以来第一个 C# 应用程序,我的系统是用 C++/CLI 编写的。我之所以选择 C# 来实现此功能,是因为它在 WCF 中提供了更简洁的通信协议。
介绍示例应用程序及其来源
我选择使用一个简单的消息服务作为示例来学习,现在我将演示我是如何学习双工 WCF 通信的。服务维护一个已订阅客户端的列表,当其中任何一个客户端发送消息时,服务会将其分发给所有已订阅的客户端。每个客户端依次通过“加入”请求订阅服务,向服务发送消息,接受服务发出的任何消息,并最终可以选择离开服务。
最初我想在 C++/CLI 中使用 Web 服务,但没有可用的合适的双工通信协议,最好的方法是在每个安装上实现客户端和服务器功能。因此,我开始研究 C# 和 WCF。
我在这里提供的所有内容都源于两个优秀的灵感来源,它们对于实现我的目标都至关重要。
- [TROELSEN] - Andrew Troelsen 的“Pro C# 2010 and the .NET 4 Platform (第五版)”第 25 章
- [BARNES] - Jeff Barnes 在 CodeProject 上发表的“WCF: Duplex Operations and UI Threads”
文本中有时我可能对其中一方或另一方有所批评。但实际情况是,两者都有非常不同的目标受众,并且在如何满足这些受众方面都非常出色。
[TROELSEN] 适用于初学者,没有它我就无法开始使用 WCF,但除了提及双工活动的存在之外,它没有做太多事情。另一方面,[BARNES] 提供了一篇关于双工通信的优秀文章,但我试图将其用作 WCF 介绍时完全迷失了。
WCF - 像 ABC 一样简单
在进一步深入之前,我建议您阅读 CodeProject 上许多优秀的 WCF 入门文章之一,或者像 [TROELSEN] 这样的作者的文章
我将强调的唯一理论是 ABC 的重要性
- Address(地址)- 新服务的地址
- Binding(绑定)- 用于传输消息的传输协议,例如 HTTP/TCP
- Contract(契约)- 描述处理消息交换的方法集
服务
服务是客户端之间所有通信的中心枢纽。为了保持清晰,我已经将它与其他两个方面(宿主和客户端)分别实现在各自的解决方案中。
创建一个名为 GPH_QuickMessageServicelib.lib 的新 C# 类库项目(xxxxlib.lib 可能被认为名称过于冗长,但后续的宿主和服务也将类似命名,因此以 lib 结尾有助于在命名空间标题中保持唯一性)。
单击“确定”
关闭 Class1.cs。使用解决方案资源管理器将 Class1.cs 重命名为 GPH_QuickMessageService.cs,并在提示时选择“是”以将重命名扩展到所有引用,如下所示。
现在重新打开 GPH_QuickMessageService.cs。再次使用解决方案资源管理器,右键单击此处突出显示的 GPH_QuickMessageServiceLib,添加对 System.ServiceModel.dll 程序集的引用。
从菜单中选择“添加引用”(稍后需要将工作服务添加到客户端时将使用“添加服务引用”)。
在 GPH_QuickMessageService.cs 中包含一个 using
语句,使其现在看起来像这样
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// Reference added to make WCF happen
using System.ServiceModel;
namespace GPH_QuickMessageServiceLib
{
public class GPH_QuickMessageService
{
}
}
契约
在此示例中,我将使用一个由 IMessageServiceInbound
和 IMessageServiceCallback
这对接口表示的单一契约。这些接口的目标是允许客户端以发布和订阅的方式向服务注册,用户在此过程中向系统标识自己。该接口将:
- 接受用户的消息
- 将这些消息路由给已命名的注册用户
- 当其他用户注册或注销服务时通知用户
警告
系统允许您从方法中省略 [OperationContract]
,但如果省略,这些方法将不会通过 WCF 运行时公开 [TROELSEN]。
我们正在 GPH_QuickMessageService.cs 中定义它们。IMessageServiceInbound
接口上的 [ServiceContract]
属性通知 WCF 将要操作一个服务。添加 CallbackContract
属性会将其附加到回调接口 (IMessageServiceCallback
)。这在两个接口之间形成了一个关联,并且为了使用服务操作,客户端必须实现回调契约并托管对象以供服务调用 [BARNES]。
IMessageServiceCallback
不需要给定 ServiceContract
属性,因为 WCF 认为它是隐含的,因为它被列为 CallBackContract
。如果您愿意,可以添加它以求完整。
这是 GPH_QuickMessageService.cs 的当前状态
/// <summary>
/// GPH Quick Message Service Operations
/// </summary>
[ServiceContract(
Name = "GPH_QuickMessageService",
Namespace = "GPH_QuickMessageServiceLib",
SessionMode = SessionMode.Required,
CallbackContract = typeof(IMessageServiceCallback))]
public interface IMessageServiceInbound
{
[OperationContract]
int JoinTheConversation(string userName);
[OperationContract(IsOneWay = true)]
void ReceiveMessage(string userName,List<string> addressList, string userMessage);
[OperationContract]
int LeaveTheConversation(string userName);
}
public interface IMessageServiceCallback
{
[OperationContract(IsOneWay = true)]
void NotifyUserJoinedTheConversation(string userName);
[OperationContract(IsOneWay = true)]
void NotifyUserOfMessage(string userName, String userMessage);
[OperationContract(IsOneWay = true)]
void NotifyUserLeftTheConversation(string userName);
}
您在此契约中看到的函数处理接收加入请求、消息、离开请求,并使用三个相应的通知函数让其他客户端知道发生了什么
向契约添加行为
用 [TROELSEN] 的话说,行为允许您进一步限定宿主、服务或客户端的功能。我使用的示例来自 [BARNES],他在其中使用行为来确定并发性:“Single 是默认的并发模式,但明确指定它永远没有坏处。请注意,单线程模型仍然可以通过对回调使用单向方法来正常工作,因为它们在契约中被标记为单向。”
[ServiceBehavior(
ConcurrencyMode = ConcurrencyMode.Single,
InstanceContextMode = InstanceContextMode.PerCall)]
服务类与契约的接口
入站契约在服务类上作为接口实现
public class GPH_QuickMessageService : IMessageServiceInbound
{
}
这是传统的 Web 服务通信,但出站契约并未被遗忘。ServiceContract
定义上的 CallbackContract
属性将其绑定到入站契约。
服务实现
我将遵循 [TROELSEN] 的示例,继续在 GPH_QuickMessageService.cs 中编码,而 [BARNES] 的示例将回调的实现放在自己的 .cs 文件中 – 但在我学习使用 WCF 的实践时,我希望尽可能多地看到机制的接近性。在熟悉这些构造之后,我可以将它们模块化以遵循最佳实践。虽然 [BARNES] 以模块化格式清晰地布局,但作为初学者,我发现很难在组件之间建立心理联系。
[TROELSEN] 只有一个函数需要创建以满足他的契约,他的服务就完成了,但是我的示例遵循 [BARNES] 模型,有六个函数,其中三个是回调,这一点至关重要。还有工作要做,但是 [BARNES] 的叙述对于初学者来说很难看出下一步是什么。
首先,是保存回调通道的列表的定义
private static List<IMessageServiceCallback> _callbackList = new List<IMessageServiceCallback>();
当然,不要忘记默认构造函数
public GPH_QuickMessageService() { }
服务有六个方法或函数,其中三个处理入站消息(JoinTheConversation
、ReceiveMessage
和 LeaveTheConversation
)。客户端将有另外三个方法与之对应(NotifyUserJoinedTheConversation
、NotifyUserOfMessage
和 NotifyUserLeftTheConversation
),它们为每个已订阅的客户端接收这些消息。您在上面定义契约时已在高级别上了解过这些方法。现在我们正在实现该契约
JoinTheConversation
此方法接收客户端加入对话的请求。它创建一个 IMessageServiceCallback
类型的用户变量,检查此用户是否已在回调列表中(如果不在则添加)。最后,它通知所有用户此新用户已加入对话。
public int JoinTheConversation(string userName)
{
// Subscribe the user to the conversation
IMessageServiceCallback registeredUser =
OperationContext.Current.GetCallbackChannel<IMessageServiceCallback>();
if (!_callbackList.Contains(registeredUser))
{
_callbackList.Add(registeredUser);
}
_callbackList.ForEach(
delegate(IMessageServiceCallback callback)
{
callback.NotifyUserJoinedTheConversation(userName);
_registeredUsers++;
});
return _registeredUsers;
}
ReceiveMessage
目前这是一个非常简单的方法,它向注册用户列表广播消息及其作者。它包含一个未使用的地址列表变量。它已声明用于将来功能定制为向特定用户广播时使用。
public void ReceiveMessage(string userName,List<string> addressList, string userMessage)
{
// Notify the users of a message.
// Use an anonymous delegate and generics to do our dirty work.
_callbackList.ForEach(
delegate(IMessageServiceCallback callback)
{ callback.NotifyUserOfMessage(userName, userMessage); });
}
LeaveTheConversation
此方法从回调列表中删除一个用户,并通知其余用户被删除的用户已离开对话。
public int LeaveTheConversation(string userName)
{
// Unsubscribe the user from the conversation.
IMessageServiceCallback registeredUser =
OperationContext.Current.GetCallbackChannel<IMessageServiceCallback>();
if (_callbackList.Contains(registeredUser))
{
_callbackList.Remove(registeredUser);
_registeredUsers--;
}
// Notify everyone that user has arrived.
// Use an anonymous delegate and generics to do our dirty work.
_callbackList.ForEach(
delegate(IMessageServiceCallback callback)
{ callback.NotifyUserLeftTheConversation(userName); });
return _registeredUsers;
}
编译
您的服务库现在已准备好编译。再次提醒,上面引用的三个通知方法将在客户端实现,以接收服务分发的任何通知。
并发
在继续之前,快速说明一下并发性。OperationContract
属性上的 IsOneWay
属性在双工示例中看起来格格不入。这是一段单线程代码,因此在处理每条消息时线程会被锁定。当函数返回回复时,存在导致死锁或超时的风险。IsOneWay
属性意味着没有回复发送给发起者。其他用户发布的任何响应都以新消息的形式出现。我首选的方法是多线程应用程序,但我还需要一段时间才能实现它。所有 OneWay 函数都必须声明为 void。我的 Join 和 Leave 方法确实返回响应,但我将实现的客户端选择忽略它们。如果客户端要等待这些响应,那么它将在它们到达之前一直锁定,而无需实现某种形式的线程。
[BARNES]. 使用单向服务操作允许在仍使用单并发模式而不是重入或多重并发模式的情况下进行回调。有关并发模式的更多信息,包括如何在服务中使用它来防止锁定,请参阅 [BARNES] 示例。
托管服务
我将遵循 [TROELSEN] 的做法,使用一个新的解决方案来托管新的消息服务。最终我将为此使用 Windows 服务,但目前我仍然遵循我正在阅读的文本中的控制台应用程序。这个新的控制台应用程序将被称为 GPH_QuickMessageServiceHost。
首先,它需要 System.ServiceModel 和 GPH_MessageServiceLib。使用上面提到的解决方案资源管理器进行此操作。使用“浏览”选项卡查找 GPH_MessageServiceLib.dll。
这些也必须导入到代码文件中
using System.ServiceModel;
using GPH_QuickMessageServiceLib;
终结点(用于描述 WCF 的 ABC 或地址/绑定/契约的合并术语)等可以在源代码本身中定义,但我更喜欢为它们使用 App.Config。现在使用 项目->添加新项 添加一个。
点击“添加”完成。
将此 XML 片段添加到 App.Config 文件中的配置标签之间
<system.serviceModel>
<services>
<service name="GPH_QuickMessageServicelib.GPH_QuickMessageService">
<endpoint address ="https://:8080/GPH_QuickMessageService"
binding="basicHttpBinding"
contract="GPH_QuickMessageService.IMessageServiceInbound"/>
</service>
</services>
</system.serviceModel>
service name
指定了 DLL 名称后跟实现类名称。
endpoint address
是一个 URL 样式的引用,指向本地机器,包括实现类名。这可以是服务运行的 Web 上任何地方的任何 URL。
binding
是标准之一,在本例中是 basicHttpBinding,由使用 http 地址决定。
contract
指定实现类名称及其与服务契约的接口。
所有关键细节都包含在 <system.serviceModel>
</system.serviceModel>
标签中。
托管服务的代码
这是宿主 main 方法中让服务启动并运行的神奇部分
using (ServiceHost serviceHost = new ServiceHost(typeof(GPH_QuickMessageService)))
{
// Open the host and start listening for incoming messages.
serviceHost.Open();
// Keep the service running until the Enter key is pressed.
Console.WriteLine("The service is ready.");
Console.WriteLine("Press the Enter key to terminate service.");
Console.ReadLine();
}
这归结为定义一个 ServiceHost 类型的变量,与之前定义的 WCF 服务绑定,并调用其 open 方法。
现在可以编译并运行它来启动一个工作服务。现在它只需要一个客户端。但它没有启动。调试器快速显示
显然,在转向客户端之前还有工作要做。
将 App.Config 中的绑定更改为 wsDualHTTPbinding。
binding="wsDualHttpBinding"
service name="GPH_QuickMessageServicelib.GPH_QuickMessageService"
和
contract="GPH_QuickMessageService.IMessageServiceInbound"
需要变成
service name="GPH_QuickMessageServiceLib.GPH_QuickMessageService"
和
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"
这反过来可能会引发另一个问题
该 EXE 需要以管理员权限运行。使用资源管理器找到它,右键单击它并选择“以管理员身份运行”。当要求确认操作时,选择“是”。现在是控制台窗口
或者,一开始就以管理员模式启动 Visual Studio。
启用 MEX
MEX 或元数据交换用于定义运行时行为,以进一步调整服务的行为方式。我在这里遵循了 [TROELSEN] 的示例来实现它们——它们也出现在 [BARNES] 模型中。在此示例中,我正在为 MEX 添加一个新的终结点,一个允许 HTTP GET 访问的 WCF 行为,并且 behaviourConfiguration
属性将用于将行为与服务匹配。一个 Host 元素将为 MEX 定义基地址。
按如下方式重构 App.config 文件
<system.serviceModel>
<services>
<service name="GPH_QuickMessageServiceLib.GPH_QuickMessageService"
behaviorConfiguration = "QuickMessageServiceMEXBehavior">
<endpoint address ="service"
binding="wsDualHttpBinding"
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"/>
<!-- Enable the MEX endpoint -->
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<!-- Need to add this so MEX knows the address of our service -->
<host>
<baseAddresses>
<add baseAddress ="https://:8080/GPH_QuickMessageService"/>
</baseAddresses>
</host>
</service>
</services>
<!-- A behavior definition for MEX -->
<behaviors>
<serviceBehaviors>
<behavior name="QuickMessageServiceMEXBehavior" >
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
再次编译宿主,并在网络浏览器中查看服务:https://:8080/GPH_QuickMessageService。
构建客户端
使用 C# 构建客户端对我来说是一个新的开始,因为这是我第一次使用这种语言。
我将从创建一个新的 Windows 窗体应用程序开始,名为 GPH_QuickMessageServiceClient。
点击“确定”。然后使用解决方案资源管理器将 From1.cs 重命名为 MessageForm.cs,在询问时点击“是”。
您需要运行您的服务才能使下一步成功。在解决方案资源管理器中,右键单击解决方案,但这次选择“添加服务引用”。将 URL https://:8080/GPH_QuickMessageService 粘贴到“添加服务引用”对话框的地址框中。
点击“前往”,IDE 将尝试下载服务详细信息。这是预期的结果
我很乐意将名称保留为 ServiceReference1,但如果您愿意,可以自由更改它。根据 [TROELSEN] 的说法,这会自动添加服务的代理和任何对 WCF 程序集的引用。实际上,如果您的项目没有 App.config,它会创建一个;如果已经存在,则会添加到现有文件中;它还会创建名为 Reference.cs 的代理。代理是服务在客户端的代表。展开解决方案资源管理器中的“引用”条目以查看它们。接下来,右键单击 MessageForm.cs 并从弹出菜单中选择“查看代码”。需要一些 Using 语句
using System.ServiceModel;
using System.Threading;
using System.ServiceModel;
// Location of the proxy.
using GPH_QuickMessageServiceClient.ServiceReference1;
您还需要添加对 system.ServiceModel 的引用,就像您为服务库所做的那样。我们已经添加了它的 using 语句。
返回到表单设计器并修改 MessageForm,直到它看起来像这样
它包括四个命令按钮:brnJoin
、btnSend
、btnLeave
和 btnExit
,以及三个文本框:txtName
、txtMessageOutbound
和 txtMessageLog
,其中 txtMessageLog
需要将 Scrollbars
属性设置为 Vertical
。txtName
将需要一个 Text_Changed
事件。
我们将从禁用“加入”、“离开”和“发送”开始。“加入”将在输入名称后可用。如果该名称被接受,则名称框将变为只读,“加入”被禁用,“发送”被启用。
添加窗体加载、窗体关闭和文本更改(在 Name 上)事件,同时在每个按钮上添加点击事件。MessageForm.cs 还需要一些通信信息。
public partial class MessageForm : Form
{
public MessageForm()
扩展为
// Specify for the callback to NOT use the current synchronization context
[CallbackBehavior(
ConcurrencyMode = ConcurrencyMode.Single,
UseSynchronizationContext = false)]
public partial class MessageForm : Form, ServiceReference1.GPH_QuickMessageServiceCallback
{
private SynchronizationContext _uiSyncContext = null;
private ServiceReference1.GPH_QuickMessageServiceClient _GPH_QuickMessageService = null;
public MessageForm()
form_load
窗体加载事件需要向服务发出其存在的信号
// Capture the UI synchronization context
_uiSyncContext = SynchronizationContext.Current;
// The client callback interface must be hosted for the server to invoke the callback
// Open a connection to the message service via
// the proxy (qualifier ServiceReference1 needed due to name clash)
_GPH_QuickMessageService =
new ServiceReference1.GPH_QuickMessageServiceClient(new InstanceContext(this),
"WSDualHttpBinding_GPH_QuickMessageService");
_GPH_QuickMessageService.Open();
名称 WSDualHttpBinding_GPH_QuickMessageService
来自自动生成的 app.config。
设置表单上字段和按钮的初始状态
this.btnJoin.Enabled = false;
this.btnSend.Enabled = false;
this.btnLeave.Enabled = false;
this.btnExit.Enabled = true;
this.txtMessageOutbound.Enabled = false;
需要声明两个事件处理程序
this.txtName.TextChanged += new EventHandler(txtName_TextChanged);
this.FormClosing += new FormClosingEventHandler(MessageForm_FormClosing);
form_closing
此事件完成在窗体关闭时终止与服务关系的重要任务
_GPH_QuickMessageService.Close();
btnJoin
“加入”点击事件必须联系服务,以指示有用户即将加入对话。
_GPH_QuickMessageService.JoinTheConversation(this.txtName.Text);
它还将更改一些按钮状态
this.btnJoin.Enabled = false;
this.btnSend.Enabled = true;
this.btnLeave.Enabled = true;
this.txtMessageOutbound.Enabled = true;
btnLeave
通知服务此用户正在离开
_GPH_QuickMessageService.LeaveTheConversation(this.txtName.Text);
更新按钮/字段状态
this.btnJoin.Enabled = true;
this.btnSend.Enabled = false;
this.btnLeave.Enabled = false;
this.txtMessageOutbound.Enabled = false;
btnSend
这里没什么特别的,只是将消息转发到服务。
_GPH_QuickMessageService.ReceiveMessage(this.txtName.Text, null, this.txtMessageOutbound.Text);
btnExit
btnExit
还会向服务发送一条消息,表明用户正在离开对话,此外,它还会触发关闭表单的事件。
_GPH_QuickMessageService.LeaveTheConversation(this.txtName.Text);
this.Close();
txtName_TextChanged
此事件只会更改“加入”按钮的状态。它没有任何 WCF 方面。
if (this.txtName.Text != String.Empty)
{
this.btnJoin.Enabled = true;
}
WriteMessage
WriteMessage
用于格式化将显示在 txtMessageLog
中的消息。这是一个个人选择,没有关键的 WCF 作用。
private void WriteMessage(string message)
{
string format = this.txtMessageLog.Text.Length > 0 ? "{0}\r\n{1} {2}" : "{0}{1} {2}";
this.txtMessageLog.Text = String.Format(format, this.txtMessageLog.Text,
DateTime.Now.ToShortTimeString(), message);
this.txtMessageLog.SelectionStart = this.txtMessageLog.Text.Length - 1;
this.txtMessageLog.ScrollToCaret();
}
客户端上的入站流量
到目前为止,客户端所做的所有事情都是以非常标准的方式将消息传递给服务。现在是时候处理我们在服务库中定义契约时第一次遇到的通知方法了。这些方法将从服务接收消息并将其显示在 txtMessageLog
框中。
[BARNES] 将回调方法放在表单类中的一个独立区域中。我遵循了这种做法,但根据我的测试,这似乎只是一个风格问题。
#region GPH_QuickMessageServiceCallback Methods
NotifyUserJoinedTheConversation
NotifyUserJoinedTheConversation
将 arg_Name
作为参数。它接收加入对话的每个新客户端的名称,并将其转发给当前客户端。我参考 [BARNES] 对此方法中发生的事情进行技术解释。
UI 线程不会处理回调,但它是唯一允许更新控件的线程。因此,我们将 UI 更新调度回 UI 同步上下文。
SendOrPostCallback callback =
delegate(object state)
{
string msg_user = state.ToString();
msg_user = msg_user.ToUpper();
this.WriteMessage(String.Format("[{0}] has joined the conversation.", msg_user));
};
_uiSyncContext.Post(callback, arg_Name);
NotifyUserOfMessage
此函数从服务中获取消息的作者和内容。
SendOrPostCallback callback =
delegate(object state)
{
this.WriteMessage(String.Format("[{0}]: {1}", arg_Name.ToUpper(), arg_Message));
};
_uiSyncContext.Post(callback, arg_Name);
NotifyUserLeftTheConversation
客户端示例中的最后一个方法,它的作用是当它从服务接收到其他用户已离开对话的消息时,通知当前用户。
SendOrPostCallback callback =
delegate(object state)
{
string msg_user = state.ToString();
msg_user = msg_user.ToUpper();
this.WriteMessage(String.Format("[{0}] has left the conversation.", msg_user));
};
_uiSyncContext.Post(callback, arg_Name);
编译错误
编译所有内容后,自动生成的代理 Reference.cs 中将出现三到四个编译错误。我无法解释为什么会发生这种情况,但在三行中有四个实例需要从引用 ServiceReference1
之前删除客户端命名空间标题 GPH_QuickMessageServiceClient
。
最早报告的实例位于第 15 行的第 208 列
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(15,208): error CS0426:
The type name 'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
[System.ServiceModel.ServiceContractAttribute(
Namespace="GPH_QuickMessageServiceLib",
ConfigurationName="ServiceReference1.GPH_QuickMessageService",
CallbackContract=typeof(
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceCallback),
SessionMode=System.ServiceModel.SessionMode.Required)]
变成
[System.ServiceModel.ServiceContractAttribute(
Namespace="GPH_QuickMessageServiceLib",
ConfigurationName="ServiceReference1.GPH_QuickMessageService",
CallbackContract=typeof(ServiceReference1.GPH_QuickMessageServiceCallback),
SessionMode=System.ServiceModel.SessionMode.Required)]
在第 45 行的第 85 列
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(43,85): error CS0426:
The type name 'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
public interface GPH_QuickMessageServiceChannel :
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageService,
System.ServiceModel.IClientChannel {
变成
public interface GPH_QuickMessageServiceChannel :
ServiceReference1.GPH_QuickMessageService, System.ServiceModel.IClientChannel {
在第 48 行有两个,分别在第 125 列和第 199 列
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(48,125): error CS0426: The type name
'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
C:\SBSB\Training - C#\GPH_QuickMessageServiceClient\GPH_QuickMessageServiceClient\
Service References\ServiceReference1\Reference.cs(48,199): error CS0426:
The type name 'ServiceReference1' does not exist in the type
'GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageServiceClient'
更改
public partial class GPH_QuickMessageServiceClient :
System.ServiceModel.DuplexClientBase<
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageService>,
GPH_QuickMessageServiceClient.ServiceReference1.GPH_QuickMessageService {
变成
public partial class GPH_QuickMessageServiceClient :
System.ServiceModel.DuplexClientBase<servicereference1.gph_quickmessageservice>,
ServiceReference1.GPH_QuickMessageService {</servicereference1.gph_quickmessageservice>
其他潜在错误
到目前为止,您已经看到我故意犯了一个错误,然后通过绑定纠正了它,并清除了一个编译错误,在我撰写本文时,我还没有在不编辑自动生成的代码的情况下消除它。以下是我在此过程中遇到的其他一些错误:
第一次尝试运行宿主时,控制台窗口中出现了这个“美妙”的错误
***** Console Based WCF Host for GPH_QuickMessageService *****
Unhandled Exception: System.InvalidOperationException: Operations marked
with IsOneWay=true must not declare output parameters,
by-reference parameters or return values.
at System.ServiceModel.Description.TypeLoader.CreateOperationDescription(
ContractDescription contractDescription, MethodInfo methodInfo,
MessageDirection direction, ContractReflectionInfo reflectionInfo,
ContractDescription declaringContract)
at System.ServiceModel.Description.TypeLoader.CreateOperationDescriptions(
ContractDescription contractDescription, ContractReflectionInfo reflectionInfo,
Type contractToGetMethodsFrom, ContractDescription declaringContract,
MessageDirection direction)
at System.ServiceModel.Description.TypeLoader.CreateContractDescription(
ServiceContractAttribute contractAttr, Type contractType, Type serviceType,
ContractReflectionInfo& reflectionInfo, Object serviceImplementation)
at System.ServiceModel.Description.TypeLoader.LoadContractDescriptionHelper(
Type contractType, Type serviceType, Object serviceImplementation)
at System.ServiceModel.Description.ContractDescription.GetContract(
Type contractType, Type serviceType)
at System.ServiceModel.ServiceHost.CreateDescription(IDictionary`2& implementedContracts)
at System.ServiceModel.ServiceHostBase.InitializeDescription(
UriSchemeKeyedCollection baseAddresses)
at System.ServiceModel.ServiceHost.InitializeDescription(Type serviceType,
UriSchemeKeyedCollection baseAddresses)
at System.ServiceModel.ServiceHost..ctor(Type serviceType, Uri[] baseAddresses)
at GPH_QuickMessageServiceHost.Program.Main(String[] args) in C:\SBSB\Training -
C#\GPH_QuickMessageServiceHost\GPH_QuickMessageServiceHost\Program.cs:line 16
Press any key to continue . . .
运行调试器会突出显示 OneWay
属性的问题(上面也显示了,但不够清晰)
这是由于我在所有三个服务方法中使用了 IsOneWay
导致的——我从 Join 和 Leave 中删除了它以解决错误。这是与返回代码的冲突。
请记住主机上关于以管理员权限运行的建议。如果您忘记这样做,您将看到以下内容:
这发生在我第一次尝试时(但之后没有),因为自动生成的 app.config 在地址末尾有 /service。删除它后,通信就建立了。
如果发生上述类似情况并导致超时,报告方式如下:
从 Visual Studio 命令提示符启动调试辅助工具 WcfTestClient。在命令中包含服务的 URL。
wcftestclient https://:8080/GPH_QuickMessageService
这些操作是不可访问的。[BARNES] 服务也受到类似的限制。在我撰写本文时,我还没有弄清楚原因。
您还会发现,如果调用宿主项目不在同一解决方案中,则无法在调试器上运行服务库。为了清晰起见,我喜欢项目和解决方案之间的一对一关系,但如果我需要在调试器上运行服务库,我将不得不搁置它。
一项增强功能 - 定向消息传递
出于简单考虑,本节讨论的代码故意未包含在随附的示例中。但是,如果您完全理解上面讨论的内容,那么添加接下来的几行应该会变得可行且有趣。在这里,我将向您展示一种部署订阅者目录的方法,该目录可用于决定谁接收消息。以下是接口的外观:
消息框右侧现在有一个名为“订阅者”的检查列表,一旦第三个订阅者加入,该列表就会填充当前的订阅者列表。在上面的示例中,每个订阅者只知道在他们之后加入的订阅者。这是使用的代码
GPH_QuickMessageServiceLib
这是服务库的更改。回调契约已更改,因此 NotifyUserJoinedTheConversation
和 NotifyUserLeftTheConversation
现在包含订阅者列表。
public interface IMessageServiceCallback
{
[OperationContract(IsOneWay = true)]
void NotifyUserJoinedTheConversation(string userName, List<string> SubscriberList);
[OperationContract(IsOneWay = true)]
void NotifyUserOfMessage(string userName, String userMessage);
[OperationContract(IsOneWay = true)]
void NotifyUserLeftTheConversation(string userName,List<string> SubscriberList);
}
服务类获得了两个新属性——一个简单的列表 SubscriberList
,它将保存订阅者名称,以便在其他订阅者来去时传回给所有订阅者;以及一个字典 NotifyList
,这次它保存订阅者列表及其关联的回调 ID。我本可以探索只从 NotifyList
传回订阅者 ID,以消除对 SubscriberList
的需求,但我将它留待以后。
private static List<string> SubscriberList = new List<string>();
private static Dictionary<string,IMessageServiceCallback> NotifyList =
new Dictionary<string,IMessageServiceCallback>();// Default Constructor
JoinTheConversation
的 'if
' 语句中需要新增两行
SubscriberList.Add(userName);//Note the callback list is just a list of channels.
NotifyList.Add(userName,registeredUser);//Bind the username to the callback channel ID
更改回调语句以在传回的参数中包含 SubscriberList
。
callback.NotifyUserJoinedTheConversation(userName, SubscriberList);
LeaveTheConversation
的 'if' 语句中需要新增两个相应的行
NotifyList.Remove(userName);
SubscriberList.Remove(userName);
更改回调语句以在传回的参数中包含 SubscriberList
。
{ callback.NotifyUserLeftTheConversation(userName, SubscriberList); });
ReceiveMessage
已经完全重构,放弃了匿名委托技术。客户端的地址列表用于查找 NotifyList
,存储在那里的回调用于定位消息。这是该方法的新主体
foreach (string tmpAddr in addressList)
{
IMessageServiceCallback tmpCallback = NotifyList[tmpAddr];
tmpCallback.NotifyUserOfMessage(userName, userMessage);
}
GPH_QuickMessageServiceHost
GPH_QuickMessageServiceHost
不需要任何修改。GPH_QuickMessageServiceClient
首先,我们需要重新加载代理,所以启动服务。完成后,返回客户端,在解决方案资源管理器中右键单击 ServiceReference1 并选择“更新服务引用”。如上图所示,在窗体上添加一个勾选列表。我将其命名为 clstSubscriber
。
MessageForm.cs 需要一些工作。它获得了一个新方法来负责刷新勾选列表
private void ShowUserList(string[] _SubscriberList)
{
clstSubscriber.Items.Clear();
clstSubscriber.Items.Clear();
foreach (string _subscriber in _SubscriberList)
clstSubscriber.Items.Add(_subscriber);
}
btnSend_Click
完全重写,用于读取勾选列表并将任何选中的订阅者传递给服务。
string[] addressList = new string[clstSubscriber.CheckedItems.Count];
int i = 0;
for (int j = 0; j < clstSubscriber.CheckedItems.Count; j++ )
{
addressList[i++] = (string)clstSubscriber.CheckedItems[j];
}
_GPH_QuickMessageService.ReceiveMessage(this.txtName.Text,
addressList, this.txtMessageOutbound.Text);
NotifyUserJoinedTheConversation
和 NotifyUserLeftTheConversation
都获得了一个新的 If 语句,作为其第一个操作来填充订阅者勾选列表。
if (SubscriberList.Count() > 0)
ShowUserList(SubscriberList);
就是这样,编译所有代码——别忘了你需要从 Reference.cs 中删除编译错误,因为你更新了服务引用。你的订阅者现在将能够定位他们的消息去向。
多机部署
虽然在同一台机器上的客户端之间发送消息一切顺利,但 WCF 的真正价值在于两台机器通过互联网/内网交换消息。如果您希望使用互联网,请为运行服务的机器分配一个静态 IP 地址——但要非常小心,因为这可能会损害您的在线安全。
我选择使用我的家庭内网,并使用路由器附带的维护软件来识别连接到它的两台机器的 IP 地址。我将宿主 App.Config 的基地址中的 localhost:8080 替换为如下所示
baseAddress ="http://123.456.1.6/GPH_QuickMessageService"
我对客户端 App.Config 的终结点地址也做了相同的更改。我将客户端的副本放在另一台机器上,然后启动服务和 IP 结尾为 .6 的机器上的客户端会话,然后运行第二台机器上的另一个客户端副本。我收到一条消息,告诉我端口 80 被另一个应用程序占用,在线研究表明 IIS 是一个可能的嫌疑犯,并建议检查绑定。
我在客户端的 app.config 的 IP 地址末尾添加了 :8001
并重新部署。
但我尽了最大努力,客户端仍然告诉我目标机器主动拒绝连接。我可以通过浏览器从第二台机器看到服务,但就是无法让我的客户端连接进去。
这是由于 HTTP 绑定上的安全隐患。我在每台机器上创建了相同的 Windows 帐户(用户名/密码),将主机和客户端添加到防火墙例外中,甚至最终禁用了防火墙。仍然被拒绝。
我在线阅读到 net.tcp 绑定限制较少,因此我将 net.tcp 绑定添加到主机和客户端配置文件中,如下所示
宿主
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="GPH_QuickMessageServiceLib.GPH_QuickMessageService"
behaviorConfiguration = "QuickMessageServiceMEXBehavior">
<endpoint
address ="service"
binding="netTcpBinding"
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"/>
<endpoint
address ="service"
binding="wsDualHttpBinding"
contract="GPH_QuickMessageServiceLib.IMessageServiceInbound"/>
<!-- Enable the MEX endpoint -->
<endpoint address="mex"
binding="mexHttpBinding"
contract="IMetadataExchange" />
<!-- Need to add this so MEX knows the address of our service -->
<host>
<baseAddresses>
<add baseAddress="net.tcp://:8002/GPH_QuickMessageService/" />
<add baseAddress ="https://:8080/GPH_QuickMessageService"/>
</baseAddresses>
</host>
</service>
</services>
<!-- A behavior definition for MEX -->
<behaviors>
<serviceBehaviors>
<behavior name="QuickMessageServiceMEXBehavior" >
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
客户端
这与自动生成的配置相去甚远,以遵循 [BARNES] 模型,我曾在夏末将他的示例部署到两台机器上。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.tcp://192.168.1.4:8002/GPH_QuickMessageService/service"
binding="netTcpBinding"
contract="ServiceReference1.GPH_QuickMessageService"
name="TcpBinding" >
</endpoint>
<endpoint address="http://192.168.1.4:8080/GPH_QuickMessageService/service"
binding="wsDualHttpBinding"
contract="ServiceReference1.GPH_QuickMessageService"
name="WSDualHttpBinding_GPH_QuickMessageService" >
</endpoint>
</client>
</system.serviceModel>
</configuration>
建议这本身就足够了,但作为一种故障保护措施,我还重新编译了我的主机和服务。
请记住确保您的网络服务正在运行
完成后,我在一台机器上启动了服务和一个客户端实例,然后在另一台机器上启动了第二个客户端实例,它们能够使用我的消息窗体进行通信。
结论
接下来,我想探索使用队列的断开连接通信,以便服务及其客户端不必同时在线。我还必须研究线程及其如何提高性能,并探讨错误处理,包括错误如何中继/错误恢复。
但本次练习的主要目的是学习 WCF,以使我的票务系统的安装能够有效通信。它在这方面取得了很好的成功。我现在很难想象,当我开始记下这些笔记时,我对 WCF 还没有任何实际的工作知识。
历史
- 2012-11-06 - V1.0 - 初次提交。