分享一個(gè)C#編寫(xiě)簡(jiǎn)單的聊天程序(詳細(xì)介紹)
引言
這是一篇基于Socket進(jìn)行網(wǎng)絡(luò)編程的入門(mén)文章,我對(duì)于網(wǎng)絡(luò)編程的學(xué)習(xí)并不夠深入,這篇文章是對(duì)于自己知識(shí)的一個(gè)鞏固,同時(shí)希望能為初學(xué)的朋友提供一點(diǎn)參考。文章大體分為四個(gè)部分:程序的分析與設(shè)計(jì)、C#網(wǎng)絡(luò)編程基礎(chǔ)(篇外篇)、聊天程序的實(shí)現(xiàn)模式、程序?qū)崿F(xiàn)。
程序的分析與設(shè)計(jì)
1.明確程序功能
如果大家現(xiàn)在已經(jīng)參加了工作,你的經(jīng)理或者老板告訴你,“小王,我需要你開(kāi)發(fā)一個(gè)聊天程序”。那么接下來(lái)該怎么做呢?你是不是在腦子里有個(gè)雛形,然后就直接打開(kāi)VS2005開(kāi)始設(shè)計(jì)窗體,編寫(xiě)代碼了呢?在開(kāi)始之前,我們首先需要進(jìn)行軟件的分析與設(shè)計(jì)。就拿本例來(lái)說(shuō),如果只有這么一句話“一個(gè)聊天程序”,恐怕現(xiàn)在大家對(duì)這個(gè)“聊天程序”的概念就很模糊,它可以是像QQ那樣的非常復(fù)雜的一個(gè)程序,也可以是很簡(jiǎn)單的聊天程序;它可能只有在對(duì)方在線的時(shí)候才可以進(jìn)行聊天,也可能進(jìn)行留言;它可能每次將消息只能發(fā)往一個(gè)人,也可能允許發(fā)往多個(gè)人。它還可能有一些高級(jí)功能,比如向?qū)Ψ絺魉臀募?。所以我們首先需要進(jìn)行分析,而不是一上手就開(kāi)始做,而分析的第一步,就是搞清楚程序的功能是什么,它能夠做些什么。在這一步,我們的任務(wù)是了解程序需要做什么,而不是如何去做。
了解程序需要做什么,我們可以從兩方面入手,接下來(lái)我們分別討論。
1.1請(qǐng)求客戶提供更詳細(xì)信息
我們可以做的第一件事就是請(qǐng)求客戶提供更加詳細(xì)的信息。盡管你的經(jīng)理或老板是你的上司,但在這個(gè)例子中,他就是你的客戶(當(dāng)然通常情況下,客戶是公司外部委托公司開(kāi)發(fā)軟件的人或單位)。當(dāng)遇到上面這種情況,我們只有少得可憐的一條信息“一個(gè)聊天程序”,首先可以做的,就是請(qǐng)求客戶提供更加確切的信息。比如,你問(wèn)經(jīng)理“對(duì)這個(gè)程序的功能能不能提供一些更具體的信息?”。他可能會(huì)像這樣回答:“哦,很簡(jiǎn)單,可以登錄聊天程序,登錄的時(shí)候能夠通知其他在線用戶,然后與在線的用戶進(jìn)行對(duì)話,如果不想對(duì)話了,就注銷或者直接關(guān)閉,就這些吧?!?/p>
有了上面這段話,我們就又可以得出下面幾個(gè)需求:
1.程序可以進(jìn)行登錄。
2.登錄后可以通知其他在線用戶。
3.可以與其他用戶進(jìn)行對(duì)話。
4.可以注銷或者關(guān)閉。
1.2對(duì)于用戶需求進(jìn)行提問(wèn),并進(jìn)行總結(jié)
經(jīng)常會(huì)有這樣的情況:可能客戶給出的需求仍然不夠細(xì)致,或者客戶自己本身對(duì)于需求就很模糊,此時(shí)我們需要做的就是針對(duì)用戶上面給出的信息進(jìn)行提問(wèn)。接下來(lái)我就看看如何對(duì)上面的需求進(jìn)行提問(wèn),我們至少可以向經(jīng)理提出以下問(wèn)題:
NOTE:這里我穿插一個(gè)我在見(jiàn)到的一個(gè)印象比較深刻的例子:客戶往往向你表達(dá)了強(qiáng)烈的意愿他多么多么想擁有一個(gè)屬于自己的網(wǎng)站,但是,他卻沒(méi)有告訴你網(wǎng)站都有哪些內(nèi)容、欄目,可以做什么。而作為開(kāi)發(fā)者,我們顯然關(guān)心的是后者。
1.登錄時(shí)需要提供哪些內(nèi)容?需不需要提供密碼?
2.允許多少人同時(shí)在線聊天?
3.與在線用戶聊天時(shí),可以將一條消息發(fā)給一個(gè)用戶,還是可以一次將消息發(fā)給多個(gè)用戶?
4.聊天時(shí)發(fā)送的消息包括哪些內(nèi)容?
5.注銷和關(guān)閉有什么區(qū)別?
6.注銷和關(guān)閉對(duì)對(duì)方需不需要給對(duì)方提示?
由于這是一個(gè)范例程序,而我在為大家講述,所以我只能再充當(dāng)一下客戶的角色,來(lái)回答上面的問(wèn)題:
1.登錄時(shí)只需要提供用戶名稱就可以了,不需要輸入密碼。
2.允許兩個(gè)人在線聊天。(這里我們只講述這種簡(jiǎn)單情況,允許多人聊天需要使用多線程)
3.因?yàn)橹挥袃蓚€(gè)人,那么自然是只能發(fā)給一個(gè)用戶了。
4.聊天發(fā)送的消息包括:用戶名稱、發(fā)送時(shí)間還有正文。
5.注銷并不關(guān)閉程序,只是離開(kāi)了對(duì)話,可以再次進(jìn)行連接。關(guān)閉則是退出整個(gè)應(yīng)用程序。
6.注銷和關(guān)閉均需要給對(duì)方提示。
好了,有了上面這些信息我們基本上就掌握了程序需要完成的功能,那么接下來(lái)做什么?開(kāi)始編碼了么?上面的這些屬于業(yè)務(wù)流程,除非你對(duì)它已經(jīng)非常熟悉,或者程序非常的小,那么可以對(duì)它進(jìn)行編碼,但是實(shí)際中,我們最好再編寫(xiě)一些用例,這樣會(huì)使程序的流程更加的清楚。
1.3編寫(xiě)用例
通常一個(gè)用例對(duì)應(yīng)一個(gè)功能或者叫需求,它是程序的一個(gè)執(zhí)行路徑或者執(zhí)行流程。編寫(xiě)用例的思路是:假設(shè)你已經(jīng)有了這樣一個(gè)聊天程序,那么你應(yīng)該如何使用它?我們的使用步驟,就是一個(gè)用例。用例的特點(diǎn)就每次只針對(duì)程序的一個(gè)功能編寫(xiě),最后根據(jù)用例編寫(xiě)代碼,最終完成程序的開(kāi)發(fā)。我們這里的需求只有簡(jiǎn)單的幾個(gè):登錄,發(fā)送消息,接收消息,注銷或關(guān)閉,上面的分析是對(duì)這幾點(diǎn)功能的一個(gè)明確。接下來(lái)我們首先編寫(xiě)第一個(gè)用例:登錄。
在開(kāi)始之前,我們先明確一個(gè)概念:客戶端,服務(wù)端。因?yàn)檫@個(gè)程序只是在兩個(gè)人(機(jī)器)之間聊天,那么我們大致可以繪出這樣一個(gè)圖來(lái):
我們期望用戶A和用戶B進(jìn)行對(duì)話,那么我們就需要在它們之間建立起連接。盡管“用戶A”和“用戶B”的地位是對(duì)等的,但按照約定俗稱的說(shuō)法:我們將發(fā)起連接請(qǐng)求的一方稱為客戶端(或叫本地),另一端稱為服務(wù)端(或叫遠(yuǎn)程)。所以我們的登錄過(guò)程,就是“用戶A”連接到“用戶B”的過(guò)程,或者說(shuō)客戶端(本地)連接到服務(wù)端(遠(yuǎn)程)的過(guò)程。在分析這個(gè)程序的過(guò)程中,我們總是將其分為兩部分,一部分為發(fā)起連接、發(fā)送消息的一方(本地),一方為接受連接、接收消息的一方(遠(yuǎn)程)。
登錄和連接(本地) | |
主路徑 | 可選路徑 |
1.打開(kāi)應(yīng)用程序,顯示登錄窗口 | |
2.輸入用戶名 | |
3.點(diǎn)擊“登錄”按鈕,登錄成功 | 3.“登錄”失敗
如果用戶名為空,重新進(jìn)入第2步。 |
4.顯示主窗口,顯示登錄的用戶名稱 | |
5.點(diǎn)擊“連接”,連接至遠(yuǎn)程 | |
6.連接成功 6.1提示用戶,連接已經(jīng)成功。 |
6.連接失敗 6.1 提示用戶,連接不成功 |
5.在用戶界面變更控件狀態(tài) 5.2連接為灰色,表示已經(jīng)連接 5.3注銷為亮色,表示可以注銷 5.4發(fā)送為亮色,表示可以發(fā)消息 |
這里我們的用例名稱為登錄和連接,但是后面我們又打了一個(gè)括號(hào),寫(xiě)著“本地”,它的意思是說(shuō),登錄和連接是客戶端,也就是發(fā)起連接的一方采取的動(dòng)作。同樣,我們需要寫(xiě)下當(dāng)客戶端連接至服務(wù)端時(shí),服務(wù)端采取的動(dòng)作。
登錄和連接(遠(yuǎn)程) | |
主路徑 | 可選路徑 |
1-4 同客戶端 | |
5.等待連接 | |
6.如果有連接,自動(dòng)在用戶界面顯示“遠(yuǎn)程主機(jī)連接成功” |
接下來(lái)我們來(lái)看發(fā)送消息。在發(fā)送消息時(shí),已經(jīng)是登錄了的,也就是“用戶A”、“用戶B”已經(jīng)做好了連接,所以我們現(xiàn)在就可以只關(guān)注發(fā)送這一過(guò)程:
發(fā)送消息(本地) | |
主路徑 | 可選路徑 |
1.輸入消息 | |
2.點(diǎn)擊發(fā)送按鈕 | 2.沒(méi)有輸入消息,重新回到第1步 |
3.在用戶界面上顯示發(fā)出的消息 | 3.服務(wù)端已經(jīng)斷開(kāi)連接或者關(guān)閉
3.1在客戶端用戶界面上顯示錯(cuò)誤消息 |
然后我們看一下接收消息,此時(shí)我們只關(guān)心接收消息這一部分。
接收消息(遠(yuǎn)程) | |
主路徑 | 可選路徑 |
1.偵聽(tīng)到客戶端發(fā)來(lái)的消息,自動(dòng)顯示在用戶界面上。 |
注意到這樣一點(diǎn):當(dāng)遠(yuǎn)程主機(jī)向本地返回消息時(shí),它的用例又變?yōu)榱松厦娴挠美鞍l(fā)送消息(本地)”。因?yàn)樗鼈兊慕巧呀?jīng)互換了。
最后看一下注銷,我們這里研究的是當(dāng)我們?cè)诒镜貦C(jī)器點(diǎn)擊“注銷”后,雙方采取的動(dòng)作:
注銷(本地主動(dòng)) | |
主路徑 | 可選路徑 |
1.點(diǎn)擊注銷按鈕,斷開(kāi)與遠(yuǎn)程的連接 | |
2.在用戶界面顯示已經(jīng)注銷 | |
3.更改控件狀態(tài) 3.1注銷為灰色,表示已經(jīng)注銷 3.2連接為亮色,表示可以連接 3.3發(fā)送為灰色,表示無(wú)法發(fā)送 |
與此對(duì)應(yīng),服務(wù)端應(yīng)該作出反應(yīng):
注銷(遠(yuǎn)程被動(dòng)) | |
主路徑 | 可選路徑 |
1.自動(dòng)顯示遠(yuǎn)程用戶已經(jīng)斷開(kāi)連接。 |
注意到一點(diǎn):當(dāng)遠(yuǎn)程主動(dòng)注銷時(shí),它采取的動(dòng)作為上面的“本地主動(dòng)”,本地采取的動(dòng)作則為這里的“遠(yuǎn)程被動(dòng)”。
至此,應(yīng)用程序的功能分析和用例編寫(xiě)就告一段落了,通過(guò)上面這些表格,之后再繼續(xù)編寫(xiě)程序變得容易了許多。另外還需要記得,用例只能為你提供一個(gè)操作步驟的指導(dǎo),在實(shí)現(xiàn)的過(guò)程中,因?yàn)榧夹g(shù)等方面的原因,可能還會(huì)有少量的修改。如果修改量很大,可以重新修改用例;如果修改量不大,那么就可以直接編碼。這是一個(gè)迭代的過(guò)程,也沒(méi)有一定的標(biāo)準(zhǔn),總之是以高效和合適為標(biāo)準(zhǔn)。
2.分析與設(shè)計(jì)
我們已經(jīng)很清楚地知道了程序需要做些什么,盡管現(xiàn)在還不知道該如何去做。我們甚至可以編寫(xiě)出這個(gè)程序所需要的接口,以后編寫(xiě)代碼的時(shí)候,我們只要去實(shí)現(xiàn)這些接口就可以了。這也符合面向接口編程的原則。另外我們注意到,盡管這是一個(gè)聊天程序,但是卻可以明確地劃分為兩部分,一部分發(fā)送消息,一部分接收消息。另外注意上面標(biāo)識(shí)為自動(dòng)的語(yǔ)句,它們暗示這個(gè)操作需要通過(guò)事件的通知機(jī)制來(lái)完成。關(guān)于委托和事件,可以參考這兩篇文章:
- C#中的委托和事件 - 委托和事件的入門(mén)文章,同時(shí)捎帶講述了Observer設(shè)計(jì)模式和.NET的事件模型
- C#中的委托和事件(續(xù)) - 委托和事件更深入的一些問(wèn)題,包括異常、超時(shí)的處理,以及使用委托來(lái)異步調(diào)用方法。
2.1消息Message
首先我們可以定義消息,前面我們已經(jīng)明確了消息包含三個(gè)部分:用戶名、時(shí)間、內(nèi)容,所以我們可以定義一個(gè)結(jié)構(gòu)來(lái)表示這個(gè)消息:
public struct Message { private readonly string userName; private readonly string content; private readonly DateTime postDate; public Message(string userName, string content) { this.userName = userName; this.content = content; this.postDate = DateTime.Now; } public Message(string content) : this("System", content) { } public string UserName { get { return userName; } } public string Content { get { return content; } } public DateTime PostDate { get { return postDate; } } public override string ToString() { return String.Format("{0}[{1}]:\r\n{2}\r\n", userName, postDate, content); } }
2.2消息發(fā)送方IMessageSender
從上面我們可以看出,消息發(fā)送方主要包含這樣幾個(gè)功能:登錄、連接、發(fā)送消息、注銷。另外在連接成功或失敗時(shí)還要通知用戶界面,發(fā)送消息成功或失敗時(shí)也需要通知用戶界面,因此,我們可以讓連接和發(fā)送消息返回一個(gè)布爾類型的值,當(dāng)它為真時(shí)表示連接或發(fā)送成功,反之則為失敗。因?yàn)榈卿洓](méi)有任何的業(yè)務(wù)邏輯,僅僅是記錄控件的值并進(jìn)行顯示,所以我不打算將它寫(xiě)到接口中。因此我們可以得出它的接口大致如下:
public interface IMessageSender { bool Connect(IPAddress ip, int port); // 連接到服務(wù)端 bool SendMessage(Message msg); // 發(fā)送用戶 void SignOut(); // 注銷系統(tǒng) }
2.3消息接收方IMessageReceiver
而對(duì)于消息接收方,從上面我們可以看出,它的操作全是被動(dòng)的:客戶端連接時(shí)自動(dòng)提示,客戶端連接丟失時(shí)顯示自動(dòng)提示,偵聽(tīng)到消息時(shí)自動(dòng)提示。注意到上面三個(gè)詞都用了“自動(dòng)”來(lái)修飾,在C#中,可以定義委托和事件,用于當(dāng)程序中某種情況發(fā)生時(shí),通知另外一個(gè)對(duì)象。在這里,程序即是我們的IMessageReceiver,某種情況就是上面的三種情況,而另外一個(gè)對(duì)象則為我們的用戶界面。因此,我們現(xiàn)在首先需要定義三個(gè)委托:
public delegate void MessageReceivedEventHandler(string msg); public delegate void ClientConnectedEventHandler(IPEndPoint endPoint); public delegate void ConnectionLostEventHandler(string info);
接下來(lái),我們注意到接收方需要偵聽(tīng)消息,因此我們需要在接口中定義的方法是StartListen()和StopListen()方法,這兩個(gè)方法是典型的技術(shù)相關(guān),而不是業(yè)務(wù)相關(guān),所以從用例中是看不出來(lái)的,可能大家現(xiàn)在對(duì)這兩個(gè)方法是做什么的還不清楚,沒(méi)有關(guān)系,我們現(xiàn)在并不寫(xiě)實(shí)現(xiàn),而定義接口并不需要什么成本,我們寫(xiě)下IMessageReceiver的接口定義:
public interface IMessageReceiver { event MessageReceivedEventHandler MessageReceived; // 接收到發(fā)來(lái)的消息 event ConnectionLostEventHandler ClientLost; // 遠(yuǎn)程主動(dòng)斷開(kāi)連接 event ClientConnectedEventHandler ClientConnected; // 遠(yuǎn)程連接到了本地 void StartListen(); // 開(kāi)始偵聽(tīng)端口 void StopListen(); // 停止偵聽(tīng)端口 }
我記得曾經(jīng)看過(guò)有篇文章說(shuō)過(guò),最好不要在接口中定義事件,但是我忘了他的理由了,所以本文還是將事件定義在了接口中。
2.4主程序Talker
而我們的主程序是既可以發(fā)送,又可以接收,一般來(lái)說(shuō),如果一個(gè)類像獲得其他類的能力,以采用兩種方法:繼承和復(fù)合。因?yàn)镃#中沒(méi)有多重繼承,所以我們無(wú)法同時(shí)繼承實(shí)現(xiàn)了IMessageReceiver和IMessageSender的類。那么我們可以采用復(fù)合,將它們作為類成員包含在Talker內(nèi)部:
public class Talker { private IMessageReceiver receiver; private IMessageSender sender; public Talker(IMessageReceiver receiver, IMessageSender sender) { this.receiver = receiver; this.sender = sender; } }
現(xiàn)在,我們的程序大體框架已經(jīng)完成,接下來(lái)要關(guān)注的就是如何實(shí)現(xiàn)它,現(xiàn)在讓我們由設(shè)計(jì)走入實(shí)現(xiàn),看看實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)聊天程序,我們需要掌握的技術(shù)吧。
C#網(wǎng)絡(luò)編程基礎(chǔ)(篇外篇)
這部分的內(nèi)容請(qǐng)參考 C#網(wǎng)絡(luò)編程 系列文章,共5個(gè)部分較為詳細(xì)的講述了基于Socket的網(wǎng)絡(luò)編程的初步內(nèi)容。
編寫(xiě)程序代碼
如果你已經(jīng)看完了上面一節(jié)C#網(wǎng)絡(luò)編程,那么本章完全沒(méi)有講解的必要了,所以我只列出代碼,對(duì)個(gè)別值得注意的地方稍微地講述一下。首先需要了解的就是,我們采用的是三個(gè)模式中開(kāi)發(fā)起來(lái)難度較大的一種,無(wú)服務(wù)器參與的模式。還有就是我們沒(méi)有使用廣播消息,所以需要提前知道連接到的遠(yuǎn)程主機(jī)的地址和端口號(hào)。
1.實(shí)現(xiàn)IMessageSender接口
public class MessageSender : IMessageSender { TcpClient client; Stream streamToServer; // 連接至遠(yuǎn)程 public bool Connect(IPAddress ip, int port) { try { client = new TcpClient(); client.Connect(ip, port); streamToServer = client.GetStream(); // 獲取連接至遠(yuǎn)程的流 return true; } catch { return false; } } // 發(fā)送消息 public bool SendMessage(Message msg) { try { lock (streamToServer) { byte[] buffer = Encoding.Unicode.GetBytes(msg.ToString()); streamToServer.Write(buffer, 0, buffer.Length); return true; } } catch { return false; } } // 注銷 public void SignOut() { if (streamToServer != null) streamToServer.Dispose(); if (client != null) client.Close(); } }
這段代碼可以用樸實(shí)無(wú)華來(lái)形容,所以我們直接看下一段。
2.實(shí)現(xiàn)IMessageReceiver接口
public delegate void PortNumberReadyEventHandler(int portNumber); public class MessageReceiver : IMessageReceiver { public event MessageReceivedEventHandler MessageReceived; public event ConnectionLostEventHandler ClientLost; public event ClientConnectedEventHandler ClientConnected; // 當(dāng)端口號(hào)Ok的時(shí)候調(diào)用 -- 需要告訴用戶界面使用了哪個(gè)端口號(hào)在偵聽(tīng) // 這里是業(yè)務(wù)上體現(xiàn)不出來(lái),在實(shí)現(xiàn)中才能體現(xiàn)出來(lái)的 public event PortNumberReadyEventHandler PortNumberReady; private Thread workerThread; private TcpListener listener; public MessageReceiver() { ((IMessageReceiver)this).StartListen(); } // 開(kāi)始偵聽(tīng):顯示實(shí)現(xiàn)接口 void IMessageReceiver.StartListen() { ThreadStart start = new ThreadStart(ListenThreadMethod); workerThread = new Thread(start); workerThread.IsBackground = true; workerThread.Start(); } // 線程入口方法 private void ListenThreadMethod() { IPAddress localIp = IPAddress.Parse("127.0.0.1"); listener = new TcpListener(localIp, 0); listener.Start(); // 獲取端口號(hào) IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint; int portNumber = endPoint.Port; if (PortNumberReady != null) { PortNumberReady(portNumber); // 端口號(hào)已經(jīng)OK,通知用戶界面 } while (true) { TcpClient remoteClient; try { remoteClient = listener.AcceptTcpClient(); } catch { break; } if (ClientConnected != null) { // 連接至本機(jī)的遠(yuǎn)程端口 endPoint = remoteClient.Client.RemoteEndPoint as IPEndPoint; ClientConnected(endPoint); // 通知用戶界面遠(yuǎn)程客戶連接 } Stream streamToClient = remoteClient.GetStream(); byte[] buffer = new byte[8192]; while (true) { try { int bytesRead = streamToClient.Read(buffer, 0, 8192); if (bytesRead == 0) { throw new Exception("客戶端已斷開(kāi)連接"); } string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead); if (MessageReceived != null) { MessageReceived(msg); // 已經(jīng)收到消息 } } catch (Exception ex) { if (ClientLost != null) { ClientLost(ex.Message); // 客戶連接丟失 break; // 退出循環(huán) } } } } } // 停止偵聽(tīng)端口 public void StopListen() { try { listener.Stop(); listener = null; workerThread.Abort(); } catch { } } }
這里需要注意的有這樣幾點(diǎn):我們StartListen()為顯式實(shí)現(xiàn)接口,因?yàn)橹荒芡ㄟ^(guò)接口才能調(diào)用此方法,接口的實(shí)現(xiàn)類看不到此方法;這通常是對(duì)于一個(gè)接口采用兩種實(shí)現(xiàn)方式時(shí)使用的,但這里我只是不希望MessageReceiver類型的客戶調(diào)用它,因?yàn)樵贛essageReceiver的構(gòu)造函數(shù)中它已經(jīng)調(diào)用了StartListen。意思是說(shuō),我們希望這個(gè)類型一旦創(chuàng)建,就立即開(kāi)始工作。我們使用了兩個(gè)嵌套的while循環(huán),這個(gè)它可以為多個(gè)客戶端的多次請(qǐng)求服務(wù),但是因?yàn)槭峭讲僮?,只要有一個(gè)客戶端連接著,我們的后臺(tái)線程就會(huì)陷入第二個(gè)循環(huán)中無(wú)法自拔。所以結(jié)果是:如果有一個(gè)客戶端已經(jīng)連接上了,其它客戶端即使連接了也無(wú)法對(duì)它應(yīng)答。最后需要注意的就是四個(gè)事件的使用,為了向用戶提供偵聽(tīng)的端口號(hào)以進(jìn)行連接,我又定義了一個(gè)PortNumberReadyEventHandler委托。
3.實(shí)現(xiàn)Talker類
Talker類是最平庸的一個(gè)類,它的全部功能就是將操作委托給實(shí)際的IMessageReceiver和IMessageSender。定義這兩個(gè)接口的好處也從這里可以看出來(lái):如果日后想重新實(shí)現(xiàn)這個(gè)程序,所有Windows窗體的代碼和Talker的代碼都不需要修改,只需要針對(duì)這兩個(gè)接口編程就可以了。
public class Talker { private IMessageReceiver receiver; private IMessageSender sender; public Talker(IMessageReceiver receiver, IMessageSender sender) { this.receiver = receiver; this.sender = sender; } public Talker() { this.receiver = new MessageReceiver(); this.sender = new MessageSender(); } public event MessageReceivedEventHandler MessageReceived { add { receiver.MessageReceived += value; } remove { receiver.MessageReceived -= value; } } public event ClientConnectedEventHandler ClientConnected { add { receiver.ClientConnected += value; } remove { receiver.ClientConnected -= value; } } public event ConnectionLostEventHandler ClientLost { add { receiver.ClientLost += value; } remove { receiver.ClientLost -= value; } } // 注意這個(gè)事件 public event PortNumberReadyEventHandler PortNumberReady { add { ((MessageReceiver)receiver).PortNumberReady += value; } remove { ((MessageReceiver)receiver).PortNumberReady -= value; } } // 連接遠(yuǎn)程 - 使用主機(jī)名 public bool ConnectByHost(string hostName, int port) { IPAddress[] ips = Dns.GetHostAddresses(hostName); return sender.Connect(ips[0], port); } // 連接遠(yuǎn)程 - 使用IP public bool ConnectByIp(string ip, int port) { IPAddress ipAddress; try { ipAddress = IPAddress.Parse(ip); } catch { return false; } return sender.Connect(ipAddress, port); } // 發(fā)送消息 public bool SendMessage(Message msg) { return sender.SendMessage(msg); } // 釋放資源,停止偵聽(tīng) public void Dispose() { try { sender.SignOut(); receiver.StopListen(); } catch { } } // 注銷 public void SignOut() { try { sender.SignOut(); } catch { } } }
4.設(shè)計(jì)窗體,編寫(xiě)窗體事件代碼
現(xiàn)在我們開(kāi)始設(shè)計(jì)窗體,我已經(jīng)設(shè)計(jì)好了,現(xiàn)在可以先進(jìn)行一下預(yù)覽:
這里需要注意的就是上面的偵聽(tīng)端口,是程序接收消息時(shí)的偵聽(tīng)端口,也就是IMessageReceiver所使用的。其他的沒(méi)有什么好說(shuō)的,下來(lái)我們直接看一下代碼,控件的命名是自解釋的,我就不多說(shuō)什么了。唯一要稍微說(shuō)明下的是txtMessage指的是下面發(fā)送消息的文本框,txtContent指上面的消息記錄文本框:
public partial class PrimaryForm : Form { private Talker talker; private string userName; public PrimaryForm(string name) { InitializeComponent(); userName = lbName.Text = name; this.talker = new Talker(); this.Text = userName + " Talking ..."; talker.ClientLost += new ConnectionLostEventHandler(talker_ClientLost); talker.ClientConnected += new ClientConnectedEventHandler(talker_ClientConnected); talker.MessageReceived += new MessageReceivedEventHandler(talker_MessageReceived); talker.PortNumberReady += new PortNumberReadyEventHandler(PrimaryForm_PortNumberReady); } void ConnectStatus() { } void DisconnectStatus() { } // 端口號(hào)OK void PrimaryForm_PortNumberReady(int portNumber) { PortNumberReadyEventHandler del = delegate(int port) { lbPort.Text = port.ToString(); }; lbPort.Invoke(del, portNumber); } // 接收到消息 void talker_MessageReceived(string msg) { MessageReceivedEventHandler del = delegate(string m) { txtContent.Text += m; }; txtContent.Invoke(del, msg); } // 有客戶端連接到本機(jī) void talker_ClientConnected(IPEndPoint endPoint) { ClientConnectedEventHandler del = delegate(IPEndPoint end) { IPHostEntry host = Dns.GetHostEntry(end.Address); txtContent.Text += String.Format("System[{0}]:\r\n遠(yuǎn)程主機(jī){1}連接至本地。\r\n", DateTime.Now, end); }; txtContent.Invoke(del, endPoint); } // 客戶端連接斷開(kāi) void talker_ClientLost(string info) { ConnectionLostEventHandler del = delegate(string information) { txtContent.Text += String.Format("System[{0}]:\r\n{1}\r\n", DateTime.Now, information); }; txtContent.Invoke(del, info); } // 發(fā)送消息 private void btnSend_Click(object sender, EventArgs e) { if (String.IsNullOrEmpty(txtMessage.Text)) { MessageBox.Show("請(qǐng)輸入內(nèi)容!"); txtMessage.Clear(); txtMessage.Focus(); return; } Message msg = new Message(userName, txtMessage.Text); if (talker.SendMessage(msg)) { txtContent.Text += msg.ToString(); txtMessage.Clear(); } else { txtContent.Text += String.Format("System[{0}]:\r\n遠(yuǎn)程主機(jī)已斷開(kāi)連接\r\n", DateTime.Now); DisconnectStatus(); } } // 點(diǎn)擊連接 private void btnConnect_Click(object sender, EventArgs e) { string host = txtHost.Text; string ip = txtHost.Text; int port; if (String.IsNullOrEmpty(txtHost.Text)) { MessageBox.Show("主機(jī)名稱或地址不能為空"); } try{ port = Convert.ToInt32(txtPort.Text); }catch{ MessageBox.Show("端口號(hào)不能為空,且必須為數(shù)字"); return; } if (talker.ConnectByHost(host, port)) { ConnectStatus(); txtContent.Text += String.Format("System[{0}]:\r\n已成功連接至遠(yuǎn)程\r\n", DateTime.Now); return; } if(talker.ConnectByIp(ip, port)){ ConnectStatus(); txtContent.Text += String.Format("System[{0}]:\r\n已成功連接至遠(yuǎn)程\r\n", DateTime.Now); }else{ MessageBox.Show("遠(yuǎn)程主機(jī)不存在,或者拒絕連接!"); } txtMessage.Focus(); } // 關(guān)閉按鈕點(diǎn)按 private void btnClose_Click(object sender, EventArgs e) { try { talker.Dispose(); Application.Exit(); } catch { } } // 直接點(diǎn)擊右上角的叉 private void PrimaryForm_FormClosing(object sender, FormClosingEventArgs e) { try { talker.Dispose(); Application.Exit(); } catch { } } // 點(diǎn)擊注銷 private void btnSignout_Click(object sender, EventArgs e) { talker.SignOut(); DisconnectStatus(); txtContent.Text += String.Format("System[{0}]:\r\n已經(jīng)注銷\r\n",DateTime.Now); } private void btnClear_Click(object sender, EventArgs e) { txtContent.Clear(); } }
在上面代碼中,分別通過(guò)四個(gè)方法訂閱了四個(gè)事件,以實(shí)現(xiàn)自動(dòng)通知的機(jī)制。最后需要注意的就是SignOut()和Dispose()的區(qū)分。SignOut()只是斷開(kāi)連接,Dispose()則是離開(kāi)應(yīng)用程序。
總結(jié)
這篇文章簡(jiǎn)單地分析、設(shè)計(jì)及實(shí)現(xiàn)了一個(gè)聊天程序。這個(gè)程序只是對(duì)無(wú)服務(wù)器模式實(shí)現(xiàn)聊天的一個(gè)嘗試。我們分析了需求,隨后編寫(xiě)了幾個(gè)用例,并對(duì)本地、遠(yuǎn)程的概念做了定義,接著編寫(xiě)了程序接口并最終實(shí)現(xiàn)了它。這個(gè)程序還有很嚴(yán)重的不足:它無(wú)法實(shí)現(xiàn)自動(dòng)上線通知,而必須要事先知道端口號(hào)并進(jìn)行手動(dòng)連接。為了實(shí)現(xiàn)一個(gè)功能強(qiáng)大且開(kāi)發(fā)容易的程序,更好的辦法是使用集中型服務(wù)器模式。
感謝閱讀,希望這篇文章能對(duì)你有所幫助。
上一篇:C#異步下載文件
欄 目:C#教程
下一篇:TortoiseSVN使用教程
本文標(biāo)題:分享一個(gè)C#編寫(xiě)簡(jiǎn)單的聊天程序(詳細(xì)介紹)
本文地址:http://mengdiqiu.com.cn/a1/C_jiaocheng/6779.html
您可能感興趣的文章
- 01-10C#實(shí)現(xiàn)多線程寫(xiě)入同一個(gè)文件的方法
- 01-10C#一個(gè)簡(jiǎn)單的定時(shí)小程序?qū)崿F(xiàn)代碼
- 01-10WPF實(shí)現(xiàn)類似360安全衛(wèi)士界面的程序源碼分享
- 01-10C#裝箱和拆箱原理詳解
- 01-10分享WCF聊天程序--WCFChat實(shí)現(xiàn)代碼
- 01-10分享WCF文件傳輸實(shí)現(xiàn)方法---WCFFileTransfer
- 01-10C#實(shí)現(xiàn)簡(jiǎn)單的登錄界面
- 01-10C#實(shí)現(xiàn)流程圖設(shè)計(jì)器
- 01-10輕松學(xué)習(xí)C#的正則表達(dá)式
- 01-10分享我在工作中遇到的多線程下導(dǎo)致RCW無(wú)法釋放的問(wèn)題


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹(shù)的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問(wèn)題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-10C#通過(guò)反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁(yè)無(wú)法打開(kāi)的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻 器左下角滾動(dòng)新
- 01-10C#停止線程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過(guò)重寫(xiě)Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 01-11Mac OSX 打開(kāi)原生自帶讀寫(xiě)NTFS功能(圖文
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?
- 04-02jquery與jsp,用jquery
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 01-10SublimeText編譯C開(kāi)發(fā)環(huán)境設(shè)置
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10delphi制作wav文件的方法
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載