通訊機制與流程
UDP是無線電,TCP是電話
我們可以將UDP的通訊方式比喻為無線電,只要調整好頻道(IP+Port),不必管對方是不是有在聽?就可以發出訊息了!我們也必須等到對方有回應時才知道對方聽到了!但是相對的,TCP網路通訊的方式就類似我們日常撥打電話,必須先撥號,等對方接聽(接受連線)之後通訊才能開始!沒有成功連線就開始送訊息不僅是無效的,以程式來說還會當掉(錯誤)!

為什麼一定要用TCP?
真正的線上遊戲與大多數的網路通訊,包括網頁、Email、FTP或MSN,除了影音直播之外都是使用TCP通訊模式的!原因很簡單,就是它可以保證通訊內容正確。如果我們可以親眼見到網路線上的『交通狀況』就會知道網路本身是充滿變數,相當複雜甚至很混亂的!網路線是用電壓高低傳遞訊號,如果碰到附近有人開關電器或碰到閃電,甚至只是有人踢到電線都有可能讓傳遞的訊號被干擾,甚至遺失!而且網路訊號是會經過很多節點(電腦或網路設備)轉傳的,如果路徑上某個節點當機,甚至只是線路太老舊有雜訊,訊號都有可能遺失或錯亂。TCP的設計就是會記錄與監視每一個封包的傳送過程與結果,發現任何封包該到沒到,或者檢查內容發現可能有問題,都會重傳,務必讓通訊內容完全正確。如何達到此目的的詳細機制不在此說明,但大家應該了解對於正牌的線上遊戲來說,使用程式較麻煩的TCP是一定要的

伺服端與客戶端的分工
在TCP模式發展的應用軟體中,都必須同時有伺服端與客戶端兩個程式,我們需要完成的程式功能可以由兩者分攤,但是多數時候是很多客戶經由一個伺服端互相連繫,無疑的伺服端程式的負擔較重。因此應該盡量簡化伺服端的工作,通常只負責通訊與客戶資料的管理。譬如各種連線遊戲的複雜動作與圖檔運用,如果可以都應該盡量由客戶端程式自行儲存與處裡。因此本課程後續的多個單元中伺服器的程式都與本單元相差無幾,主要就是接受連線,並轉傳客戶之間的訊息而已。本單元其實主要是介紹伺服端程式的工作,看起來複雜,但是之後就不會有太大變動,基本上可以沿用於所有範例,遊戲介面與互動機制則都會寫作於客戶端程式之中,不必擔心每個範例都必須同時製作複雜的兩個程式。

建立伺服器程式
TCP通訊與UDP最大的不同之處是必須有明確的伺服器客戶端軟體區隔!伺服器如同電話總機,負責接收客戶的連線需求,與所有上線的客戶建立直接連線,當然客戶離線時也要能偵測到而移除該連線,不然伺服器電腦的記憶體很快就會被塞爆的!本單元先不介紹如何做實際通話,只先完成一組伺服器與客戶端的程式,伺服器可以監聽客戶連線要求,接受連線與移除連線,並能顯示誰上線誰離線。首先請開啟一個程式專案,建立如下表單介面:

啟動伺服器鈕是Button1,顯示Server IP的是TextBox1,顯示Port的是TextBox2,ListBox1用來顯示誰在線上的名單。

匯入命名空間
如同UDP程式,我們也需要在程式碼頁面最頂端匯入以下幾個命名空間:

在此我們一樣要用到多執行緒,事實上在伺服器與多客戶同時連線時,每一個客戶與伺服器之間都需要一個獨立的執行緒,所以在此會用到的將不只是兩個執行緒而已,連線數目越多就會有越多執行緒。當然實務上任何電腦都不可能擁有無限多的執行緒,總會有記憶體不足的時候,所以線上遊戲才會常常需要加掛伺服器,就是用多部電腦來分擔伺服器的工作囉!

公用物件的宣告
請先在Class區塊內最前面宣告建立以下物件

說明:
1. Server(TcpListener)是專用於伺服端接受客戶連線要求的物件,可以想像它是一個電話總機
2. Server以一個獨立的執行緒 Th_Svr執行監聽工作,這樣其他表單物件才能正常工作,譬如顯示線上名單。
2. Server 收到連線要求時會幫客戶建立一個Client連線物件,並建立一個獨立執行緒 Th_Clt讓客戶與伺服器保持溝通。
3.但是每次建立的新客戶連線都是Client與 Th_Clt,不會重複嗎?事實上是程式會將兩者拷貝到新執行緒內部。因此兩者有如轉接用的計程車,可以重複使用!
4. HT是一個稱為雜湊表(HashTable)的特殊集合物件,一般陣列是用序號取得成員,像A(2)表示陣列A的第三個元素;但是雜湊表是使用 "key" 來辨識成員,譬如HT的某個成員是:key="國文",value=90。HT("國文")就等於90了!
5. 本範例中 HT 用於存放所有線上客戶的連線物件(Socket),key是使用者名稱,value 就是該使用者的連線物件。所以要與使用者"A"通訊的物件就是HT("A")了!


啟動伺服器的程式

Button1的程式與之前的UDP啟動監聽非常相似!先宣告忽略跨執行緒錯誤,接著建立並啟動監聽執行緒。
啟動之前我們設定執行緒於背景(Background)執行,這樣可以讓它關閉時不會檢查通訊錯誤,較容易直接關閉。
此處用於監聽的副程式 ServerSub 只負責接收使用者的連線並建立專屬的(監聽)執行緒,至於處理實際通訊動作的程式則在個別用戶的監聽程式 Listen 之中,ServerSub 完整程式碼如下:

