引言
這是一篇基于socket進行網絡編程的入門文章,我對于網絡編程的學習并不夠深入,這篇文章是對于自己知識的一個鞏固,同時希望能為初學的朋友提供一點參考。文章大體分為四個部分:程序的分析與設計、c#網絡編程基礎(篇外篇)、聊天程序的實現模式、程序實現。
程序的分析與設計
1.明確程序功能
如果大家現在已經參加了工作,你的經理或者老板告訴你,“小王,我需要你開發一個聊天程序”。那么接下來該怎么做呢?你是不是在腦子里有個雛形,然后就直接打開vs2005開始設計窗體,編寫代碼了呢?在開始之前,我們首先需要進行軟件的分析與設計。就拿本例來說,如果只有這么一句話“一個聊天程序”,恐怕現在大家對這個“聊天程序”的概念就很模糊,它可以是像qq那樣的非常復雜的一個程序,也可以是很簡單的聊天程序;它可能只有在對方在線的時候才可以進行聊天,也可能進行留言;它可能每次將消息只能發往一個人,也可能允許發往多個人。它還可能有一些高級功能,比如向對方傳送文件等。所以我們首先需要進行分析,而不是一上手就開始做,而分析的第一步,就是搞清楚程序的功能是什么,它能夠做些什么。在這一步,我們的任務是了解程序需要做什么,而不是如何去做。
了解程序需要做什么,我們可以從兩方面入手,接下來我們分別討論。
1.1請求客戶提供更詳細信息
我們可以做的第一件事就是請求客戶提供更加詳細的信息。盡管你的經理或老板是你的上司,但在這個例子中,他就是你的客戶(當然通常情況下,客戶是公司外部委托公司開發軟件的人或單位)。當遇到上面這種情況,我們只有少得可憐的一條信息“一個聊天程序”,首先可以做的,就是請求客戶提供更加確切的信息。比如,你問經理“對這個程序的功能能不能提供一些更具體的信息?”。他可能會像這樣回答:“哦,很簡單,可以登錄聊天程序,登錄的時候能夠通知其他在線用戶,然后與在線的用戶進行對話,如果不想對話了,就注銷或者直接關閉,就這些吧。”
有了上面這段話,我們就又可以得出下面幾個需求:
1.程序可以進行登錄。
2.登錄后可以通知其他在線用戶。
3.可以與其他用戶進行對話。
4.可以注銷或者關閉。
1.2對于用戶需求進行提問,并進行總結
經常會有這樣的情況:可能客戶給出的需求仍然不夠細致,或者客戶自己本身對于需求就很模糊,此時我們需要做的就是針對用戶上面給出的信息進行提問。接下來我就看看如何對上面的需求進行提問,我們至少可以向經理提出以下問題:
note:這里我穿插一個我在見到的一個印象比較深刻的例子:客戶往往向你表達了強烈的意愿他多么多么想擁有一個屬于自己的網站,但是,他卻沒有告訴你網站都有哪些內容、欄目,可以做什么。而作為開發者,我們顯然關心的是后者。
1.登錄時需要提供哪些內容?需不需要提供密碼?
2.允許多少人同時在線聊天?
3.與在線用戶聊天時,可以將一條消息發給一個用戶,還是可以一次將消息發給多個用戶?
4.聊天時發送的消息包括哪些內容?
5.注銷和關閉有什么區別?
6.注銷和關閉對對方需不需要給對方提示?
由于這是一個范例程序,而我在為大家講述,所以我只能再充當一下客戶的角色,來回答上面的問題:
1.登錄時只需要提供用戶名稱就可以了,不需要輸入密碼。
2.允許兩個人在線聊天。(這里我們只講述這種簡單情況,允許多人聊天需要使用多線程)
3.因為只有兩個人,那么自然是只能發給一個用戶了。
4.聊天發送的消息包括:用戶名稱、發送時間還有正文。
5.注銷并不關閉程序,只是離開了對話,可以再次進行連接。關閉則是退出整個應用程序。
6.注銷和關閉均需要給對方提示。
好了,有了上面這些信息我們基本上就掌握了程序需要完成的功能,那么接下來做什么?開始編碼了么?上面的這些屬于業務流程,除非你對它已經非常熟悉,或者程序非常的小,那么可以對它進行編碼,但是實際中,我們最好再編寫一些用例,這樣會使程序的流程更加的清楚。
1.3編寫用例
通常一個用例對應一個功能或者叫需求,它是程序的一個執行路徑或者執行流程。編寫用例的思路是:假設你已經有了這樣一個聊天程序,那么你應該如何使用它?我們的使用步驟,就是一個用例。用例的特點就每次只針對程序的一個功能編寫,最后根據用例編寫代碼,最終完成程序的開發。我們這里的需求只有簡單的幾個:登錄,發送消息,接收消息,注銷或關閉,上面的分析是對這幾點功能的一個明確。接下來我們首先編寫第一個用例:登錄。
在開始之前,我們先明確一個概念:客戶端,服務端。因為這個程序只是在兩個人(機器)之間聊天,那么我們大致可以繪出這樣一個圖來:
我們期望用戶a和用戶b進行對話,那么我們就需要在它們之間建立起連接。盡管“用戶a”和“用戶b”的地位是對等的,但按照約定俗稱的說法:我們將發起連接請求的一方稱為客戶端(或叫本地),另一端稱為服務端(或叫遠程)。所以我們的登錄過程,就是“用戶a”連接到“用戶b”的過程,或者說客戶端(本地)連接到服務端(遠程)的過程。在分析這個程序的過程中,我們總是將其分為兩部分,一部分為發起連接、發送消息的一方(本地),一方為接受連接、接收消息的一方(遠程)。
登錄和連接(本地) | |
主路徑 | 可選路徑 |
1.打開應用程序,顯示登錄窗口 | |
2.輸入用戶名 | |
3.點擊“登錄”按鈕,登錄成功 |
3.“登錄”失敗
如果用戶名為空,重新進入第2步。 |
4.顯示主窗口,顯示登錄的用戶名稱 | |
5.點擊“連接”,連接至遠程 | |
6.連接成功 6.1提示用戶,連接已經成功。 |
6.連接失敗 6.1 提示用戶,連接不成功 |
5.在用戶界面變更控件狀態 5.2連接為灰色,表示已經連接 5.3注銷為亮色,表示可以注銷 5.4發送為亮色,表示可以發消息 |
這里我們的用例名稱為登錄和連接,但是后面我們又打了一個括號,寫著“本地”,它的意思是說,登錄和連接是客戶端,也就是發起連接的一方采取的動作。同樣,我們需要寫下當客戶端連接至服務端時,服務端采取的動作。
登錄和連接(遠程) | |
主路徑 | 可選路徑 |
1-4 同客戶端 | |
5.等待連接 | |
6.如果有連接,自動在用戶界面顯示“遠程主機連接成功” |
接下來我們來看發送消息。在發送消息時,已經是登錄了的,也就是“用戶a”、“用戶b”已經做好了連接,所以我們現在就可以只關注發送這一過程:
發送消息(本地) | |
主路徑 | 可選路徑 |
1.輸入消息 | |
2.點擊發送按鈕 | 2.沒有輸入消息,重新回到第1步 |
3.在用戶界面上顯示發出的消息 |
3.服務端已經斷開連接或者關閉
3.1在客戶端用戶界面上顯示錯誤消息 |
然后我們看一下接收消息,此時我們只關心接收消息這一部分。
接收消息(遠程) | |
主路徑 | 可選路徑 |
1.偵聽到客戶端發來的消息,自動顯示在用戶界面上。 |
注意到這樣一點:當遠程主機向本地返回消息時,它的用例又變為了上面的用例“發送消息(本地)”。因為它們的角色已經互換了。
最后看一下注銷,我們這里研究的是當我們在本地機器點擊“注銷”后,雙方采取的動作:
注銷(本地主動) | |
主路徑 | 可選路徑 |
1.點擊注銷按鈕,斷開與遠程的連接 | |
2.在用戶界面顯示已經注銷 | |
3.更改控件狀態 3.1注銷為灰色,表示已經注銷 3.2連接為亮色,表示可以連接 3.3發送為灰色,表示無法發送 |
與此對應,服務端應該作出反應:
注銷(遠程被動) | |
主路徑 | 可選路徑 |
1.自動顯示遠程用戶已經斷開連接。 |
注意到一點:當遠程主動注銷時,它采取的動作為上面的“本地主動”,本地采取的動作則為這里的“遠程被動”。
至此,應用程序的功能分析和用例編寫就告一段落了,通過上面這些表格,之后再繼續編寫程序變得容易了許多。另外還需要記得,用例只能為你提供一個操作步驟的指導,在實現的過程中,因為技術等方面的原因,可能還會有少量的修改。如果修改量很大,可以重新修改用例;如果修改量不大,那么就可以直接編碼。這是一個迭代的過程,也沒有一定的標準,總之是以高效和合適為標準。
2.分析與設計
我們已經很清楚地知道了程序需要做些什么,盡管現在還不知道該如何去做。我們甚至可以編寫出這個程序所需要的接口,以后編寫代碼的時候,我們只要去實現這些接口就可以了。這也符合面向接口編程的原則。另外我們注意到,盡管這是一個聊天程序,但是卻可以明確地劃分為兩部分,一部分發送消息,一部分接收消息。另外注意上面標識為自動的語句,它們暗示這個操作需要通過事件的通知機制來完成。關于委托和事件,可以參考這兩篇文章:
- c#中的委托和事件 - 委托和事件的入門文章,同時捎帶講述了observer設計模式和.net的事件模型
- c#中的委托和事件(續) - 委托和事件更深入的一些問題,包括異常、超時的處理,以及使用委托來異步調用方法。
2.1消息message
首先我們可以定義消息,前面我們已經明確了消息包含三個部分:用戶名、時間、內容,所以我們可以定義一個結構來表示這個消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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消息發送方imessagesender
從上面我們可以看出,消息發送方主要包含這樣幾個功能:登錄、連接、發送消息、注銷。另外在連接成功或失敗時還要通知用戶界面,發送消息成功或失敗時也需要通知用戶界面,因此,我們可以讓連接和發送消息返回一個布爾類型的值,當它為真時表示連接或發送成功,反之則為失敗。因為登錄沒有任何的業務邏輯,僅僅是記錄控件的值并進行顯示,所以我不打算將它寫到接口中。因此我們可以得出它的接口大致如下:
1
2
3
4
5
|
public interface imessagesender { bool connect(ipaddress ip, int port); // 連接到服務端 bool sendmessage(message msg); // 發送用戶 void signout(); // 注銷系統 } |
2.3消息接收方imessagereceiver
而對于消息接收方,從上面我們可以看出,它的操作全是被動的:客戶端連接時自動提示,客戶端連接丟失時顯示自動提示,偵聽到消息時自動提示。注意到上面三個詞都用了“自動”來修飾,在c#中,可以定義委托和事件,用于當程序中某種情況發生時,通知另外一個對象。在這里,程序即是我們的imessagereceiver,某種情況就是上面的三種情況,而另外一個對象則為我們的用戶界面。因此,我們現在首先需要定義三個委托:
1
2
3
|
public delegate void messagereceivedeventhandler( string msg); public delegate void clientconnectedeventhandler(ipendpoint endpoint); public delegate void connectionlosteventhandler( string info); |
接下來,我們注意到接收方需要偵聽消息,因此我們需要在接口中定義的方法是startlisten()和stoplisten()方法,這兩個方法是典型的技術相關,而不是業務相關,所以從用例中是看不出來的,可能大家現在對這兩個方法是做什么的還不清楚,沒有關系,我們現在并不寫實現,而定義接口并不需要什么成本,我們寫下imessagereceiver的接口定義:
1
2
3
4
5
6
7
|
public interface imessagereceiver { event messagereceivedeventhandler messagereceived; // 接收到發來的消息 event connectionlosteventhandler clientlost; // 遠程主動斷開連接 event clientconnectedeventhandler clientconnected; // 遠程連接到了本地 void startlisten(); // 開始偵聽端口 void stoplisten(); // 停止偵聽端口 } |
我記得曾經看過有篇文章說過,最好不要在接口中定義事件,但是我忘了他的理由了,所以本文還是將事件定義在了接口中。
2.4主程序talker
而我們的主程序是既可以發送,又可以接收,一般來說,如果一個類像獲得其他類的能力,以采用兩種方法:繼承和復合。因為c#中沒有多重繼承,所以我們無法同時繼承實現了imessagereceiver和imessagesender的類。那么我們可以采用復合,將它們作為類成員包含在talker內部:
1
2
3
4
5
6
7
8
9
|
public class talker { private imessagereceiver receiver; private imessagesender sender; public talker(imessagereceiver receiver, imessagesender sender) { this .receiver = receiver; this .sender = sender; } } |
現在,我們的程序大體框架已經完成,接下來要關注的就是如何實現它,現在讓我們由設計走入實現,看看實現一個網絡聊天程序,我們需要掌握的技術吧。
c#網絡編程基礎(篇外篇)
這部分的內容請參考 c#網絡編程 系列文章,共5個部分較為詳細的講述了基于socket的網絡編程的初步內容。
編寫程序代碼
如果你已經看完了上面一節c#網絡編程,那么本章完全沒有講解的必要了,所以我只列出代碼,對個別值得注意的地方稍微地講述一下。首先需要了解的就是,我們采用的是三個模式中開發起來難度較大的一種,無服務器參與的模式。還有就是我們沒有使用廣播消息,所以需要提前知道連接到的遠程主機的地址和端口號。
1.實現imessagesender接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public class messagesender : imessagesender { tcpclient client; stream streamtoserver; // 連接至遠程 public bool connect(ipaddress ip, int port) { try { client = new tcpclient(); client.connect(ip, port); streamtoserver = client.getstream(); // 獲取連接至遠程的流 return true ; } catch { return false ; } } // 發送消息 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(); } } |
這段代碼可以用樸實無華來形容,所以我們直接看下一段。
2.實現imessagereceiver接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
public delegate void portnumberreadyeventhandler( int portnumber); public class messagereceiver : imessagereceiver { public event messagereceivedeventhandler messagereceived; public event connectionlosteventhandler clientlost; public event clientconnectedeventhandler clientconnected; // 當端口號ok的時候調用 -- 需要告訴用戶界面使用了哪個端口號在偵聽 // 這里是業務上體現不出來,在實現中才能體現出來的 public event portnumberreadyeventhandler portnumberready; private thread workerthread; private tcplistener listener; public messagereceiver() { ((imessagereceiver) this ).startlisten(); } // 開始偵聽:顯示實現接口 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(); // 獲取端口號 ipendpoint endpoint = listener.localendpoint as ipendpoint; int portnumber = endpoint.port; if (portnumberready != null ) { portnumberready(portnumber); // 端口號已經ok,通知用戶界面 } while ( true ) { tcpclient remoteclient; try { remoteclient = listener.accepttcpclient(); } catch { break ; } if (clientconnected != null ) { // 連接至本機的遠程端口 endpoint = remoteclient.client.remoteendpoint as ipendpoint; clientconnected(endpoint); // 通知用戶界面遠程客戶連接 } 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( "客戶端已斷開連接" ); } string msg = encoding.unicode.getstring(buffer, 0, bytesread); if (messagereceived != null ) { messagereceived(msg); // 已經收到消息 } } catch (exception ex) { if (clientlost != null ) { clientlost(ex.message); // 客戶連接丟失 break ; // 退出循環 } } } } } // 停止偵聽端口 public void stoplisten() { try { listener.stop(); listener = null ; workerthread.abort(); } catch { } } } |
這里需要注意的有這樣幾點:我們startlisten()為顯式實現接口,因為只能通過接口才能調用此方法,接口的實現類看不到此方法;這通常是對于一個接口采用兩種實現方式時使用的,但這里我只是不希望messagereceiver類型的客戶調用它,因為在messagereceiver的構造函數中它已經調用了startlisten。意思是說,我們希望這個類型一旦創建,就立即開始工作。我們使用了兩個嵌套的while循環,這個它可以為多個客戶端的多次請求服務,但是因為是同步操作,只要有一個客戶端連接著,我們的后臺線程就會陷入第二個循環中無法自拔。所以結果是:如果有一個客戶端已經連接上了,其它客戶端即使連接了也無法對它應答。最后需要注意的就是四個事件的使用,為了向用戶提供偵聽的端口號以進行連接,我又定義了一個portnumberreadyeventhandler委托。
3.實現talker類
talker類是最平庸的一個類,它的全部功能就是將操作委托給實際的imessagereceiver和imessagesender。定義這兩個接口的好處也從這里可以看出來:如果日后想重新實現這個程序,所有windows窗體的代碼和talker的代碼都不需要修改,只需要針對這兩個接口編程就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
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; } } // 注意這個事件 public event portnumberreadyeventhandler portnumberready { add { ((messagereceiver)receiver).portnumberready += value; } remove { ((messagereceiver)receiver).portnumberready -= value; } } // 連接遠程 - 使用主機名 public bool connectbyhost( string hostname, int port) { ipaddress[] ips = dns.gethostaddresses(hostname); return sender.connect(ips[0], port); } // 連接遠程 - 使用ip public bool connectbyip( string ip, int port) { ipaddress ipaddress; try { ipaddress = ipaddress.parse(ip); } catch { return false ; } return sender.connect(ipaddress, port); } // 發送消息 public bool sendmessage(message msg) { return sender.sendmessage(msg); } // 釋放資源,停止偵聽 public void dispose() { try { sender.signout(); receiver.stoplisten(); } catch { } } // 注銷 public void signout() { try { sender.signout(); } catch { } } } |
4.設計窗體,編寫窗體事件代碼
現在我們開始設計窗體,我已經設計好了,現在可以先進行一下預覽:
這里需要注意的就是上面的偵聽端口,是程序接收消息時的偵聽端口,也就是imessagereceiver所使用的。其他的沒有什么好說的,下來我們直接看一下代碼,控件的命名是自解釋的,我就不多說什么了。唯一要稍微說明下的是txtmessage指的是下面發送消息的文本框,txtcontent指上面的消息記錄文本框:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
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() { } // 端口號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); } // 有客戶端連接到本機 void talker_clientconnected(ipendpoint endpoint) { clientconnectedeventhandler del = delegate (ipendpoint end) { iphostentry host = dns.gethostentry(end.address); txtcontent.text += string .format( "system[{0}]:\r\n遠程主機{1}連接至本地。\r\n" , datetime.now, end); }; txtcontent.invoke(del, endpoint); } // 客戶端連接斷開 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); } // 發送消息 private void btnsend_click( object sender, eventargs e) { if ( string .isnullorempty(txtmessage.text)) { messagebox.show( "請輸入內容!" ); 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遠程主機已斷開連接\r\n" , datetime.now); disconnectstatus(); } } // 點擊連接 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( "主機名稱或地址不能為空" ); } try { port = convert.toint32(txtport.text); } catch { messagebox.show( "端口號不能為空,且必須為數字" ); return ; } if (talker.connectbyhost(host, port)) { connectstatus(); txtcontent.text += string .format( "system[{0}]:\r\n已成功連接至遠程\r\n" , datetime.now); return ; } if (talker.connectbyip(ip, port)){ connectstatus(); txtcontent.text += string .format( "system[{0}]:\r\n已成功連接至遠程\r\n" , datetime.now); } else { messagebox.show( "遠程主機不存在,或者拒絕連接!" ); } txtmessage.focus(); } // 關閉按鈕點按 private void btnclose_click( object sender, eventargs e) { try { talker.dispose(); application.exit(); } catch { } } // 直接點擊右上角的叉 private void primaryform_formclosing( object sender, formclosingeventargs e) { try { talker.dispose(); application.exit(); } catch { } } // 點擊注銷 private void btnsignout_click( object sender, eventargs e) { talker.signout(); disconnectstatus(); txtcontent.text += string .format( "system[{0}]:\r\n已經注銷\r\n" ,datetime.now); } private void btnclear_click( object sender, eventargs e) { txtcontent.clear(); } } |
在上面代碼中,分別通過四個方法訂閱了四個事件,以實現自動通知的機制。最后需要注意的就是signout()和dispose()的區分。signout()只是斷開連接,dispose()則是離開應用程序。
總結
這篇文章簡單地分析、設計及實現了一個聊天程序。這個程序只是對無服務器模式實現聊天的一個嘗試。我們分析了需求,隨后編寫了幾個用例,并對本地、遠程的概念做了定義,接著編寫了程序接口并最終實現了它。這個程序還有很嚴重的不足:它無法實現自動上線通知,而必須要事先知道端口號并進行手動連接。為了實現一個功能強大且開發容易的程序,更好的辦法是使用集中型服務器模式。
感謝閱讀,希望這篇文章能對你有所幫助。