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