WCF-WPF 聊天应用程序






4.90/5 (116投票s)
一个带有文件传输功能的互联网聊天应用程序。
*编辑
一个功能更强大的版本,在我的 GitHub 上使用 WPF Dynamic Modules 实现了屏幕共享。.
另一个(性能更好)版本,使用标准的 Windows Forms 并增加了功能。
引言
此应用程序基于 WCF .NET Framework 3.0 构建。它使用双向通信和 TCP 绑定(在本文后面会讨论一些原因)。它专注于处理和控制一个必须与多个客户端建立可靠会话并尽可能长时间保持连接的 WCF 服务。
应用程序 UI 基于 WPF .NET Framework 3.0 构建,在这里我必须承认,我对 WPF 不太精通,也没有使用动画;我只是想让它看起来更好,所以选择了 WPF。
在线试用演示
必须安装 .NET 3.5 Framework。
在我们开始之前,我必须提及两个我发现的很棒的应用程序。
正如 Sacha 在 这里 所解释的,这是一个很棒的应用程序(Sacha 真的很聪明——我喜欢这个人)。
- WCF Chat by Nikola Paljetak
- WCF / WPF Chat Application by Sacha Barber (基于 Nikola 的应用程序构建)。
Nikola 和 Sacha 开发的 WCF Chat 机制是一项很棒的技术,但它在某种程度上很复杂;我第一次读 Sacha 的文章时没能理解,这就是为什么我决定一步一步地来,把它讲得简单明了。
请注意,本文使用的是客户端-服务器模式,而不是点对点模式。
在本文中,你将学习如何
- 创建服务并配置其使用 TCP 绑定。
- 托管服务并手动控制它。
- 定义服务契约、双向通信和数据契约。
- 在服务和客户端之间实现可靠会话,并长时间保持其活动状态。
- 增加最大连接数,比如到 100。
- 在客户端应用程序中处理通信状态。
- 异步调用服务操作。
- 使服务能够在线访问。
应用程序功能包括
- 连接和断开服务,可以在网络内部离线,也可以通过互联网在线连接。
- 选择昵称和头像。
- 参与公共聊天,或进行私聊。
- 知道是否有人正在输入消息。
- 知道服务是否停止或断开连接。
- 故障后重新连接。
- 文件传输(已更新)。
更新
已启用文件传输
如果您喜欢 WCF,我强烈推荐这两本书。
- Microsoft Press WCF Step by Step by John Sharp,以及
- Wrox Professional WCF Programming by Scott Klein。
技术
此服务是一个单例服务;每个开始会话的客户端都不会实例化一个新的服务实例。这是为了让单个服务能够处理多个客户端。这意味着第一个调用服务的客户端会实例化一个新的服务实例,而后续的每个调用都只是调用服务操作。服务操作是由服务实现的或函数;服务在一个接口中表示其操作,并在另一个接口中表示回调操作,要求客户端实现这些回调操作,以便在需要时能够再次调用客户端。
所以,你得知道
- 契约:Windows Communication Foundation 中的契约提供了通信客户端所需的互操作性。正是通过契约,客户端和服务才能就它们在通信往返期间使用的操作类型和结构达成一致。没有契约,就不会完成任何工作,因为不会达成任何协议(Wrox Professional WCF Programming)。在我们的例子中,我们在一个接口中定义了服务想要实现的服务操作,并在另一个称为回调接口的接口中定义了服务想要客户端实现的操作,这就是它们通过契约达成一致的地方。
- 终结点:服务必须定义至少一个终结点并为其应用绑定。
- 绑定:WCF 有一些预定义的绑定,它们非常有用,可以适应许多情况。在我们的例子中,我们将使用
netTcpBinding
(允许双向通信,如上图中的红色终结点)和mexTcpBinding
来支持发布服务元数据(如上图中的蓝色终结点)。 - 地址:服务必须为每个终结点定义一个地址(稍后我们将讨论基地址),以便可以从中调用它,这里我们有三个地址,一个在 TCP 协议上用于调用服务,一个在 TCP 协议上用于发布元数据,还有一个在 HTTP 协议上用于启用 HTTP GET 元数据。而你(开发者)是定义服务地址的人(它不与其他任何名称或路径相关,你只需定义一个新地址,如 https:///blablabla/myService.svc,它就会工作)。
一步一步构建 WCF 双向服务
步骤概述
- 创建服务程序集
- 数据契约
- 服务契约
- 回调契约
- 并发处理
- 服务实现
- 在 WPF 中创建宿主
ServiceHost
类- 配置文件中的绑定配置
- 以编程方式配置绑定
- 基地址
- 在配置文件中启用元数据配置
- 以编程方式启用元数据配置
- 可靠会话
- 最大连接数
- 在 WPF 中创建客户端
- 生成代理
- 将头像添加为嵌入资源
- 代理双向通道状态处理
- 异步调用服务操作
- 实现回调接口
- 其他事项
- 启用互联网在线访问(端口转发和防火墙)
- 自动定位服务 IP
创建服务程序集
using System.Linq;
using System.Text;
using System;
using System.Collections.Generic;
using System.ServiceModel;
namespace ServiceAssembly
{
public class ChatService
{
}
}
[DataContract]
public class Client
{
private string _name;
private int _avatarID;
private DateTime _time;
[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}
[DataMember]
public int AvatarID
{
get { return _avatarID; }
set { _avatarID = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
[DataContract]
public class Message
{
private string _sender;
private string _content;
private DateTime _time;
[DataMember]
public string Sender
{
get { return _sender; }
set { _sender = value; }
}
[DataMember]
public string Content
{
get { return _content; }
set { _content = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
[DataContract]
public class FileMessage
{
private string sender;
private string fileName;
private byte[] data;
private DateTime time;
[DataMember]
public string Sender
{
get { return sender; }
set { sender = value; }
}
[DataMember]
public string FileName
{
get { return fileName; }
set { fileName = value; }
}
[DataMember]
public byte[] Data
{
get { return data; }
set { data = value; }
}
[DataMember]
public DateTime Time
{
get { return time; }
set { time = value; }
}
}
[ServiceContract(CallbackContract = typeof(IChatCallback),
SessionMode = SessionMode.Required)]
public interface IChat
{
[OperationContract(IsInitiating = true)]
bool Connect(Client client);
[OperationContract(IsOneWay = true)]
void Say(Message msg);
[OperationContract(IsOneWay = true)]
void Whisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWriting(Client client);
[OperationContract(IsOneWay = false)]
bool SendFile(FileMessage fileMsg, Client receiver);
[OperationContract(IsOneWay = true, IsTerminating = true)]
void Disconnect(Client client);
}
为了设计服务契约,我们需要知道客户端将如何与服务交互。在这里,我们指定客户端必须启动与服务的会话,并通过调用可以真正启动和终止会话的操作来终止此会话(这就是为什么我们将 SessionMode = SessionMode.Required
设置为必需)。那么,服务如何启动或终止会话呢?答案是通过在 OperationContract
属性中设置两个属性(IsInitiating
或 IsTerminating
),WCF 运行时就会理解。操作可以是单向的(void);它的优点是客户端可以调用操作并继续其进程,而无需等待服务的回复。在我们的契约中,除了 Connect 操作外,所有操作都是单向的;它返回一个布尔值,以了解客户端是否已成功加入,或者客户端名称是否已找到并且已存在。最后一件事是引用回调接口,即客户端将实现的接口:CallbackContract = typeof(IChatCallback)
。
public interface IChatCallback
{
[OperationContract(IsOneWay = true)]
void RefreshClients(List< Client> clients);
[OperationContract(IsOneWay = true)]
void Receive(Message msg);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWritingCallback(Client client);
[OperationContract(IsOneWay = true)]
void ReceiverFile(FileMessage fileMsg, Client receiver);
[OperationContract(IsOneWay = true)]
void UserJoin(Client client);
[OperationContract(IsOneWay = true)]
void UserLeave(Client client);
}
服务负责定义这些参数并调用这些操作。
好了,这就引出了我们关于服务并发处理的讨论。WCF 服务可以通过三种方式处理并发。Single 和 Reentrant 选项使用同步模式来处理来自客户端的入站调用,但这可能会导致死锁,如果客户端尝试调用服务并等待回复,服务处理客户端请求并需要再次调用客户端,但客户端仍在等待服务的回复,从而导致死锁。Reentrant 选项会使 WCF 释放锁定,但我们将使用 Multiple 选项,它允许在另一个线程上进行调用(这很好,但正如我们之前所说,需要我们同步代码)。
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
UseSynchronizationContext = false)]
public class ChatService
{
Dictionary< Client, IChatCallback> clients =
new Dictionary< Client, IChatCallback>();
List< Client> clientList = new List< Client>();
public INewServiceCallback CurrentCallback
{
get
{
return OperationContext.Current.
GetCallbackChannel< IChatCallback>();
}
}
object syncObj = new object();
private bool SearchClientsByName(string name)
{
foreach (Client c in clients.Keys)
{
if (c.Name == name)
{
return true;
}
}
return false;
}
}
public class ChatService : IChat
{
...
#region IChat Members
public bool Connect(Client client)
{
if (!clients.ContainsValue(CurrentCallback) &&
!SearchClientsByName(client.Name))
{
lock (syncObj)
{
clients.Add(client, CurrentCallback);
clientList.Add(client);
foreach (Client key in clients.Keys)
{
IChatCallback callback = clients[key];
try
{
callback.RefreshClients(clientList);
callback.UserJoin(client);
}
catch
{
clients.Remove(key);
return false;
}
}
}
return true;
}
return false;
}
public void Say(Message msg)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.Receive(msg);
}
}
}
public void Whisper(Message msg, Client receiver)
{
foreach (Client rec in clients.Keys)
{
if (rec.Name == receiver.Name)
{
IChatCallback callback = clients[rec];
callback.ReceiveWhisper(msg, rec);
foreach (Client sender in clients.Keys)
{
if (sender.Name == msg.Sender)
{
IChatCallback senderCallback = clients[sender];
senderCallback.ReceiveWhisper(msg, rec);
return;
}
}
}
}
}
public void IsWriting(Client client)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.IsWritingCallback(client);
}
}
}
public bool SendFile(FileMessage fileMsg, Client receiver)
{
foreach (Client rcvr in clients.Keys)
{
if (rcvr.Name == receiver.Name)
{
Message msg = new Message();
msg.Sender = fileMsg.Sender;
msg.Content = "I'M SENDING FILE.. " + fileMsg.FileName;
IChatCallback rcvrCallback = clients[rcvr];
rcvrCallback.ReceiveWhisper(msg, receiver);
rcvrCallback.ReceiverFile(fileMsg, receiver);
foreach (Client sender in clients.Keys)
{
if (sender.Name == fileMsg.Sender)
{
IChatCallback sndrCallback = clients[sender];
sndrCallback.ReceiveWhisper(msg, receiver);
return true;
}
}
}
}
return false;
}
public void Disconnect(Client client)
{
foreach (Client c in clients.Keys)
{
if (client.Name == c.Name)
{
lock (syncObj)
{
this.clients.Remove(c);
this.clientList.Remove(c);
foreach (IChatCallback callback in clients.Values)
{
callback.RefreshClients(this.clientList);
callback.UserLeave(client);
}
}
return;
}
}
}
#endregion
}
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Runtime.Serialization;
namespace ServiceAssembly
{
[DataContract]
public class Client
{
private string _name;
private int _avatarID;
private DateTime _time;
[DataMember]
public string Name
{
get { return _name; }
set { _name = value; }
}
[DataMember]
public int AvatarID
{
get { return _avatarID; }
set { _avatarID = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
[DataContract]
public class Message
{
private string _sender;
private string _content;
private DateTime _time;
[DataMember]
public string Sender
{
get { return _sender; }
set { _sender = value; }
}
[DataMember]
public string Content
{
get { return _content; }
set { _content = value; }
}
[DataMember]
public DateTime Time
{
get { return _time; }
set { _time = value; }
}
}
[ServiceContract(CallbackContract = typeof(IChatCallback),
SessionMode = SessionMode.Required)]
public interface IChat
{
[OperationContract(IsInitiating = true)]
bool Connect(Client client);
[OperationContract(IsOneWay = true)]
void Say(Message msg);
[OperationContract(IsOneWay = true)]
void Whisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWriting(Client client);
[OperationContract(IsOneWay = true,
IsTerminating = true)]
void Disconnect(Client client);
}
public interface IChatCallback
{
[OperationContract(IsOneWay = true)]
void RefreshClients(List< Client> clients);
[OperationContract(IsOneWay = true)]
void Receive(Message msg);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(Message msg, Client receiver);
[OperationContract(IsOneWay = true)]
void IsWritingCallback(Client client);
[OperationContract(IsOneWay = true)]
void UserJoin(Client client);
[OperationContract(IsOneWay = true)]
void UserLeave(Client client);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
ConcurrencyMode = ConcurrencyMode.Multiple,
UseSynchronizationContext = false)]
public class ChatService : IChat
{
Dictionary< Client, IChatCallback> clients =
new Dictionary< List< Client>
clientList = new List< Client>();
public IChatCallback CurrentCallback
{
get
{
return OperationContext.Current.
GetCallbackChannel< IChatCallback>();
}
}
object syncObj = new object();
private bool SearchClientsByName(string name)
{
foreach (Client c in clients.Keys)
{
if (c.Name == name)
{
return true;
}
}
return false;
}
#region IChat Members
public bool Connect(Client client)
{
if (!clients.ContainsValue(CurrentCallback) &&
!SearchClientsByName(client.Name))
{
lock (syncObj)
{
clients.Add(client, CurrentCallback);
clientList.Add(client);
foreach (Client key in clients.Keys)
{
IChatCallback callback = clients[key];
try
{
callback.RefreshClients(clientList);
callback.UserJoin(client);
}
catch
{
clients.Remove(key);
return false;
}
}
}
return true;
}
return false;
}
public void Say(Message msg)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.Receive(msg);
}
}
}
public void Whisper(Message msg, Client receiver)
{
foreach (Client rec in clients.Keys)
{
if (rec.Name == receiver.Name)
{
IChatCallback callback = clients[rec];
callback.ReceiveWhisper(msg, rec);
foreach (Client sender in clients.Keys)
{
if (sender.Name == msg.Sender)
{
IChatCallback senderCallback = clients[sender];
senderCallback.ReceiveWhisper(msg, rec);
return;
}
}
}
}
}
public void IsWriting(Client client)
{
lock (syncObj)
{
foreach (IChatCallback callback in clients.Values)
{
callback.IsWritingCallback(client);
}
}
}
public bool SendFile(FileMessage fileMsg, Client receiver)
{
foreach (Client rcvr in clients.Keys)
{
if (rcvr.Name == receiver.Name)
{
Message msg = new Message();
msg.Sender = fileMsg.Sender;
msg.Content = "I'M SENDING FILE.. " + fileMsg.FileName;
IChatCallback rcvrCallback = clients[rcvr];
rcvrCallback.ReceiveWhisper(msg, receiver);
rcvrCallback.ReceiverFile(fileMsg, receiver);
foreach (Client sender in clients.Keys)
{
if (sender.Name == fileMsg.Sender)
{
IChatCallback sndrCallback = clients[sender];
sndrCallback.ReceiveWhisper(msg, receiver);
return true;
}
}
}
}
return false;
}
public void Disconnect(Client client)
{
foreach (Client c in clients.Keys)
{
if (client.Name == c.Name)
{
lock (syncObj)
{
this.clients.Remove(c);
this.clientList.Remove(c);
foreach (IChatCallback callback in clients.Values)
{
callback.RefreshClients(this.clientList);
callback.UserLeave(client);
}
}
return;
}
}
}
#endregion
}
}
- 在 C: 驱动器中创建一个新文件夹,并将其命名为 WCFWPFRoot。
- 启动 Visual Studio,选择 文件 > 新建 > 项目,选择 C# 类库。
- 将解决方案名称设置为 WCFWPFApp,项目名称设置为 ServiceAssembly;位置是 WCFWPFRoot 文件夹。
- 将 Class1.cs 重命名为 ChatService.cs。
- 移除
- 右键单击项目名称,选择 添加引用..,并为
System.ServiceModel
添加引用。 - 在代码中添加此行:
using System.ServiceModel;
。 - ServiceChat.cs 应如下所示
- 现在,我们将开始在此类(
ChatService
)旁边添加一些接口和其他类,都在ServiceAssembly
命名空间中。我们添加的第一个类是数据契约;数据契约表示数据传输的协议。 - 要添加数据契约,请添加对
System.Runtime.Serialization
的引用,并添加此行:using System.Runtime.Serialization;
。 - 我们的数据将是两种类型,第一种代表一个客户端,包括姓名、头像 ID 和时间;因此,我们将客户端设计如下:
- 第二个数据契约是关于消息的。我们希望客户端可以互相发送消息;因此,
Message
将包含有关发件人、内容和时间的信息。 - 下一个数据契约是
MessageFile
,用于在客户端之间传输文件。 - 在设计了将在服务和客户端之间交换的数据契约后,我们必须在一个接口中设计服务操作。
- 设计一个回调接口非常简单;只需创建一个接口来定义一些要在客户端上调用的操作。
- 到了实现服务(
IChat
接口)的时候了。我们的服务包含一个泛型字典,其键类型为客户端,值类型为IChatCallback
;因此,此泛型集合将在线客户端作为键,将每个客户端的回调对象作为值。服务还包含一个泛型列表,用于存储在线客户端(以便快速将其传递给用户),一个代表当前回调对象的公共属性,一个由服务使用的私有方法来在客户端列表中搜索客户端,以及一个用于同步我们工作的对象——这个对象用于锁定当前线程,阻止其接收来自客户端的调用,并等待当前操作完成。我们需要这个,因为在服务上下文中,你可能正在使用foreach
循环为每个回调对象发送一些信息,突然其中一个回调对象的客户端可能会断开连接,由于集合已被修改并且一个客户端已从中移除,操作将无法完成。 - 实现
IChat
接口 - 最后,这是完整的服务程序集
- 将调试模式设置为 Release 并生成项目。ServiceAssembly.dll 将被添加到 bin 文件夹下的 Release 文件夹中。
在 WPF 中创建宿主
< window title="Chat Service Host"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
x:class="WPFHost.Window1" height="300" width="300" />
< grid />
< grid.background />
< lineargradientbrush />
< gradientstop color="LightSlateGray" offset="0" />
< gradientstop color="White" offset="0.5" />
< gradientstop color="LightSlateGray" offset="0.9" />
< /lineargradientbrush />
< /grid.background />
< label name="label1" height="28" width="67"
margin="10,93,0,0" verticalalignment="Top"
horizontalalignment="Left">Local IP:</label />
< label name="label2" height="28" width="67"
margin="10,0,0,85" verticalalignment="Bottom"
horizontalalignment="Left">Listen Port:</label />
< textbox height="23" margin="76,98,108,0"
verticalalignment="Top" x:name="textBoxIP" text="localhost" />
< textbox height="23" margin="76,0,108,88"
verticalalignment="Bottom" x:name="textBoxPort"
text="7997" />
< button height="23" width="82"
margin="0,0,15,88" verticalalignment="Bottom"
horizontalalignment="Right" x:name="buttonStop"
click="buttonStop_Click">Stop</button />
< button height="23" width="82"
margin="0,96,15,0" verticalalignment="Top"
horizontalalignment="Right" x:name="buttonStart"
click="buttonStart_Click">Start</button />
< label height="28" margin="10,0,15,45"
verticalalignment="Bottom" x:name="labelStatus">Status</label />
< label height="37" margin="10,18,15,0"
verticalalignment="Top" x:name="labelTitle"
fontfamily="Jokerman" fontsize="20"
foreground="White">Chat Service</label />
< /grid />
< /window />
这是我们的宿主应用程序,它应该实例化一个 ServiceHost
对象。ServiceHost
对象将实际托管你的服务,使你能够应用绑定、添加终结点、启动或停止服务。因此,我们将开始以编程方式定义 ServiceHost
,然后使用配置文件。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ServiceModel;
using ServiceAssembly;
using System.ServiceModel.Description;
using System.Xml;
namespace WPFHost
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
ServiceHost host;
private void buttonStart_Click(object sender,
RoutedEventArgs e)
{
buttonStart.IsEnabled = false;
//Define base addresses so all
//endPoints can go under it
Uri tcpAdrs = new Uri("net.tcp://" +
textBoxIP.Text.ToString() + ":" +
textBoxPort.Text.ToString() + "/WPFHost/");
Uri httpAdrs = new Uri("http://" +
textBoxIP.Text.ToString() + ":" +
(int.Parse(textBoxPort.Text.ToString()) + 1).ToString() +
"/WPFHost/");
Uri[] baseAdresses = { tcpAdrs, httpAdrs };
host = new ServiceHost(
typeof(ServiceAssembly.ChatService), baseAdresses);
NetTcpBinding tcpBinding =
new NetTcpBinding(SecurityMode.None, true);
//Updated: to enable file transefer of 64 MB
tcpBinding.MaxBufferPoolSize = (int)67108864;
tcpBinding.MaxBufferSize = 67108864;
tcpBinding.MaxReceivedMessageSize = (int)67108864;
tcpBinding.TransferMode = TransferMode.Buffered;
tcpBinding.ReaderQuotas.MaxArrayLength = 67108864;
tcpBinding.ReaderQuotas.MaxBytesPerRead = 67108864;
tcpBinding.ReaderQuotas.MaxStringContentLength = 67108864;
tcpBinding.MaxConnections = 100;
//To maxmize MaxConnections you have
//to assign another port for mex endpoint
//and configure ServiceThrottling as well
ServiceThrottlingBehavior throttle;
throttle =
host.Description.Behaviors.Find< ServiceThrottlingBehavior>();
if (throttle == null)
{
throttle = new ServiceThrottlingBehavior();
throttle.MaxConcurrentCalls = 100;
throttle.MaxConcurrentSessions = 100;
host.Description.Behaviors.Add(throttle);
}
//Enable reliable session and keep
//the connection alive for 20 hours.
tcpBinding.ReceiveTimeout = new TimeSpan(20, 0, 0);
tcpBinding.ReliableSession.Enabled = true;
tcpBinding.ReliableSession.InactivityTimeout =
new TimeSpan(20, 0, 10);
host.AddServiceEndpoint(typeof(ServiceAssembly.IChat),
tcpBinding, "tcp");
//Define Metadata endPoint, So we can
//publish information about the service
ServiceMetadataBehavior mBehave =
new ServiceMetadataBehavior();
host.Description.Behaviors.Add(mBehave);
host.AddServiceEndpoint(typeof(IMetadataExchange),
MetadataExchangeBindings.CreateMexTcpBinding(),
"net.tcp://" + textBoxIP.Text.ToString() + ":" +
(int.Parse(textBoxPort.Text.ToString()) - 1).ToString() +
"/WPFHost/mex");
try
{
host.Open();
}
catch (Exception ex)
{
labelStatus.Content = ex.Message.ToString();
}
finally
{
if (host.State == CommunicationState.Opened)
{
labelStatus.Content = "Opened";
buttonStop.IsEnabled = true;
}
}
}
private void buttonStop_Click(object sender, RoutedEventArgs e)
{
if (host != null)
{
try
{
host.Close();
}
catch (Exception ex)
{
labelStatus.Content = ex.Message.ToString();
}
finally
{
if (host.State == CommunicationState.Closed)
{
labelStatus.Content = "Closed";
buttonStart.IsEnabled = true;
buttonStop.IsEnabled = false;
}
}
}
}
}
}
< configuration />
< system.servicemodel />
< services />
< service name="WCFService.Service"
behaviorconfiguration="behaviorConfig" />
< host />
< baseaddresses />
< add baseaddress="net.tcp://:7997/WPFHost/" />
< add baseaddress="https://:7998/WPFHost/" />
< /baseaddresses />
< /host />
< endpoint contract="ServiceAssembly.IChat" binding="netTcpBinding"
address="tcp" bindingconfiguration="tcpBinding" />
< endpoint contract="IMetadataExchange" binding="mexTcpBinding"
address="net.tcp://:7996/WcfWinFormsHost/mex" />
< /service />
< /services />
< behaviors />
< servicebehaviors />
< behavior name="behaviorConfig" />
< servicemetadata httpgetenabled="true" />
< servicedebug includeexceptiondetailinfaults="true" />
< servicethrottling maxconcurrentcalls="100"
maxconcurrentsessions="100" />
< /behavior />
< /servicebehaviors />
< /behaviors />
< bindings />
< nettcpbinding />
< binding name="tcpBinding" maxbuffersize="67108864"
maxreceivedmessagesize="67108864" maxbufferpoolsize="67108864"
transfermode="Buffered" closetimeout="00:00:10"
opentimeout="00:00:10" receivetimeout="00:20:00"
sendtimeout="00:01:00" maxconnections="100" />
< security mode="None" />
< /security />
< readerquotas maxarraylength="67108864"
maxbytesperread="67108864"
maxstringcontentlength="67108864" />
< reliablesession enabled="true"
inactivitytimeout="00:20:00" />
< /binding />
< /nettcpbinding />
< /bindings />
< /system.servicemodel />
< /configuration />
- 选择 文件 > 添加 > 新建项目..,选择 WPF 应用程序,并将其名称设置为 WPFHost。
- 添加对
System.ServiceModel
的引用。 - 添加引用并浏览服务 DLL 文件。在我们的例子中,路径是 C: > WCFWPFRoot > WCFWPFApp > ServiceAssembly > bin > Release > ServiceAssembly.dll。
- 将 window1.xaml 中的 XAML 代码替换为以下代码。
- 以编程方式:添加对
System.Runtime.Serialization
的引用。 - 将 window1.xaml.cs 中的代码替换为以下代码。
- 使用配置文件:添加新项并选择配置文件。
- 添加此代码
在 WPF 中创建客户端
正如你所见,我们提供了 mex(我们的元数据)终结点地址,它将知道并配置一个新的配置文件,其中包含 TCP 终结点地址。点击“高级”以启用异步操作和泛型列表。
maxBufferPoolSize="67108864"
maxBufferSize="67108864" maxConnections="100"
maxReceivedMessageSize="67108864">
客户端代码已注释,从上到下阅读..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using System.Reflection;
using System.ServiceModel;
using WPFClient.SVC;
using System.Collections;
using System.Windows.Threading;
using Microsoft.Win32;
namespace WPFClient
{
/// < summary>
/// Interaction logic for Window1.xaml
/// < /summary>
public partial class Window1 : Window, SVC.IChatCallback
{
//SVC holds references to the proxy and cotracts..
SVC.ChatClient proxy = null;
SVC.Client receiver = null;
SVC.Client localClient = null;
//Client will create this folder when loading
string rcvFilesPath = @"C:/WCF_Received_Files/";
//When the communication object
//turns to fault state it will
//require another thread to invoke a fault event
private delegate void FaultedInvoker();
//This will hold each online client with
//a listBoxItem to quickly handle adding
//and removing clients when they join or leave
Dictionary< ListBoxItem, SVC.Client> OnlineClients =
new Dictionary< ListBoxItem, Client>();
public Window1()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Window1_Loaded);
chatListBoxNames.SelectionChanged += new
SelectionChangedEventHandler(
chatListBoxNames_SelectionChanged);
chatTxtBoxType.KeyDown +=
new KeyEventHandler(chatTxtBoxType_KeyDown);
chatTxtBoxType.KeyUp +=
new KeyEventHandler(chatTxtBoxType_KeyUp);
}
//Service might be disconnected or stopped for any reason,
//so we have to handle the state of the communication object,
//the communication object will fire
//an event for each transitioning
//from a state to another, notice that when a connection state goes
//from opening to opened or from opened to closing state.. it can't go
//back so, if it is closed or faulted you have to set the proxy = null;
//to be able to create a proxy again and open a connection
//..
//I have made a method called HandleProxy() to handle the state
//of the connection, so in each event like opened, closed or faulted
//we will call this method, and it will switch on the connection state
//and apply a suitable reaction.
//..
//Because this events will need to be invoked on another thread
//you can do like so in WPF applications (I've got this idea from
//Sacha Barber's greate article on WCF WPF Application)
void InnerDuplexChannel_Closed(object sender, EventArgs e)
{
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new FaultedInvoker(HandleProxy));
return;
}
HandleProxy();
}
void InnerDuplexChannel_Opened(object sender, EventArgs e)
{
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new FaultedInvoker(HandleProxy));
return;
}
HandleProxy();
}
void InnerDuplexChannel_Faulted(object sender, EventArgs e)
{
if (!this.Dispatcher.CheckAccess())
{
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new FaultedInvoker(HandleProxy));
return;
}
HandleProxy();
}
#region Private Methods
/// < summary>
/// This is the most method I like, it helps us alot
/// We may can't know when a connection is lost in
/// of network failure or service stopped.
/// And also to maintain performance client doesnt know
/// that the connection will be lost when hitting the
/// disconnect button, but when a session is terminated
/// this method will be called, and it will handle everything.
/// < /summary>
private void HandleProxy()
{
if (proxy != null)
{
switch (this.proxy.State)
{
case CommunicationState.Closed:
proxy = null;
chatListBoxMsgs.Items.Clear();
chatListBoxNames.Items.Clear();
loginLabelStatus.Content = "Disconnected";
ShowChat(false);
ShowLogin(true);
loginButtonConnect.IsEnabled = true;
break;
case CommunicationState.Closing:
break;
case CommunicationState.Created:
break;
case CommunicationState.Faulted:
proxy.Abort();
proxy = null;
chatListBoxMsgs.Items.Clear();
chatListBoxNames.Items.Clear();
ShowChat(false);
ShowLogin(true);
loginLabelStatus.Content = "Disconnected";
loginButtonConnect.IsEnabled = true;
break;
case CommunicationState.Opened:
ShowLogin(false);
ShowChat(true);
chatLabelCurrentStatus.Content = "online";
chatLabelCurrentUName.Content = this.localClient.Name;
Dictionary< int, Image> images = GetImages();
Image img = images[loginComboBoxImgs.SelectedIndex];
chatCurrentImage.Source = img.Source;
break;
case CommunicationState.Opening:
break;
default:
break;
}
}
}
/// < summary>
/// This is the second important method, which creates
/// the proxy, subscribe to connection state events
/// and open a connection with the service
/// < /summary>
private void Connect()
{
if (proxy == null)
{
try
{
this.localClient = new SVC.Client();
this.localClient.Name = loginTxtBoxUName.Text.ToString();
this.localClient.AvatarID = loginComboBoxImgs.SelectedIndex;
InstanceContext context = new InstanceContext(this);
proxy = new SVC.ChatClient(context);
//As the address in the configuration file is set to localhost
//we want to change it so we can call a service in internal
//network, or over internet
string servicePath = proxy.Endpoint.ListenUri.AbsolutePath;
string serviceListenPort =
proxy.Endpoint.Address.Uri.Port.ToString();
proxy.Endpoint.Address = new EndpointAddress("net.tcp://"
+ loginTxtBoxIP.Text.ToString() + ":" +
serviceListenPort + servicePath);
proxy.Open();
proxy.InnerDuplexChannel.Faulted +=
new EventHandler(InnerDuplexChannel_Faulted);
proxy.InnerDuplexChannel.Opened +=
new EventHandler(InnerDuplexChannel_Opened);
proxy.InnerDuplexChannel.Closed +=
new EventHandler(InnerDuplexChannel_Closed);
proxy.ConnectAsync(this.localClient);
proxy.ConnectCompleted += new EventHandler<
ConnectCompletedEventArgs>(proxy_ConnectCompleted);
}
catch (Exception ex)
{
loginTxtBoxUName.Text = ex.Message.ToString();
loginLabelStatus.Content = "Offline";
loginButtonConnect.IsEnabled = true;
}
}
else
{
HandleProxy();
}
}
private void Send()
{
if (proxy != null && chatTxtBoxType.Text != "")
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
//Create message, assign its properties
SVC.Message msg = new WPFClient.SVC.Message();
msg.Sender = this.localClient.Name;
msg.Content = chatTxtBoxType.Text.ToString();
//If whisper mode is checked and an item is
//selected in the list box of clients, it will
//arrange a client object called receiver
//to whisper
if ((bool)chatCheckBoxWhisper.IsChecked)
{
if (this.receiver != null)
{
proxy.WhisperAsync(msg, this.receiver);
chatTxtBoxType.Text = "";
chatTxtBoxType.Focus();
}
}
else
{
proxy.SayAsync(msg);
chatTxtBoxType.Text = "";
chatTxtBoxType.Focus();
}
//Tell the service to tell back
//all clients that this client
//has just finished typing..
proxy.IsWritingAsync(null);
}
}
}
/// < summary>
/// This method to enable us scrolling the list box of messages
/// when a new message comes from the service..
/// < /summary>
private ScrollViewer FindVisualChild(DependencyObject obj)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is ScrollViewer)
{
return (ScrollViewer)child;
}
else
{
ScrollViewer childOfChild = FindVisualChild(child);
if (childOfChild != null)
{
return childOfChild;
}
}
}
return null;
}
/// < summary>
/// This is an important method which is called whenever
/// a message comes from the service, a client joins or
/// leaves, to return a ready item to be added in the
/// list box (either the one for messages or the one for
/// clients).
/// < /summary>
private ListBoxItem MakeItem(int imgID, string text)
{
ListBoxItem item = new ListBoxItem();
Dictionary< int, Image> images = GetImages();
Image img = images[imgID];
img.Height = 70;
img.Width = 60;
item.Content = img;
TextBlock txtblock = new TextBlock();
txtblock.Text = text;
txtblock.VerticalAlignment = VerticalAlignment.Center;
StackPanel panel = new StackPanel();
panel.Orientation = Orientation.Horizontal;
panel.Children.Add(item);
panel.Children.Add(txtblock);
ListBoxItem bigItem = new ListBoxItem();
bigItem.Content = panel;
return bigItem;
}
/// < summary>
/// This method is not used, I just put it here to help
/// you in case you want to make a rich text box and enable
/// emoticons for example.
/// Just add a richTextBox control and set
/// richTextBox.Document = MakeDocument(imgid, text);
/// < /summary>
private FlowDocument MakeDocument(int imgID, string text)
{
Dictionary< int, Image> images = GetImages();
Image img = images[imgID];
img.Height = 70;
img.Width = 60;
Block imgBlock = new BlockUIContainer(img);
Block txtBlock = new Paragraph(new Run(text));
FlowDocument doc = new FlowDocument();
doc.Blocks.Add(imgBlock);
doc.Blocks.Add(txtBlock);
doc.FlowDirection = FlowDirection.LeftToRight;
return doc;
}
/// < summary>
/// A method to retreive avatars as stream objects
/// and get an objects of type Image from the stream,
/// to return a dictionary of images and an ID for each
/// image.
/// < /summary>
private Dictionary< int, Image> GetImages()
{
List< Stream> picsStrm = new List< Stream>();
Assembly asmb = Assembly.GetExecutingAssembly();
string[] picNames = asmb.GetManifestResourceNames();
foreach (string s in picNames)
{
if (s.EndsWith(".png"))
{
Stream strm = asmb.GetManifestResourceStream(s);
if (strm != null)
{
picsStrm.Add(strm);
}
}
}
Dictionary< int, Image> images = new Dictionary< int, Image>();
int i = 0;
foreach (Stream strm in picsStrm)
{
PngBitmapDecoder decoder = new PngBitmapDecoder(strm,
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.Default);
BitmapSource bitmap = decoder.Frames[0] as BitmapSource;
Image img = new Image();
img.Source = bitmap;
img.Stretch = Stretch.UniformToFill;
images.Add(i, img);
i++;
strm.Close();
}
return images;
}
/// < summary>
/// Show or hide login controls depends on the parameter
/// < /summary>
/// < param name="show">< /param>
private void ShowLogin(bool show)
{
if (show)
{
loginButtonConnect.Visibility = Visibility.Visible;
loginComboBoxImgs.Visibility = Visibility.Visible;
loginLabelIP.Visibility = Visibility.Visible;
loginLabelStatus.Visibility = Visibility.Visible;
loginLabelTitle.Visibility = Visibility.Visible;
loginLabelUName.Visibility = Visibility.Visible;
loginPolyLine.Visibility = Visibility.Visible;
loginTxtBoxIP.Visibility = Visibility.Visible;
loginTxtBoxUName.Visibility = Visibility.Visible;
}
else
{
loginButtonConnect.Visibility = Visibility.Collapsed;
loginComboBoxImgs.Visibility = Visibility.Collapsed;
loginLabelIP.Visibility = Visibility.Collapsed;
loginLabelStatus.Visibility = Visibility.Collapsed;
loginLabelTitle.Visibility = Visibility.Collapsed;
loginLabelUName.Visibility = Visibility.Collapsed;
loginPolyLine.Visibility = Visibility.Collapsed;
loginTxtBoxIP.Visibility = Visibility.Collapsed;
loginTxtBoxUName.Visibility = Visibility.Collapsed;
}
}
/// < summary>
/// Show or hide chat controls depends on the parameter
/// < /summary>
/// < param name="show">< /param>
private void ShowChat(bool show)
{
if (show)
{
chatButtonDisconnect.Visibility = Visibility.Visible;
chatButtonSend.Visibility = Visibility.Visible;
chatCheckBoxWhisper.Visibility = Visibility.Visible;
chatCurrentImage.Visibility = Visibility.Visible;
chatLabelCurrentStatus.Visibility = Visibility.Visible;
chatLabelCurrentUName.Visibility = Visibility.Visible;
chatListBoxMsgs.Visibility = Visibility.Visible;
chatListBoxNames.Visibility = Visibility.Visible;
chatTxtBoxType.Visibility = Visibility.Visible;
chatLabelWritingMsg.Visibility = Visibility.Visible;
chatLabelSendFileStatus.Visibility = Visibility.Visible;
chatButtonOpenReceived.Visibility = Visibility.Visible;
chatButtonSendFile.Visibility = Visibility.Visible;
}
else
{
chatButtonDisconnect.Visibility = Visibility.Collapsed;
chatButtonSend.Visibility = Visibility.Collapsed;
chatCheckBoxWhisper.Visibility = Visibility.Collapsed;
chatCurrentImage.Visibility = Visibility.Collapsed;
chatLabelCurrentStatus.Visibility = Visibility.Collapsed;
chatLabelCurrentUName.Visibility = Visibility.Collapsed;
chatListBoxMsgs.Visibility = Visibility.Collapsed;
chatListBoxNames.Visibility = Visibility.Collapsed;
chatTxtBoxType.Visibility = Visibility.Collapsed;
chatLabelWritingMsg.Visibility = Visibility.Collapsed;
chatLabelSendFileStatus.Visibility = Visibility.Collapsed;
chatButtonOpenReceived.Visibility = Visibility.Collapsed;
chatButtonSendFile.Visibility = Visibility.Collapsed;
}
}
#endregion
#region UI_Events
void Window1_Loaded(object sender, RoutedEventArgs e)
{
//Create a folder named WCF_Received_Files in C directory
DirectoryInfo dir = new DirectoryInfo(rcvFilesPath);
dir.Create();
Dictionary< int, Image> images = GetImages();
//Populate images in the login comboBoc control
foreach (Image img in images.Values)
{
ListBoxItem item = new ListBoxItem();
item.Width = 90;
item.Height = 90;
item.Content = img;
loginComboBoxImgs.Items.Add(item);
}
loginComboBoxImgs.SelectedIndex = 0;
ShowChat(false);
ShowLogin(true);
}
private void chatButtonOpenReceived_Click(object sender,
RoutedEventArgs e)
{
//Open WCF_Received_Files folder in windows explorer
System.Diagnostics.Process.Start(rcvFilesPath);
}
private void chatButtonSendFile_Click(object sender,
RoutedEventArgs e)
{
if (this.receiver != null)
{
Stream strm = null;
try
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.Multiselect = false;
if (fileDialog.ShowDialog() == DialogResult.HasValue)
{
return;
}
strm = fileDialog.OpenFile();
if (strm != null)
{
byte[] buffer = new byte[(int)strm.Length];
int i = strm.Read(buffer, 0, buffer.Length);
if (i > 0)
{
SVC.FileMessage fMsg = new FileMessage();
fMsg.FileName = fileDialog.SafeFileName;
fMsg.Sender = this.localClient.Name;
fMsg.Data = buffer;
proxy.SendFileAsync(fMsg, this.receiver);
proxy.SendFileCompleted += new
EventHandler< SendFileCompletedEventArgs>
(proxy_SendFileCompleted);
chatLabelSendFileStatus.Content = "Sending...";
}
}
}
catch (Exception ex)
{
chatTxtBoxType.Text = ex.Message.ToString();
}
finally
{
if (strm != null)
{
strm.Close();
}
}
}
}
void proxy_SendFileCompleted(object sender,
SendFileCompletedEventArgs e)
{
chatLabelSendFileStatus.Content = "File Sent";
}
protected override void OnClosing(
System.ComponentModel.CancelEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Opened)
{
proxy.Disconnect(this.localClient);
//dont set proxy.Close(); because
//isTerminating = true on Disconnect()
//and this by default will call
//HandleProxy() to take care of this.
}
else
{
HandleProxy();
}
}
}
private void buttonConnect_Click(object sender,
RoutedEventArgs e)
{
loginButtonConnect.IsEnabled = false;
loginLabelStatus.Content = "Connecting..";
proxy = null;
Connect();
}
void proxy_ConnectCompleted(object sender,
ConnectCompletedEventArgs e)
{
if (e.Error != null)
{
loginLabelStatus.Foreground =
new SolidColorBrush(Colors.Red);
loginTxtBoxUName.Text = e.Error.Message.ToString();
loginButtonConnect.IsEnabled = true;
}
else if (e.Result)
{
HandleProxy();
}
else if (!e.Result)
{
loginLabelStatus.Content = "Name found";
loginButtonConnect.IsEnabled = true;
}
}
private void chatButtonSend_Click(object sender,
RoutedEventArgs e)
{
Send();
}
private void chatButtonDisconnect_Click(object sender,
RoutedEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
proxy.DisconnectAsync(this.localClient);
}
}
}
void chatTxtBoxType_KeyUp(object sender, KeyEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
if (chatTxtBoxType.Text.Length < 1)
{
proxy.IsWritingAsync(null);
}
}
}
}
void chatTxtBoxType_KeyDown(object sender, KeyEventArgs e)
{
if (proxy != null)
{
if (proxy.State == CommunicationState.Faulted)
{
HandleProxy();
}
else
{
if (e.Key == Key.Enter)
{
Send();
}
else if (chatTxtBoxType.Text.Length < 1)
{
proxy.IsWritingAsync(this.localClient);
}
}
}
}
void chatListBoxNames_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
//If user select an online client, make a client object
//to be the receiver if the user wants to whisper him.
ListBoxItem item =
chatListBoxNames.SelectedItem as ListBoxItem;
if (item != null)
{
this.receiver = this.OnlineClients[item];
}
}
#endregion
#region IChatCallback Members
public void RefreshClients(List< WPFClient.SVC.Client> clients)
{
chatListBoxNames.Items.Clear();
OnlineClients.Clear();
foreach (SVC.Client c in clients)
{
ListBoxItem item = MakeItem(c.AvatarID, c.Name);
chatListBoxNames.Items.Add(item);
OnlineClients.Add(item, c);
}
}
public void Receive(WPFClient.SVC.Message msg)
{
foreach (SVC.Client c in this.OnlineClients.Values)
{
if (c.Name == msg.Sender)
{
ListBoxItem item = MakeItem(c.AvatarID,
msg.Sender + " : " + msg.Content);
chatListBoxMsgs.Items.Add(item);
}
}
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
public void ReceiveWhisper(WPFClient.SVC.Message msg,
WPFClient.SVC.Client receiver)
{
foreach (SVC.Client c in this.OnlineClients.Values)
{
if (c.Name == msg.Sender)
{
ListBoxItem item = MakeItem(c.AvatarID,
msg.Sender + " whispers " +
receiver.Name + " : " + msg.Content);
chatListBoxMsgs.Items.Add(item);
}
}
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
public void IsWritingCallback(WPFClient.SVC.Client client)
{
if (client == null)
{
chatLabelWritingMsg.Content = "";
}
else
{
chatLabelWritingMsg.Content += client.Name +
" is writing a message.., ";
}
}
public void ReceiverFile(WPFClient.SVC.FileMessage fileMsg,
WPFClient.SVC.Client receiver)
{
try
{
FileStream fileStrm = new FileStream(rcvFilesPath +
fileMsg.FileName, FileMode.Create,
FileAccess.ReadWrite);
fileStrm.Write(fileMsg.Data, 0, fileMsg.Data.Length);
chatLabelSendFileStatus.Content =
"Received file, " + fileMsg.FileName;
}
catch (Exception ex)
{
chatLabelSendFileStatus.Content = ex.Message.ToString();
}
}
public void UserJoin(WPFClient.SVC.Client client)
{
ListBoxItem item = MakeItem(client.AvatarID,
"------------ " + client.Name + " joined chat ------------");
chatListBoxMsgs.Items.Add(item);
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
public void UserLeave(WPFClient.SVC.Client client)
{
ListBoxItem item = MakeItem(client.AvatarID,
"------------ " + client.Name + " left chat ------------");
chatListBoxMsgs.Items.Add(item);
ScrollViewer sv = FindVisualChild(chatListBoxMsgs);
sv.LineDown();
}
#endregion
}
}
- 选择 文件 > 添加 > 新建项目..,选择 WPF 应用程序,并将其名称设置为 WPFClient。
- 添加对
System.ServiceModel
的引用。 - 将 WPFHost 项目设置为启动项目,启动它,然后点击“开始”按钮运行服务。
- 将 WPFClient 项目设置为启动项目,并添加服务引用,如下图所示。
- 现在,点击 OK,你会注意到一个名为 app.config 的新文件已被添加。打开它并修改绑定以启用 64 MB 的文件传输,并将最大连接数增加到 100。
- 切换到 window1.xaml.cs 并用此代码替换其内容。
其他事项
启用互联网在线访问
- 如果你的服务器(运行服务应用程序的机器)位于网络内部,你必须转发服务监听的端口(在本例中是 7997)。为此,请登录到你的路由器配置,并转发端口 7997(帮助)。
- 你可能还想手动打开此端口,或在防火墙中为其创建规则。我使用的是 Kaspersky Internet Security。
- .
自动定位服务 IP
你可以通过将一个始终更新的 IP 地址保存在一个文本文件中,将其上传到网上,然后让你的客户端应用程序从上传的文本文件中读取 IP 地址来自动定位你的服务。
WebRequest request = WebRequest.Create("www.yourserver.com/textfile.txt");
WebResponse response = request.GetResponse();
Stream strm = response.GetResponseStream();
StreamReader reader = new StreamReader(strm);
string serviceIP = reader.ReadToEnd();
反馈
这些分布式应用程序建立在学习经验之上。我想分享代码以便更好地学习并让其他人也能学习,所以如果你遇到问题、错误或有想法,请告诉我。如果你喜欢,请投票。谢谢。