2009/07/31

P2P by using WCF

P2P by using WCF
這是一個用C# 寫的應用,主要是練習 P2P,使用的是 .NET 3.0 推出的 WCF,可以參考 A simple peer to peer chat application using WCF netPeerTcpBinding。若對開啟 C# 應用有興趣的人,可以參考 Button Controls。P2P 可以想成一個應用程式本身具有 Client 及 Server。

Server

在 Server 端需要額外引入三個 namespace:

* System.ServiceModel;
* System.ServiceModel.Channels;
* System.ServiceModel.PeerResolvers;

完整程式碼如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.PeerResolvers;

namespace ChatServer
{
public partial class ChatServer : Form
{
private CustomPeerResolverService cprs;
private ServiceHost host;

public ChatServer()
{
InitializeComponent();
btnStop.Enabled = false;
}

private void btnStart_Click(object sender, EventArgs e)
{
try
{
cprs = new CustomPeerResolverService();
cprs.RefreshInterval = TimeSpan.FromSeconds(5);
host = new ServiceHost(cprs);
cprs.ControlShape = true;
cprs.Open();
host.Open(TimeSpan.FromDays(1000000));
lblMessage.Text = "Server started successfully.";
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
btnStart.Enabled = false;
btnStop.Enabled = true;
}
}

private void btnStop_Click(object sender, EventArgs e)
{
try
{
cprs.Close();
host.Close();
lblMessage.Text = "Server stopped successfully.";
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
finally
{
btnStart.Enabled = true;
btnStop.Enabled = false;
}
}
}
}

很簡單,若真的要說程式片斷就在前面的六行:

cprs = new CustomPeerResolverService();
cprs.RefreshInterval = TimeSpan.FromSeconds(5);
host = new ServiceHost(cprs);
cprs.ControlShape = true;
cprs.Open();
host.Open(TimeSpan.FromDays(1000000));

cprs 是 CustomPeerResolverService 物件,用來當 ServiceHost 物件的輸入參數,主要會讀取一個設定檔(App.config), 並且設定連線資訊更新時間是 5ms。
host 是 ServiceHost 物件,並設定 timeout 時間為永不停止(1000000 天實在是非常久的時間)。然後....就這樣.....

CONFIG

至於設定檔,內容如下:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="System.ServiceModel.PeerResolvers.CustomPeerResolverService">
        <host>
          <baseAddresses>
            <add baseAddress="net.tcp://localhost/ChatServer"/>
          </baseAddresses>
        </host>
        <endpoint address="net.tcp://localhost/ChatServer" binding="netTcpBinding"
                  bindingConfiguration="TcpConfig"
                  contract="System.ServiceModel.PeerResolvers.IPeerResolverContract">         
        </endpoint>         
      </service>
    </services>

