.NET 4.0網路開發入門之旅--
與Socket的第一次“約會”
註:
這是一個針對
網路開發領域初學者
的系列文章,可作為《.NET 4.0 物件導向編程漫談
》一書的擴充閱讀,寫作過程中我假設讀者可以對照閱讀此書的相關章節,不再浪費筆墨重複介紹相關的內容。
對於其他類型的讀者,除非您已經有相應的.NET 技術背景與一定的開發經驗,否則,閱讀中可能會遇到困難。
我希望這系列文章能讓讀者領略到網路開發的魅力!
另外,這些文章均為本人原創,請讀者尊重作者的勞動,我允許大家出於知識共用的目的自由轉載這些文章及相關樣本,但未經本人許可,請不要用於商業盈利目的。
本文如有錯誤,敬請回貼指正。
謝謝大家!
金旭亮
=================================================
點擊以下連結閱讀本系列前面的文章:
《
開篇語——
無網不勝》
《
IP知多少》
《我在“網” 中央
》
=================================================
在前面的文章中,我們已經介紹了使用.NET平台開發網路應用程式諸如IP地址、網路介面之類的背景知識,本文將介紹.NET網路應用程式的主角--Socket。如果把Socket比喻為一位“美女”,那麼有關她的“愛情故事”實在太多,而本系列後繼的文章,就圍繞著這位“美女”所展開。
1 Socket美女的“家庭背景”
Socket,中文譯為“通訊端”,最早在UNIX中引入並得到廣泛應用,後來微軟在設計Windows時引入了UNIX中的這個概念和相應的設計理念,並針對Windows的特性略作調整,形成了Windows平台上的Socket,簡稱為“WinSock”,並為開發人員提供了一整套的API,稱為“Windows WinSock Win32 API
”。
WinSock經曆了兩個版本,Windows Sockets 2是目前用得最多的版本(參看 http://en.wikipedia.org/wiki/Winsock
),微軟似乎從來沒有宣布要開發WinSock 3,也許“永遠也不會有”了。
圖 1所示為.NET平台下網路應用程式的層次架構:
圖 1
WinSock在底層使用一個運行於作業系統核心的系統驅動(Windows Sockets Knernel-mode Driver)tcpip.SYS,由它們負責管理網路連接和緩衝管理。
還有另一個驅動Afd.sys(Ancillary Function Driver for WinSock)則用於支援基於 window socket的應用程式,比如ftp、telnet等,被稱為“ Windows NT 通訊端驅動程式
”。
早期的Windows開發人員,需要使用C/C++去調用WinSock,比如MFC就提供了一個“CSocket”類封裝底層的Socket。
.NET也提供了一組類來封裝WinSock Win32 API,這些類集中於System.Net這一命名空間中,其中的核心類型就是Socket。
Socket類是對WinSock API一個很淺的封裝,擁有不少方法直接對應於WinSock中的C/C++函數,比如Poll、Select、IOControl等。
Socket有一個Handle屬性,它引用位於作業系統核心的Socket核心對象。
提示:
有關係統核心對象(Kernel Object)的通俗解釋,請參看《.NET 4.0物件導向編程漫談
》中的15.1.2節《作業系統的進程管理》
Socket提供了眾多的屬性,還提供了SetSocketOption方法來設定各種選項,對.NET網路應用程式的資料通訊進行“微調”。
Socket的功能出奇地強大,在.NET平台上,它支援以下四種典型的編程模式:
(1) 居於阻塞模式的Socket編程(單線程或多線程的),每個線程處理一個用戶端串連
(2)“非阻塞”模式的Socket編程,這是早期UNIX為提升網路應用程式效能而採用的編程模式,出於相容和方便移植原有程式的目的而保留,建議新開發的.NET網路程式不要再使用。
(3) 使用IAsyncResult的非同步編程模式:Socket類提供有一堆的“BeginXXX/EndXXX”方法實現非同步Socket編程,使用線程池中的線程完成工作,效能較好。
(4) 使用EAP的非同步編程模式:Socket類提供了“另一堆”以“Async”結尾的方法,在底層使用Windows作業系統的Completion Port(完成連接埠)和Overlapped I/O mechanism(重疊輸入/輸出機制),據說可以提供“最高”的效能。
在後面的文章中,將逐步地展開介紹這些編程模式。
提示:
強烈建議讀者仔細閱讀《.NET 4.0物件導向編程漫談
》中的第10章《非同步編程模式》,以提前掌握.NET非同步編程的基礎知識與基本技能,否則,後面的文章可以不用看了。
瞭解了Socket這位“美女”的“家庭背景”之後,在與她進行第一次“約會”之前,我們不妨弄清楚一個問題:
現在我們還有必要掌握Sokcet編程技術嗎?
2 Socket是否已人老珠黃?
基於Socket開發網路應用程式已經有很多年的曆史了,現在的新技術層出不窮,在.NET平台之上,WCF大有“一統江湖”的勢頭,Socket是否真的“人老珠黃”?
請看圖 2所示的多層“松花蛋”:
圖 2
圖 2說明,WCF與WinSocket等底層技術之間實際上是一種“包含”關係,每一層都在下一層所提供服務的基礎上,又擴充了新的功能,越外層的應用程式,可以使用的功能往往越多,開發效率往往也會更高。
WCF在WinSocket的基礎之上擴充了大量的功能,使用它可以很高效地開發網路應用程式,尤其非常適合於開發基於SOA的分布式軟體系統,但這並不是說它可以完全把Socket打入冷宮。在不少場合,拋棄WCF那龐大的架構,直接使用Socket更合適:
(1)需要實現自己的通訊協議的場合(比如你要架設一個網路遊戲伺服器)
(2)你開發的系統需要實現“一問一答”的“互動式”運行模式
(3)你希望能全面控制你的網路應用程式的“每個方面”,不想花時間去理解WCF那個複雜無比的內部架構
(4)你的網路應用程式應用背景非常單一與明確,比如就解決一個問題:定期將分佈於多台電腦上的資料檔案上傳“匯總”到一台中心伺服器上。
(5)……
如果需要基於各種標準協議(比如WS-*等)開發SOA的分布式軟體系統,再使用Socket就不合適了,那會大大地增加開發的工作量和難度,WCF更適合於解決這個問題。
在實際開發中,我們還可以混用WCF和Socket。比如我們可以基於WCF開發P2P的應用程式,使用NetPeerTcpBinding在P2P節點間“廣播訊息”,然後,在兩個P2P節點之間直接使用Socket“私下”裡傳送一個“秘密”檔案。
是可謂“運用之妙,存乎一心
”。
好了,下面就介紹使用Socket開發的最基礎知識吧。
3 第一個Socket應用程式
一般我們都將網路應用中用於提供“服務”的一方稱為“服務端應用程式(Server)”,另一方訪問這些服務的稱為“用戶端應用程式(Client)”。Server端和Client端的Socket用法是不一樣的。
3.1 服務端應用程式
開發網路程式的第一步,是建立Socket對象,以下是範例程式碼:
Socket newsock = new Socket(
AddressFamily.InterNetwork, //使用IPv4
SocketType.Stream, //使用可靠的雙向資料流,不儲存資訊邊界
ProtocolType.Tcp //使用TCP協議
);
緊接著,需要將Socket對象“綁定(Bind)
”到一個“終結點(IPEndPoint的執行個體)”。
IPEndPoint ipep = new IPEndPoint(IP地址,開啟連接埠); //綁定
newsock.Bind(ipep);
提示:
前面的《IP知多少
》一文中介紹過IPEndPoint。WCF中也定義了“終結點
”,它代表一個WCF服務的訪問點。
“綁定(Bind)
”這個術語非常值得關注,簡單地說,“綁定”就是將原先可能不相關的兩個事物“關聯”起來,打個可能不太恰當的比喻,“綁定”就是相愛的兩個人最終決定結婚,並領了結婚證。
“綁定”的身影在.NET平台中頻頻出現,比如“資料繫結(DataBind)”,就是使用控制項將資料來源中的資料展示在應用程式的介面上,並且將使用者對資料的修改和查詢等傳給資料來源。
在Socket應用程式中,“綁定”的作用是讓某個Socket對象關聯上特定的網路介面(Network Interface)。一台網路主機可能安裝有多個網路介面,“綁定”之後,Socket對象將可以在指定那個網路介面(Network
Interface
)上監聽。如果不需要指定特定的網路介面,也不在意使用的連接埠,那麼,可以建立一個使用IPAddress.Any,連接埠為0的IPEndPoint,Socket綁定這一IPEndPoint之後,作業系統會決定最終使用哪個網路介面,並且在“[1024,5000]”之間的選擇一個未用連接埠分配給此Socket。
注意:
WCF中也有“綁定”,但WCF中的“綁定”的含義要豐富得多,它其實是一組特殊的對象,它的主要功能是建立用於實現WCF應用程式間相互連訊的“通道棧”,WCF基底類別庫中提供了一堆的“綁定”,特定的綁定使用特定的通訊協議和技術,比如NetTcpBinding採用TCP協議,NetMsmqBinding則使用了微軟訊息佇列。
Socket對象綁定網路介面之後,就可以監聽並等待用戶端串連了:
newsock.Listen(10); //開始監聽
Socket client = newsock.Accept(); //等待用戶端串連
所謂“監聽
(Listen)
”,其實是告訴作業系統:“我關心本機某個網路介面上的資料包,當有資料包到達,並且連接埠號碼和我所規定的一致,請通知我”。
Socket.Listen方法的參數有著特殊的含義。此處暫時按下,留待後文分解。
Socket.Accept方法等待用戶端發來的串連請求資料包,預設情況下,這一方法是“同步”方法,線程將在此處阻塞等待,直到有客戶發來串連請求。
當用戶端發來串連請求時,Accept方法返回一個Socket對象,這個對象代表雙方已建立了一條資料通訊的鏈路,可以相互傳送資料了。這時,原先的Socket將得到“解放”,可以繼續監聽。
注意:
負責監聽的Socket不負責發送與接收資料,而Accept方法返回的Socket可以用於接收和發送資料,但不能用於接收新的串連,同時,其RemoteEndPoint方法可以擷取遠程用戶端的IP地址和使用的連接埠
以下代碼調用剛得到的Socket對象的Receive方法接收用戶端發來的資料:
byte[] data = new byte[1024];
int r
ecv
= client.Receive(data);
Socket.Receive方法也是一個“阻塞”的同步方法,它將收到的資料儲存到一個位元組數組中,這個位元組數組通常稱為“資料緩衝區”。
提示:
資料緩衝區在Socket編程中非常重要,讀者會發現,在開發中你時時刻刻都得關注它,一不小心,它就給你搗亂。
Receive方法的傳回值代表接收的資料位元組數。以下代碼使用這一傳回值瞭解用戶端到底發來了什麼訊息:
Console.WriteLine(Encoding.UTF8
.GetString(data, 0, recv
));
上面這句代碼中有幾點需要特別注意:
(1)一定要使用recv來“定界”用戶端傳來的資料。
(2)我們假設用戶端發送過來的訊息是一個字串,這裡使用UTF8進行解碼。很明顯,這要求用戶端與服務端必須事先達成一致,使用同樣的編碼和解碼方式。這種需要在事先進行協商的“東西”,就是“通訊協議
”。不同的網路應用會使用不同的通訊協議,比如互連網普遍使用HTTP,這是一個業界標準,而我們也可以定義自己的通訊協議,比如QQ就有自己的通訊協議。
提示:
我在《 漫談.NET開發中的字串編碼
》一文中介紹了字串編碼的基礎知識。
資料接收完畢,服務端就可以斷開客戶的串連:
client.Shutdown(SocketShutdown.Both); //通知OS,不再接收與發送資料
client.Close(); //關閉Socket
完成資料傳送任務之後,注意應該及時地關閉Socket。這通常分為兩步:
(1)調用Shutdown方法通知TCP/IP協議棧發送所有未發送的資料,或停止接收資料
(2)調用Close方法關閉通訊端。
Socket本身對應著一個核心對象,它有一個控制代碼(Handle)供作業系統核心進行管理。因此,它不再有用時必須及時地被關閉,否則,有可能會造成嚴重的問題。
提示:
作業系統能管理的控制代碼數是有限的,而網路應用服務端程式通常會運行很長的時間,如果不及時地關閉不用的Socket,將導致它所佔用的控制代碼不能及時回收,有可能導致伺服器Down掉。
Socket本身實現了IDisposable介面,所以也可以使用using關鍵字實現“自動釋放”:
using (newsock)
{
……
} //自動關閉newsock
3.2 用戶端應用程式
用戶端應用程式與服務端大同小異:
首先建立好一個Socket對象,然後再調用其Connect方法建立到服務端的串連,如果之前Socket沒有使用Bind方法指定一個連接埠,Connect方法會自動選擇一個未用的連接埠:
Socket server = new Socket( AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp );
server.Connect(服務端的IP終結點);
如果Connect方法沒有拋出異常,則表示成功串連伺服器,現在,就可以使用Socket對象的Send方法發送資料,資料同樣儲存於一個資料緩衝區(其實就是一個byte[])中:
server.Send(Encoding.UTF8
.GetBytes(要發送的訊息));
注意這裡選擇的字串編碼方式必須要與服務端一致,否則,將導致服務端無法正確地解碼出字串。
資料發送完畢,關閉通訊端就行了。
3.3 處理網路應用程式中的異常
Socket對象的Connect、Send、Receive等方法都有可能出錯,這時,.NET基底類別庫將拋出一個SocketException,它實際上封裝的是底層WinSock出錯資訊。
每一個SocketException對象都有一個對應的錯誤號碼,其含義是由底層的WinSock定的。比如錯誤號碼為10048的SocketException其含義是:地址已被使用。發生這一異常的原因通常是你嘗試把兩個Socket對象綁定到同一個IPEndPoint。
以下是Socket網路應用程式中的典型代碼架構:
Socket remote=new Socket(……);
try
{
//……
remote.Connect(iep); //iep為遠程主機的終結點
//……
remote.Send(……);
//……
}
catch (SocketException e)
{
Console.WriteLine("無法串連遠程主機 {0} ,原因:{1},
NativeErrorCode:{2},SocketErrorCode:{3}", iep.Address,
e.Message, e.NativeErrorCode, e.SocketErrorCode);
}
finally
{
server.Close();
}
樣本項目IntroduceSocket展示了本文所介紹的知識(圖 3)。
圖 3
到此,我們與“Socket美女”的“第一次約會”到此結束。您對她的第一印象如何?
點擊下載本文樣本
==============================================================================
下一篇文章,將介紹Socket美女的“追求者”隊伍,以及如何開發“一問一答”的網路應用程式。