C#網絡編程系列文章計劃簡單地講述網絡編程方面的基礎知識,由於本人在這方面功力有限,所以只能提供一些初步的入門知識,希望能對剛開始學習的朋友提供一些幫助。如果想要更加深入的內容,可以參考相關書籍。
本文是該系列第一篇,主要講述了基於套接字(Socket)進行網絡編程的基本概念,其中包括TCP協議、套接字、聊天程序的三種開發模式,以及兩個基本操作:偵聽端口、連接遠程服務端;第二篇講述了一個簡單的范例:從客戶端傳輸字符串到服務端,服務端接收並打印字符串,將字符串改為大寫,然後再將字符串回發到客戶端,客戶端最後打印傳回的字符串;第三篇是第二篇的一個強化,講述了第二篇中沒有解決的一個問題,並使用了異步傳輸的方式來完成和第二篇同樣的功能;第四篇則演示了如何在客戶端與服務端之間收發文件;第五篇實現了一個能夠在線聊天並進行文件傳輸的聊天程序,實際上是對前面知識的一個綜合應用。
對於TCP協議我不想說太多東西,這屬於大學課程,又涉及計算機科學,而我不是“學院派”,對於這部分內容,我覺得作為開發人員,只需要掌握與程序相關的概念就可以了,不需要做太艱深的研究。
我們首先知道TCP是面向連接的,它的意思是說兩個遠程主機(或者叫進程,因為實際上遠程通信是進程之間的通信,而進程則是運行中的程序),必須首先進行一個握手過程,確認連接成功,之後才能傳輸實際的數據。比如說進程A想將字符串“Its a fine day today”發給進程B,它首先要建立連接。在這一過程中,它首先需要知道進程B的位置(主機地址和端口號)。隨後發送一個不包含實際數據的請求報文,我們可以將這個報文稱之為“hello”。如果進程B接收到了這個“hello”,就向進程A回復一個“hello”,進程A隨後才發送實際的數據“Its a fine day today”。
關於TCP第二個需要了解的,就是它是全雙工的。意思是說如果兩個主機上的進程(比如進程A、進程B),一旦建立好連接,那麼數據就既可以由A流向B,也可以由B流向A。除此以外,它還是點對點的,意思是說一個TCP連接總是兩者之間的,在發送中,通過一個連接將數據發給多個接收方是不可能的。TCP還有一個特性,就是稱為可靠的數據傳輸,意思是連接建立後,數據的發送一定能夠到達,並且是有序的,就是說發的時候你發了ABC,那麼收的一方收到的也一定是ABC,而不會是BCA或者別的什麼。
編程中與TCP相關的最重要的一個概念就是套接字。我們應該知道網絡七層協議,如果我們將上面的應用程、表示層、會話層籠統地算作一層(有的教材便是如此劃分的),那麼我們編寫的網絡應用程序就位於應用層,而大家知道TCP是屬於傳輸層的協議,那麼我們在應用層如何使用傳輸層的服務呢(消息發送或者文件上傳下載)?大家知道在應用程序中我們用接口來分離實現,在應用層和傳輸層之間,則是使用套接字來進行分離。它就像是傳輸層為應用層開的一個小口,應用程序通過這個小口向遠程發送數據,或者接收遠程發來的數據;而這個小口以內,也就是數據進入這個口之後,或者數據從這個口出來之前,我們是不知道也不需要知道的,我們也不會關心它如何傳輸,這屬於網絡其它層次的工作。
舉個例子,如果你想寫封郵件發給遠方的朋友,那麼你如何寫信、將信打包,屬於應用層,信怎麼寫,怎麼打包完全由我們做主;而當我們將信投入郵筒時,郵筒的那個口就是套接字,在進入套接字之後,就是傳輸層、網絡層等(郵局、公路交管或者航線等)其它層次的工作了。我們從來不會去關心信是如何從西安發往北京的,我們只知道寫好了投入郵筒就OK了。可以用下面這兩幅圖來表示它:
注意在上面圖中,兩個主機是對等的,但是按照約定,我們將發起請求的一方稱為客戶端,將另一端稱為服務端。可以看出兩個程序之間的對話是通過套接字這個出入口來完成的,實際上套接字包含的最重要的也就是兩個信息:連接至遠程的本地的端口信息(本機地址和端口號),連接到的遠程的端口信息(遠程地址和端口號)。注意上面詞語的微妙變化,一個是本地地址,一個是遠程地址。
這裡又出現了了一個名詞端口。一般來說我們的計算機上運行著非常多的應用程序,它們可能都需要同遠程主機打交道,所以遠程主機就需要有一個ID來標識它想與本地機器上的哪個應用程序打交道,這裡的ID就是端口。將端口分配給一個應用程序,那麼來自這個端口的數據則總是針對這個應用程序的。有這樣一個很好的例子:可以將主機地址想象為電話號碼,而將端口號想象為分機號。
在.NET中,盡管我們可以直接對套接字編程,但是.NET提供了兩個類將對套接字的編程進行了一個封裝,使我們的使用能夠更加方便,這兩個類是TcpClient和TcpListener,它與套接字的關系如下:
從上面圖中可以看出TcpClient和TcpListener對套接字進行了封裝。從中也可以看出,TcpListener用於接受連接請求,而TcpClient則用於接收和發送流數據。這幅圖的意思是TcpListener持續地保持對端口的偵聽,一旦收到一個連接請求後,就可以獲得一個TcpClient對象,而對於數據的發送和接收都有TcpClient去完成。此時,TcpListener並沒有停止工作,它始終持續地保持對端口的偵聽狀態。
我們考慮這樣一種情況:兩台主機,主機A和主機B,起初它們誰也不知道誰在哪兒,當它們想要進行對話時,總是需要有一方發起連接,而另一方則需要對本機的某一端口進行偵聽。而在偵聽方收到連接請求、並建立起連接以後,它們之間進行收發數據時,發起連接的一方並不需要再進行偵聽。因為連接是全雙工的,它可以使用現有的連接進行收發數據。而我們前面已經做了定義:將發起連接的一方稱為客戶端,另一段稱為服務端,則現在可以得出:總是服務端在使用TcpListener類,因為它需要建立起一個初始的連接。
實現一個網絡聊天程序本應是最後一篇文章的內容,也是本系列最後的一個程序,來作為一個終結。但是我想後面更多的是編碼,講述的內容應該不會太多,所以還是把講述的東西都放到這裡吧。
當采用這種模式時,即是所謂的完全點對點模式,此時每台計算機本身也是服務器,因為它需要進行端口的偵聽。實現這個模式的難點是:各個主機(或終端)之間如何知道其它主機的存在?此時通常的做法是當某一主機上線時,使用UDP協議進行一個廣播(Broadcast),通過這種方式來“告知”其它主機自己已經在線並說明位置,收到廣播的主機發回一個應答,此時主機便知道其他主機的存在。這種方式我個人並不喜歡,但在 C#編寫簡單的聊天程序 這篇文章中,我使用了這種模式,可惜的是我沒有實現廣播,所以還很不完善。
第二種方式較好的解決了上面的問題,它引入了服務器,由這個服務器來專門進行廣播。服務器持續保持對端口的偵聽狀態,每當有主機上線時,首先連接至服務器,服務器收到連接後,將該主機的位置(地址和端口號)發往其他在線主機(綠色箭頭標識)。這樣其他主機便知道該主機已上線,並知道其所在位置,從而可以進行連接和對話。在服務器進行了廣播之後,因為各個主機已經知道了其他主機的位置,因此主機之間的對話就不再通過服務器(黑色箭頭表示),而是直接進行連接。因此,使用這種模式時,各個主機依然需要保持對端口的偵聽。在某台主機離線時,與登錄時的模式類似,服務器會收到通知,然後轉告給其他的主機。
第三種模式是我覺得最簡單也最實用的一種,主機的登錄與離線與第二種模式相同。注意到每台主機在上線時首先就與服務器建立了連接,那麼從主機A發往主機B發送消息,就可以通過這樣一條路徑,主機A --> 服務器 --> 主機B,通過這種方式,各個主機不需要在對端口進行偵聽,而只需要服務器進行偵聽就可以了,大大地簡化了開發。
而對於一些較大的文件,比如說圖片或者文件,如果想由主機A發往主機B,如果通過服務器進行傳輸效率會比較低,此時可以臨時搭建一個主機A至主機B之間的連接,用於傳輸大文件。當文件傳輸結束之後再關閉連接(桔紅色箭頭標識)。
除此以外,由於消息都經過服務器,所以服務器還可以緩存主機間的對話,即是說當主機A發往主機B時,如果主機B已經離線,則服務器可以對消息進行緩存,當主機B下次連接到服務器時,服務器自動將緩存的消息發給主機B。
本系列文章最後采用的即是此種模式,不過沒有實現過多復雜的功能。接下來我們的理論知識告一段落,開始下一階段――漫長的編碼。
接下來我們開始編寫一些實際的代碼,第一步就是開啟對本地機器上某一端口的偵聽。首先創建一個控制台應用程序,將項目名稱命名為ServerConsole,它代表我們的服務端。如果想要與外界進行通信,第一件要做的事情就是開啟對端口的偵聽,這就像為計算機打開了一個“門”,所有向這個&ldquo