與Socket的第一次“約會”

   在前面的文章中,我們已經介紹了使用.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美女”的“第一次約會”到此結束。您對她的第一印象如何?

 


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/bitfan/archive/2010/12/24/6097011.aspx

發佈了23 篇原創文章 · 獲贊 0 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章