1. ServerSub首先確定伺服器監聽連線用的網路端點EP。
2.接著啟動可接受連線的TcpListener物件,設定最高連線數100,以免無限制地連線最終可能導致資源耗盡而當機。
*.此處的TcpListener只負責接收使用者的連線並建立專屬的(監聽)執行緒,至於處理實際通訊動作的程式則在個別用戶的監聽程式 Listen 之中。
3. 有連線要求時接受連線產生一個 Client連線物件,裡面會包含與此客戶連線的資訊(IP與Port等)。
4.隨即建立一個執行緒,設定於背景執行,新執行緒的程式寫在 Listen 副程式中!內容如下:

1. 首先將 Client 拷貝到執行緒內的 sck 物件,此時 Client 就除役了,可以讓前面的 ServerSub 程式再次使用於新客戶。
2. 執行緒 Th_Clt 也拷貝到 Th 讓公用的 Th_Clt 可以給下一個使用者使用。
3. 接著是無限等待的迴圈 Do While True...Loop,表示這個客戶連線會一直保持運作。
4. B(1023)只是預先宣告一個『應該大於』通訊內容的固定大小陣列。因為目前版本的TCP接收函數不會自動產生動態大小的Byte陣列(UDP可以),因此必須先宣告好夠大的陣列準備承接訊息。
5. inLen是實際傳來的訊息長度,譬如實際傳入50個byte,那麼B(1023)之中只有前50個Byte的值是有意義的,翻譯成字串時就只須翻譯前50個byte成為字串。
6. Msg是完整的訊息字串,為方便對不同目的的訊息分流處理,第一個字元是命令碼(Cmd),其餘就是訊息內容 Str了!
7. 依據命令碼:"0"是新上線的客戶傳來自己的名字,所以此地Str就是使用者名稱,應該加入ListBox1,同時此連線使用的物件(sck)也要記錄到HT雜湊表,以備稍後需要傳訊給此客戶使用(如同記下某人的電話號碼)。
8. 命令碼"9"是客戶宣告離線,此客戶的名稱與通訊物件會被移出名單,使用的連線物件與執行緒也會被關閉。
9. 此處使用Try結構避免意外錯誤,通常是客戶端程式未正式關閉連線就結束程式,這在測試階段時常發生,但是這種語法其實會拖慢執行速度,當客戶端程式建立完善之後可以將此語法移除。
10. HT表在此存放著每個客戶的連線物件,雖然本範例其實沒有使用,但後續的TCP程式要對客戶發出訊息時都會用到。

關閉伺服器的程式
表單被關閉時當然就是要關閉伺服器了,程式如下寫在Form_Closing事件中:

此處程式碼非常簡單,就是將此專案的所有執行緒全部關閉!因為本範例各個程式進行中增加的執行緒都設定為背景執行(Isbackground=True),才能這樣關閉,否則執行緒關閉時會進行一些檢查導致有時無法順利關閉。

至此伺服端程式建置完成,可以執行程式按下啟動監聽,然後關閉表單,如果都不會當掉就算初步成功,是否可以接受連線必須等客戶端程式建置完成後才能測試。

客戶端程式表單設計
文字方塊依序為TextBox1用於輸入伺服器IP,TextBox2為Port,TextBox3為姓名,登入伺服器按鈕為Button1。


匯入命名空間

匯入的命名空間少了執行緒相關的System.Threading,因為本程式只負責發出上線與離線訊息,並不做接聽的動作,因此暫時不需要額外的執行緒。

公用變數宣告

T是本程式的連線物件,Socket就是所有網路連線物件的基礎物件,它有很大的彈性設定為各種連線模式,包括UDP通訊。事實上.NET函式庫中有很多Socket的衍生物件,包括專用於TCP連線的TcpClient物件,之前我們也是用UdpClient物件來作UDP通訊,但是每種物件使用的方法還是稍有不同,在此的選擇都是以整體程式碼簡單易理解為主要考量。

傳送訊息的Send副程式

傳送訊息的 Send 程式與 UDP 模式十分相似,先翻譯字串為Byte陣列之後用Socket物件T送出,只是多了一個宣告無旗標的參數而已。

登入伺服器的程式

1. 登入伺服器的程式是先建立伺服器的網路端點 EP (IP + Port),接著宣告建立TCP模式的連線物件 T
2. 嘗試連線到伺服器(T.Connect(EP)),此時很多原因會不成功,譬如網路沒通、伺服器未開、或者IP寫錯等等,都會顯示不能連線的訊息(MsgBox)。
3. 如果成功會隨即送出自己的使用者名稱並宣告上線("0"+User)。

關閉視窗與連線的程式

關閉視窗(FormClosing事件)時必須要告訴伺服器你要離線了("9"+User),讓伺服器處理你的離線狀態,隨即關閉此連線。如果你的程式已經用T物件連線到伺服器,離開程式前未執行T.Close就直接關閉程式或關機,伺服端就會產生一個錯誤,伺服器程式若未是當處理就會讓伺服器當機了!

測試連線與離線
此時先打開伺服器啟動監聽,再開啟客戶端程式,按下登入伺服器按鍵,畫面大致如下,就是成功上線了!

此時再關閉客戶端視窗時應該伺服器端的 ListBox1顯示的 User1也會消失才對!你也可以嘗試用多個客戶端程式實體用不同使用者名稱登入看看效果!如果一切都OK我們就可以開始寫真正的聊天室程式了!