    <bindings>
      <netTcpBinding>
        <binding name="TcpConfig">
          <security mode="None"></security>
        </binding>
      </netTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>


Client

Client 是稍微複雜了點,先貼一下原始程式碼如下,只需要用到兩個 namespace(System.ServiceModel, 及System.ServiceModel.Channels):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace ChatClient
{
[ServiceContract(CallbackContract = typeof(IChatService))]
public interface IChatService
{
[OperationContract(IsOneWay = true)]
void Join(string memberName);
[OperationContract(IsOneWay = true)]
void Leave(string memberName);
[OperationContract(IsOneWay = true)]
void SendMessage(string memberName, string message);
}

public interface IChatChannel : IChatService, IClientChannel
{
}

public partial class ChatClient : Form, IChatService
{
private delegate void UserJoined(string name);
private delegate void UserSendMessage(string name, string message);
private delegate void UserLeft(string name);

private static event UserJoined NewJoin;
private static event UserSendMessage MessageSent;
private static event UserLeft RemoveUser;

private string userName;
private IChatChannel channel;
private DuplexChannelFactory factory;

public ChatClient()
{
InitializeComponent();
this.AcceptButton = btnLogin;
}

public ChatClient(string userName)
{
this.userName = userName;
}

private void btnLogin_Click(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(txtUserName.Text.Trim()))
{
try
{
NewJoin += new UserJoined(ChatClient_NewJoin);
MessageSent += new UserSendMessage(ChatClient_MessageSent);
RemoveUser += new UserLeft(ChatClient_RemoveUser);

channel = null;
this.userName = txtUserName.Text.Trim();
InstanceContext context = new InstanceContext(
new ChatClient(txtUserName.Text.Trim()));
factory =
new DuplexChannelFactory(context, "ChatEndPoint");
channel = factory.CreateChannel();
IOnlineStatus status = channel.GetProperty();
status.Offline += new EventHandler(Offline);
status.Online += new EventHandler(Online);
channel.Open();
channel.Join(this.userName);
grpMessageWindow.Enabled = true;
grpUserList.Enabled = true;
grpUserCredentials.Enabled = false;
this.AcceptButton = btnSend;
rtbMessages.AppendText("*****************************WEL-COME to Chat Application*****************************\r\n");
txtSendMessage.Select();
txtSendMessage.Focus();
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
}

void ChatClient_RemoveUser(string name)
{
try
{
rtbMessages.AppendText("\r\n");
rtbMessages.AppendText(name + " left at " + DateTime.Now.ToString());
lstUsers.Items.Remove(name);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.ToString());
}
}

void ChatClient_MessageSent(string name, string message)
{
if (!lstUsers.Items.Contains(name))
{
lstUsers.Items.Add(name);
}
rtbMessages.AppendText("\r\n");
rtbMessages.AppendText(name + " says: " + message);
}

void ChatClient_NewJoin(string name)
{
rtbMessages.AppendText("\r\n");
rtbMessages.AppendText(name + " joined at: [" + DateTime.Now.ToString() + "]");
lstUsers.Items.Add(name);
}

void Online(object sender, EventArgs e)
{
rtbMessages.AppendText("\r\nOnline: " + this.userName);
}

void Offline(object sender, EventArgs e)
{
rtbMessages.AppendText("\r\nOffline: " + this.userName);
}

#region IChatService Members

public void Join(string memberName)
{
if (NewJoin != null)
{
NewJoin(memberName);
}
}

public new void Leave(string memberName)
{
if (RemoveUser != null)
{
RemoveUser(memberName);
}
}

public void SendMessage(string memberName, string message)
{
if (MessageSent != null)
{
MessageSent(memberName, message);
}
}

#endregion

private void btnSend_Click(object sender, EventArgs e)
{
channel.SendMessage(this.userName, txtSendMessage.Text.Trim());
txtSendMessage.Clear();
txtSendMessage.Select();
txtSendMessage.Focus();
}

private void ChatClient_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
if (channel != null)
{
channel.Leave(this.userName);
channel.Close();
}
if (factory != null)
{
factory.Close();
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
}
}

雖然小小長了點,可以看到 interface 中只有定義了三個動作:

Join : 用來登入用
Leave: 用來登出用
SendMessage: 用來傳送訊息

在繼續看這份 souce code 之前,先來讀一下設定檔:

Config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="ChatEndPoint" address="net.p2p://chatMesh/ChatServer"
                binding="netPeerTcpBinding" bindingConfiguration="PeerTcpConfig"
                contract="ChatClient.IChatService"></endpoint>
   </client>

    <bindings>
      <netPeerTcpBinding>
        <binding name="PeerTcpConfig" port="0">
          <security mode="None"></security>
          <resolver mode="Custom">
            <custom address="net.tcp://localhost/ChatServer" binding="netTcpBinding"
                    bindingConfiguration="TcpConfig"></custom>
          </resolver>
        </binding>
      </netPeerTcpBinding>
      <netTcpBinding>
        <binding name="TcpConfig">
          <security mode="None"></security>
        </binding>
      </netTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>


對應到前面講的 Join, Leave, SendMessage 三個介面,需要三個事件來配合:

private static event UserJoined NewJoin;
private static event UserSendMessage MessageSent;
private static event UserLeft RemoveUser;

Event 的使用在程式中如下:

NewJoin += new UserJoined(ChatClient_NewJoin);
MessageSent += new UserSendMessage(ChatClient_MessageSent);
RemoveUser += new UserLeft(ChatClient_RemoveUser);

聊天室功能裡面,最重要的就是聊天,對,有點廢話,不過難就難在網路連線的維護,幸好都被 WCF 做掉了,在程式裡面是透過 DuplexChannelFactory() 物件來達成:

factory = new DuplexChannelFactory(context, "ChatEndPoint");
channel = factory.CreateChannel();

若我們看看幾個動作即可明白中間的關聯:

channel.Join(this.userName);
channel.SendMessage(this.userName, txtSendMessage.Text.Trim());
channel.Leave(this.userName);
channel.Close();

好了,就這樣。什麼?沒講完?對,聰明的你一定有發現,好像沒指出收訊息的機制?呵,其實是沒特別講清楚而已。

訊息收送

收: MessageSent += new UserSendMessage(ChatClient_MessageSent);
送: channel.SendMessage(this.userName, txtSendMessage.Text.Trim());

0 意見: