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());

2009/07/24

在 blog 中優化顯示程式碼片段

底下的內容是 CSS, 放在 blog 設定畫面裡版面配置==>修改 HTML==>找到類似 body{ 附近(意指在它的前面或是那一段的後面, 不要插到裡面去了)插入下面兩種可以選擇之一的 CSS 碼, 一個是用 <Code> 括住,使用時的範例如下:

<Code>
code {
display: block;
font-family: Courier New;
font-size: 9pt;
overflow:auto;
background: #fff5ee url(http://sites.google.com/a/hc888.com.tw/file/img/Code_BG0.gif) left top repeat-y;
border: 1px solid #ccc;
padding: 5px 5px 5px 20px;
max-height:200px;
line-height: 1.2em;
margin: 5px 0 0 15px;
}
</CODE>


效果如下:

code {
display: block;
font-family: Courier New;
font-size: 9pt;
overflow:auto;
background: #fff5ee url(http://sites.google.com/a/hc888.com.tw/file/img/Code_BG0.gif) left top repeat-y;
border: 1px solid #ccc;
padding: 5px 5px 5px 20px;
max-height:200px;
line-height: 1.2em;
margin: 5px 0 0 15px;
}


或者用有縮編的 PRE tag, 在寫 blog 時的範例如下:

<PRE>
pre {
display: block;
font-family: Courier New;
font-size: 9pt;
overflow:auto;
background: #fff5ee;
border: 1px solid #ccc;
padding: 5px 5px 5px 20px;
max-height:200px;
line-height: 1.2em;
margin: 5px 0 0 15px;
}
</PRE>

效果如下:

pre {
display: block;
font-family: Courier New;
font-size: 9pt;
overflow:auto;
background: #fff5ee;
border: 1px solid #ccc;
padding: 5px 5px 5px 20px;
max-height:200px;
line-height: 1.2em;
margin: 5px 0 0 15px;
}

2009/07/18

The value of Multi touch 的價值

要說 multi touch 的價值實在是愧不敢當,但是最近的工作剛好就是在開發多點觸控的應用程式,跟著一群年輕小夥子工作,感覺非常溫馨之餘,也讓我有機會在跟他們探討中去深思多點觸控到底價值何處?

一開始我對多點觸控的期待,就是改變對電腦的操作,當然我更想要像駭客任務那樣,不止可以觸控,最希望的是立體感那種操作,最好要更先進的思想操作。但是在接觸多點觸控一年來,發現裡面的問題不少,但是這篇不是要講問題點,而是要講價值。

大概最快想到的就是「手勢操作」。可是問題是誰來定義手勢?M$ 確實定義了些手勢操作,可是若實際去開發軟體的話就會發現其中存在不少問題,這邊指的是我們開發軟體的過程,而不是「應用」。問題在我們的功力還不夠,幸好也是因為大家都聚精會神的研究並解決問題,因而讓我更有機會去探索多點觸控的價值,而這樣的思考至少目前必須先面對 M$ 所建立起來的環境是那麼的複雜與充滿矛盾上。

第一個價值,當然希望能有更直覺式的操作,剛剛提到手勢就是這樣的產物。目前開發出來的就是 Zoom In/ Zoom Out, Scroll, PAN(Move) 這三個。事實上滑鼠也可以做到。我一直在思考,click, double click, move, drag....與 what is gesture! 困惑著我。答案或許是,請拋棄滑鼠吧。(註: 鍵盤留著)

第二個價值,是希望能讓整個螢幕上出現的框框有立體操作感。立體感早就有了,我說的是立體的操作感,否則只是多了另一隻滑鼠而已。要有立體操作感很難,要在平面表達立體已經發展多年,早期都是用滑鼠,當然相安無事,現在要在投射在平面所產生的立體物件上操作,難度非常大。幸好,我也定義了三種操作,有機會再來分享。我們定義了一些立體操作與手勢,希望能改變人類操作平面電腦的習慣,這邊講平面的意思當然是希望將來的顯示裝置操作起來能隨想隨動。

第三個價值,是......趁著這波可攜熱與觸控熱,讓觸控拉近你我彼此間的距離變得更加容易。我個人一直是在玩 Linux, 嵌入式是我的擅項,突然來搞 M$ 真是有夠突兀,不過這不妨礙,問題是我最想要的是建立一個龐大而自然的虛擬世界,能因此而實現嗎?

且拭目以待吧。