使用Socket API

簡介

通過這篇文章我們想爲大家帶來一些Symbian操作系統的有關sockets API的基本介紹。 本文的讀者應該是希望在他們的應用程序中增添socket通信功能的Symbian操作系統的開發者,本文不僅提供了理論介紹,同樣給出了可供實踐參考的代碼範例。

本文包含的內容有:

  • 概括介紹了有關socket通信的有關組件。
  • 概括介紹了socket服務架構以及使用兩個主要API類RSocketServRSocket的使用。
  • 討論了創建兩個終端之間進行通信的過程。
  • 討論了socket之間通信的不同模式:基於一串數據流的模式以及基於離散消息的模式。
  • 一個如何使用活動對象來進行socket連接的實踐範例。

 有關Socket的服務構架

本文的一個內容是介紹給大家如何將基於Socket服務的通信功能加入到應用程序中來。儘管如此,計算機通信系統乃是一個十分複雜的系統,本文介紹的基於socket服務的通信仍然是在一個相對比較高級的層次,沒有深入底層探討的話題和技術。要想讓socket服務來發揮作用,許多底層支持軟件將是必須的。

下圖說明了socket服務組件在Symbian系統的通信子系統中的哪一層位置,扮演如何一個角色。

 

Symbian OS通信系統組件

首先我們來考慮傳輸層協議。上圖的Internet互聯網協議和紅外協議,從Symbian 6.0之後支持的藍牙?無線通訊技術以及都在這一層中。

當我們談到Internet協議時,我們其實包括了一個隱式的依賴動作,那就是向ISP(互聯網服務提供商)進行撥號連接。因此,如圖所示我們可以看到Symbian系統提供了撥號網絡接入組件。而在本圖中,最重要的的系統組件是電話通信服務組件。

最終,我們需要設計到一個硬件設備,有了硬件我們纔可以在選定的網絡環境中接收和發送數據。上圖的核心部分就是使用Internet協議的撥號接入網絡,並且顯示了串行通信組件在整個通信系統扮演瞭如何的角色。串行通信服務組件通過特定的硬件設備驅動,完成了硬件設備與它周圍環境的通信。

 

什麼是socket?

那麼什麼是socket呢? 用一句引自伯克利(Berkeley)UNIX關於socket實現的經典定義來回答就是“socket就是通信終端”。

那究竟是什麼意思呢?

一個socket代表了一條通信‘通道’邏輯上的終端。而實際上講,socket是物理網絡地址和邏輯端口號的一個集合,而這個集合可以向另外一個位置的與他具有相同定義的socket進行數據傳輸。

因爲socket是由機器地址和端口號來區分/識別的,那麼在一個特定的計算機網絡上,每一個socket都是以此方式被唯一識別的。這就使得應用程序可以唯一地去定位網絡上的另外一個位置的socket。

注意:對於同一臺機器上的兩個socket,他們是完全具備彼此間進行通信的可能的;在這種情況下,兩個socket具有相同的主機地址,但是他們擁有不同的端口號。

主機地址和端口號的組合,對於不同協議是不同的。在socket的經典應用中,網絡通信使用的是IP(Internet Protocol)協議,但是實際上socket是支持很多其它協議的,對於這方面的信息稍後會提到。

正如我們將會看到的,不管我們選擇怎樣的通信協議(傳輸層),我們都可以使用同一種已成熟的socket API來實現通信。

 協議模塊

如上文所述,socket的經典應用是在TCP/IP協議的計算機網絡上,使兩個邏輯端點之間展開通信活動。最著名的應用TCP/IP的計算機網絡,當然就是Internet了。

絕大多數socket系統的實現都限定在了TCP/IP網絡的通信上。

但是,Symbian系統的socket服務組件,就實現了更多的內容;不僅如此,它還爲其他組件提供了支持模塊插件協議的基礎構架。這就使得Symbian公司和它的開發夥伴們大大延長了socket服務組件以及支持socket的應用程序的應用時間。

由於新協議和傳輸層的引入,支持了新的傳輸‘語言’或協議的協議組件,從而使得socket服務組件可以隨之適應新的應用環境。

隨着Symbian系統第五版的socket服務組件支持了TCP/IP和紅外協議的稽覈。在Symbian 6.0版的時候,就增加了藍牙?無線技術和短信息服務插件。

協議模塊其實就是標準的Symbian系統動態鏈接庫(DLL)。他們都有共同的UID2--KUidProtocolModule(0x1000004A)來表示他們的類型,並且擁有特殊的擴展名*.PRT。

一個關於經典系統的方面,就是socket服務對PLP(Psion Link Protocol)協議也是支持的。PLP被用來進行Symbian系統的手機和運行Microsoft Windows的臺式或筆記本計算機之間進行通信。PLP的一個應用就是Symbian Connnect - 目前的被用於名爲‘PsiWin’的Psion計算機。

socket服務組件可以以兩種方式加載協議模塊:

  • 最通常的做法就是,協議模塊會在第一個使用該協議的socket被打開的時候進行加載。
  • 另外一種做法是,應用程序可以顯式地加載協議模塊。這種做法的一個好處就在於,當協議加載需要一個比較長的時間的時候,應用程序或用戶可以得到相應的提示。使用這種方法調用的API在本文的後面將會進行討論。

要說明的幾點:一個協議模塊可以包含多種協議實現。比如,在TCPIP.PRT模塊中,就包含了UDP、TCP、ICMP、IP以及DNS協議的實現。單個協議的實現可以通過位於/system/data/.的.esk文件進行映射。而每個協議模塊都有一個.esk文件來指定該模塊所包含的協議,以及每個協議在插件模塊中所處的索引位置。

傳輸的獨立性

上文已經提到,socket服務組件的插件架構特性可以使得新的協議模塊在任何時間被安裝到一部Symbian系統的手機當中。

這個架構可以使得socket服務組件來實現獨立傳輸層的概念。藉助於提供一個通用的核心socket API接口,這種架構就可以處理所有一般性數據傳輸系統的需求,並且通過添加特定協議的協議模塊,socket服務組件就可以被廣大應用程序開發者來給自己的產品增添通信功能,從而省下了大量的開發通信子系統的時間。

隨着時間的發展,新的協議逐步登上歷史舞臺,協議模塊都將會爲了適應socket接口而被重寫。而應用程序開發者,他們只需要增添協議新近引入的屬性或者動作,來支持新的協議即可,Socket服務組件便會使用新的協議,藉助操作系統底層的通信組件,來完成通信機制,而並不會影響到上層應用程序開發者的接口和開發。

總而言之,socket服務組件可以讓應用程序開發者在僅僅維護一套核心API接口的情況下,可以藉助操作系統的通信子系統來使用多個協議,從而減少了自己的開發工作量以及開發時間。

“客戶端-服務器”接口

Symbian系統的一個特點就是它具有一個體積很小的微內核(micro-kernel),因此我們只能把必須和硬件設備交互以及進行主機控制的核心服務放在內核端運行。而另外許許多多的系統服務只能以用戶模式的服務器線程的形式運行,通常被稱爲‘系統服務器’。

socket服務組件就是這些‘系統服務器’中的一個,第三方應用程序就藉助公開的客戶端API,通過該組件完成通信功能。其中最重要的四個類爲:

 

  • RSocketServer: 這個類是用來建立和socket服務組件之間的連接以及獲取必要的資源的。在客戶端-服務器架構的定義中,該類表示了應用程序與socket服務組件之間建立連接的會話。所有的其他客戶端接口類,在使用中都需要一個被打開的本類的實例來進行操作。
  • RSocket: 這個類表示了一個socket連接。一個標準的應用程序可能會在不同時間的時候,擁有若干個RSocket的實例在同時進行操作。
  • RHostResolver: 這個類用來提供主機名稱解析服務的接口。
  • RNetDatabase: 這個類用來提供網絡數據庫訪問的接口。

RSocket, RHostResolver & RNetDatabase 均表示了一個給定的應用程序與socket服務組件之間進行的會話下的子會話,而應用程序與socket服務組件之間的會話就是一個RSocketServer的實例。

 sockets服務器的主要類

socket服務組件提供了兩個主類,供他的客戶端訪問內部的API。

  • RSocketServ: 在每個應用程序線程中,只要需要連接socket請求,他就必須使用一個本類的實例,來爲其他連接(會話)提供socket服務。
  • RSocket: 每一個需要使用socket的應用程序線程,同樣也需要一個或多個RSocket對象,這些對象就是子會話了。

下面的兩個部分將會介紹會話和子會話類(RSocketServ 和 RSocket)的詳細內容。

使用RSocketServ類

RSocketServ類扮演了一個十分重要的角色,因爲它是客戶端應用程序與socket服務組建之間的連接會話。

但是,客戶端應用程序並不直接使用這個類來進行數據的發送和接收,或者創建一個遠程通信端點;要完成這些任務的話,使用的是RSocket類,這個類將會在稍後進行介紹。

RSocketServ可以讓客戶端應用程序來向socket服務組件發起一些查詢,查詢的內容包括服務器支持的協議個數以及支持哪些協議,每個支持協議的具體信息等等。

希望使用socket的客戶端應用程序,都將需要自己創建一個RSocketServ類的實例對象,用這個對象來表示該客戶端應用程序和 socket服務之間的會話。每一個獨立的socket連接,都是一個獨立的RSocket類的實例對象。可以說,在一個客戶端應用程序中,該程序的 RSocketServ類對象就是所有的RSocket類對象的容器。

RSocketServ類的兩個常用函數就是Connect()和StandardProtocol()。

 

 建立一個連接到sockets服務的會話

使用Connect()方法,應用程序就可以建立與socket服務之間的一個會話。它僅僅使用一個參數--該會話所提供的消息通道的個數。

 TInt Connect (TUint aMessageSlots); 

消息數參數被用來限定應用程序向socket服務所同時併發的異步操作的請求通道數。每一個同步請求都將佔用一個消息通道,並且請求準備中的異步操作也將佔用一個消息通道。

一個普通socket進行的讀寫通信操作,都是異步進行的,也就是說這樣的操作要佔用兩個消息通道。如果socket也可以進行同步操作的話,那麼我們其實並不需要指定過多的消息通道,因爲同步操作的消息通道是由socket客戶端-服務器框架來完成的。對於你的應用程序在同一個時間內會使用到多少個消息通道,這完全是由你來斷定的,而在大多數情況下,我們要儘可能的減少同時請求的消息通道數。

如果我們不指定任何特定的值,那麼系統會使用一個默認值作爲消息通道個數的參數:KESockDefaultMessageSlots (0x08)。

 預載入協議模塊

socket服務組件載入協議協議模塊的動作是動態進行的,當針對某一個協議的第一個socket被創建的時候,該協議模塊在此時纔會被載入。儘管如此,載入協議仍然是一件比較費時的操作,RSocketServ提供了一個StartProtocol()函數,來進行協議模塊的預載入操作,調用該函數可以在socket連接請求的時候節省載入協議模塊的時間。

如果你的應用程序需要在程序啓動之初就載入協議模塊,而並非需要連接的時候才進行載入,那麼可以使用下面的函數範例來調用StartProtocol()方法:

    void StartProtocol (TUint aFamily, TUint aSockType,
TUint aProtocol, TRequestStatus& aStatus);

StartProtocol()函數的參數有:協議族(例如,KAfInet),使用該協議的socket類型(例如,KSockStream),協議族中的協議標示(例如,KProtocolInetTcp),最後一個參數是異步調用的完成狀態參數。這些參數的意義將會在下面做以簡短介紹。

請注意,儘管StartProtocol()函數是一個異步服務,但是它卻是一個在操作過程中不能被取消的操作。

使用RSocket類

RSocket代表了應用程序的一個socket連接,在一個應用程序中,每一個socket連接都是一個單獨的RSocket的實例。事實上,客戶端應用程序的代碼中使用更多的是RSocket類而並不是RSocketServ類。

RSocket是一個提供了許許多多服務的體積龐大的類,這些服務包括:

  • 連接到服務,無論作爲客戶端還是服務端
  • 設置或者查詢自己的地址,或者查詢遠程地址
  • 從socket讀取數據
  • 向socket寫入數據
  • 其他更多...

在打開任何socket之前,我們必須有一個激活了的RSocketServ會話。並且,在上述提到的任何服務進行操作之前,我們要確保 socket是打開的。作爲打開一個socket的一部分,RSocket這個子回話對象(見上文說明)需要同一個socket服務器進行連接,這個服務器就是一個RScoketServ類的實例。

下面的章節介紹了RSocket的各種函數,有了這些函數的介紹和幫助我們就可以寫出基於socket通信的應用程序來。

主機解析服務

什麼是主機解析?

在一個由計算機組成的網絡裏,獨立的主機使用不同的地址格式來判斷各自是誰,是什麼。

例如,你的電子郵件有可能保存在一臺主機當中,這臺主機可能有一個可讀的地址,比如pop3.freeserve.net。這個地址儘管對人來說是可讀的、是一個具有一定意義的地址,但是對於網絡上的計算機來說,並沒有任何直接的用處。

當你的郵件客戶端程序嘗試下載你可能會收到的電子郵件的時候,你的電腦就會使用你的電子郵件服務器的地址(先前舉例的 pop3.freeserve.net)去進行查詢,將他們相對應的數字網絡地址查詢出來。當獲得了機器可讀的數字網絡地址,應用程序纔可能建立起連接。在TCP/IP協議族中,地址解析轉換是由域名解析服務(Domain Name Service, DNS)進行的。

地址解析服務的用處有兩個。首先,它可以讓計算機網絡(在本例中指的是Internet)的用戶可以使用一個直接的、有意義的、人們可以理解並且可以記住的的地址來指向某一個網絡資源。也許你曾經見過這樣的網絡地址212.134.93.203、204.71.202.160,但是一般情況下也許你並不會使用這樣的數字地址去訪問網絡,一般情況下你更多使用的是例如www.symbian.com或者www.yahoo.com這樣的地址。

其次,這種將網絡物理地址和用戶記憶的網絡資源地址進行分割的服務,達到了網絡硬件層進行升級或者替換的情況下並不會影響到用戶訪問的目的。這種機制也從另外一種情況下幫助了大的網絡服務提供商,比如微軟公司的Hotmail服務,使這些運營商可以在世界各地部署本地服務器,從而讓每一個用戶獲得更快的訪問速度,無論用戶是在西雅圖或者別的任何地方。

使用RHostResolver類

作爲客戶端API的一部分,socket服務組件提供了RHostResolver類,用這個類我們可以獲得一個通用的主機地址解析服務,這項服務的內部會自己處理相應不同協議的主機地址解析的細節問題。如果我們針對TCP/IP協議族而言,那麼RHostResolver類扮演的就是客戶端與域名解析服務(DNS)之間進行通信的服務角色。

每一個不同的協議,都提供了自己的主機解析服務,這些服務是作爲協議模塊的一個標準部分實現的。這樣的設計就使得客戶端可以僅僅訪問RHostResolver類,而並不需要關心socket使用的是哪一種協議。

RHostResolver接口提供瞭如下幾種功能供客戶端應用程序訪問,他們是:

  • 將一個數字網絡地址轉換爲人所能識別的包含一定意義的文本表現形式
  • 將人讀地址轉換爲相對應的機讀數字地址
  • 讀取或者設置本地設備的主機名的方法/函數

就像是RSocket一樣,RHostResolver類繼承自RSubSessionBase。因此,要想使用RHostResolver類,客戶端應用程序就必須先進行對socket服務組件的服務器的連接,這個服務組件的服務器就是一個RSocketServ類的實例。

RHostResolver類提供了許多主機地址解析服務的函數/方法,每一個函數都提供了兩個版本的多態函數--同步和異步操作。

請注意,因爲這是一個通用的主機地址解析接口,但是並不是所有的協議都提供了主機地址解析服務,所以有些協議可能並沒有提供任何主機地址解析服務。

如果客戶端應用程序嘗試使用RHostResolver中的函數去對一個不支持主機地址解析服務的協議請求主機地址解析服務,那麼將會得到錯誤代碼KErrNotSupported。

在進行任何主機地址解析服務之前,我們要打開一個RHostResolver類的實例。正如前面所提到過的,因爲主機解析服務類是一個子會話類,所以在調用RHostResolver::Open()函數之前,該子會話類必須關聯一個socket服務組件的服務器會話對象實例。

    TInt Open(RSocketServ& aSocketServer, TUint anAddrFamily,
TUint aProtocol);

下一步,我們將會根據上面所示的函數原形,制定我們希望用哪個地址類型來解析的主機地址,地址類型應該是和傳遞給RSocket::Open()函數的參數一致的。

最後,我們還需要指定一個協議來進行主機地址解析服務。如果之前選擇的地址類型是協議無關類型的,那麼我們可以在這裏指定KUndefinedProtocol。

其他的RHostResolver類提供的函數如下所示:

    TInt GetByName(const TDesC& aName, TNameEntry& aResult);
TInt GetByAddress(const TSockAddr& anAddr, TNameEntry& aResult);
TInt GetHostName(TDes& aName);
TInt SetHostName(const TDesC& aName); // sync only
TInt Next(TNameEntry& aResult);

這些函數中的大多數都是可以見名知意的;不過Next()函數例外,我們來進行一些解釋:對於有些協議來說,GetByName()和 GetByAddress()函數可能會一次找到不止一個結果,比如地址假名被允許的時候。如果這樣的話,我們就需要調用Next()函數來返回下一個地址結果。

域名服務(DNS)

域名解析服務(Domain Name Service,DNS)是TCP/IP協議所提供的主機解析服務。

一個標準的DNS查詢一般由以下三個步驟組成:

  • 一個在某一個網絡硬件設備(例如一塊以太網卡)設備上運行的客戶端應用程序,將自己的查詢主機請求發送給網絡上的另外一臺主機--DNS服務器。
  • DNS服務器將查詢請求進行查詢,查詢是在龐大的數字地址與主機名稱對應列表中進行的,查詢到的結果將會被轉換成不同的地址格式。
  • DNS服務器將地址發送回客戶端。

請注意,DNS服務可以將文本格式的地址(例如www.symbian.com)解析爲數值格式地址(例如212.134.93.203),或者將數值地址(204.71.202.160)解析爲文本格式的地址——www.yahoo.com。

互聯網服務提供商一般都提供了很多DNS服務器(一般都不只一臺)來供他們的客戶使用。如果沒有這些服務器,那麼使用互聯網對於普通用戶來說將是一場災難。如果沒有DNS的話我們將不得不記住我們感興趣的web站點的32位數字地址,或者使用十分十分冗長難記的地址去給其他人發電子郵件。

這裏我們需要注意的重要一點是,實際上地址轉換這項工作並不是客戶端設備進行的,而是待轉換地址被髮送到了另外一臺主機,由另外一臺主機進行的解析。所以我們在建立一個使用TPC/IP協議建立連接的時候,就必須提供一個DNS服務器地址,否則一切連接將幾乎無法進行。

在socket代碼中使用活動對象(active objects)

計算機網絡通信,在一般情況下都是使用異步操作的。下面我們先放下談論已久的socket通信系統,來看看一個打電話過程是如何進行的,這樣會有助於我們理解下面要討論的問題。

當一個朋友給你打電話,你的電話機會收到電話打入的電信號,它在收到這個信號後就開始振鈴,然後你聽到了鈴聲之後就拿起聽筒,開始進行通話,直到掛斷電話此次通話結束。

當等待電話呼叫的時候,我們可以進行其他任何事情,並不會對我們的生活造成影響。與此的,假如你的朋友給你發送了一個是十分困難的問題讓你幫助解決,也許這是一個相當大的難題,你要花一些時間來考慮或者解決,當這個時候,你的朋友可以利用你考慮或者解決的時間,進行他自己的其他活動。

上面的電話通信例子,就是一個很好的一部通信系統的例子。

當我們使用socket來在兩臺計算機之間傳輸數據的時候,我們看到的是一個類似上面打電話例子的異步模型。

在一個使用socket進行網絡通信的應用程序中,上述異步通信的事件包括:

  • 連接, 斷開連接以及確認請求連接的要求
  • 接受數據(因爲我們並不知道有多少數據要發送過來,所以這個過程是異步的)
  • 發出數據(因爲對於應用程序層來說,我們並不知道底層的硬件需要多長時間才能夠將數據發出,所以這個過程也是異步的)
  • 其他,比如載入協議模塊之類的,看似並不是十分明顯的異步操作

因爲我們需要在應用程序中處理這些異步事件,所以我們需要用到Symbian OS的活動對象(Active objects, AOs)來解決這些問題。

活動對象的特點有:

  • 使得應用程序開發者可以很容易的控制對象的生存週期
  • 在一個單線程程序中完成並非嚴格意義上的多任務操作
  • 爲Symbian系統提供了效率較高的單線程多任務解決方案,而並不是真正地使用多線程。

在Symbian系統中,所有的線程都是通過一個或者多個活動對象,使用一個激活的進度管理器來進行高效率的外部事件處理。

一個活動對象,在一個時間內只能處理一個事件源。在實際情況中,活動對象通常也都是被設計爲處理一類特定事件的。

在稍後的代碼示例中,這些代碼因爲有不同的需求所以使用了不止一個活動對象,無論是客戶端還是服務器程序,都使用了不止三個活動對象。其中一個用來處理連接機制,一個用來接收數據,另外一個用來發送數據。

下面我們就來看看如合利用活動對象來處理客戶端和服務器之間進行socket流式連接的範例。

代碼示例: 連接sockets

下面一部分就是藉助代碼的演示來向大家說明如何利用活動對象進行socket連接。這寫代碼段是從一個進行監聽接入連接的‘服務器’和發送連接請求到服務器的‘客戶端’程序中提取出來的。

服務‘監聽’類的定義

下面的代碼是從一個完整的進行‘監聽’(listening)的服務器類定義中取出的一部分。

class CModel : public CActive {
public:
void StartEngineL(void);
private:
void RunL(void);
void DoCancel (void);
private:
RSocketServ iSession;
RSocket iListen, iSocket;
CRx* iRxAO; // 用於接收數據的活動對象
CTx* iTxAO; // 用於發送數據的活動對象
};

請注意,在成員變量中有兩個socket,一個是用來監聽和連接的,而另外一個是用來處理和客戶端之間進行數據的傳輸的。

在這個類的定義中,還有兩個活動對象,他們是iRxAO和iTxAO。這兩個活動對象用來在連接到服務之後異步地、分別地處理數據的發送和接收工作。

(對上面已經定義的類而言,這個類僅僅接收一個客戶端連接,那麼請你不要對自己的創造力作任何限制地去想象和學習一下吧,你可以以這個類定義爲基礎,將他擴展爲接收多個客戶端連接的服務器吧!)

下面我們來看看連接過程是如何實現的。

做好接收客戶端連接的準備

首先,在我們的服務器沒有進行服務接入請求之前,我們要先創建兩個socket,創建方法如下所示:

// Need to use two sockets - one to listen for
// an incoming connection.
err = iListen.Open(iSession, KAfInet,KSockStream, KUndefinedProtocol);
User::LeaveIfError(err);
// The second (blank) socket is required to
// build the connection & transfer data.
err = iSocket.Open(iSession);
User::LeaveIfError(err);

一個socket叫做iListen,他扮演的就是‘監聽者’的角色,用來監聽是否有來自客戶端的接入請求。iListen是一個和協議流關聯的對象,在本例中這個協議就是TCP協議,因爲我們使用的是Internet地址格式。

另外一個socket,叫做iSocket,在現在是被構造爲空socket的,它僅僅在客戶端連接請求的時候纔會被準備好進入工作狀態。這個socket就是用來處理來自客戶端的任何請求,並且進行數據傳輸工作的。

那麼下面,監聽socket就可以去進行監聽客戶端連接請求的工作了。

請注意上面例子中使用的兩個不同的RSocket::Open()函數的多態。

其中第一個,用在iListen成員變量的,它是用來進行客戶端請求連接監聽的,所以它需要一個本地地址,只有這樣連接數據才能本正確地路由到該對象。

要設定本地地址,我們需要將一個地址和一個socket進行綁定(bind)操作:

// Bind the listening socket to the required
// port.
TInetAddr anyAddrOnPort(KInetAddrAny, KTestPort);
iListen.Bind(anyAddrOnPort);

在本例中,我們並沒有過多考慮socket的網絡地址,因爲我們使用的是易於操作的主機地址名稱。儘管如此,我們還是需要指定端口號,這樣才能完整確定一個綁定地址。

這個時候,客戶端就可以通過我們的主機的Internet主機地址和端口號(事先在程序中用#define宏定義好了的 KTextPort)向我們的主機(服務器)發送請求了。不過有一點,如果我們不向客戶端告知我們的主機名稱和端口號,那麼客戶端將永遠無法訪問到我們的服務器。

還要注意,因爲我們的socket是使用Internet地址格式協議族進行打開操作的,所以我們調用Bind()函數時送入的函數參數TSockAddr就是一個TInetAddr類型的一個實例。

在TInetAddr類中,它除了保存TSockAddr中定義的一般性數據值外,還保存了一個TUint32類型的IP地址數據。在協議族屬性中,TInetAddr類提供的永遠是KAfInet值,因爲該值表示這個地址是一個TCP/IP地址。

當完成了socket的建立,綁定了監聽socket,我們就幾乎完成了所有準備工作,可以相應來自任何客戶端的連接請求。

下面我們就是需要把接入連接請求創建一個隊列,這個時候我們需要調用RScocket::Listen()函數,另外還要注意我們應該使用長度爲1的隊列,之後我們看到連接是如何進行的時候,就會明白這個隊列長度是足夠了的。

void CModel::StartEngineL (void)
{

// Listen for incoming connections...
iListen.Listen(1);
// and accept an incoming connection.
// On connection, subsequent data transfer will
// occur using the socket iSocket
iListen.Accept(iSocket, iStatus);
SetActive();
...
}

最後,我們調用異步函數RSocket::Accept()來準備接收客戶端連接請求。

那麼我們再來回顧一下繼承自活動對象CActive類的CModel類,當一個客戶端連接到我們定義的服務器類的時候,CModel::RunL()函數將會被調用。

該函數被調用後的過程,請看下一部分。

處理連接請求

當一個客戶端連接請求被收到的時候,最前線的RSocket::Accept()函數執行請求完成,然後活動對象的RunL()函數將會被調用,這一切步驟都是因爲CModel類是一個被激活狀態的活動對象。

    void CModel::RunL(void)
{
if (iStatus==KErrNone)
{
// Connection has been established
NotifyEvent(EEventConnected);
// Now need to start the receiver AO.
iRxAO->RxL(iSocketType);
}
else // error condition
...
}

那麼假設現在所有步驟都是正常進行,那麼我們獲得的完成狀態變量就是KErrNone。在上面的範例代碼中,我們會向用戶界面層傳遞一個連接建立成功的消息,然後我們啓動活動對象,對接收到的數據進行處理,然後連接iSocket進行返回數據的準備。

因爲我們進行操作的是一個異步系統,所以現在因爲客戶端和服務器是已經連接的狀態,那麼客戶端可以在任何時間向服務器socket發送數據。所以我們需要在接收到數據之後,儘可能快地進行數據的處理。

有一點,在我們進行已連接的socket的數據發送的時候,我們並不會打開活動對象。數據僅僅會在客戶端程序或者用戶希望發送數據到客戶端的時候,才進行操作。

使用有連接的socket

回顧一下我們前面定義的CModel類,我們有一個成員變量,類型爲CRx的iRxAO。

類CRx是一個繼承自CActive的類,他也是一個活動對象。

CRx類的成員函數RxL(),定義如下;這個函數向連接到我們的服務器的客戶端發出了一個一個異步請求。

    void CRx::RxL ( ) //class CRx derived from CActive
{
// Issue read request
iSocket->RecvOneOrMore(iDataBuffer, 0, iStatus, iRecvLen);
SetActive();
}

函數RecvOneOrMore()將會在稍後,和其他一些讀取以及寫入socket的函數一同進行討論。

在接入數據請求完成的時候,CRx::RunL()函數將會被調用,完成後返回的內容有完成狀態事件以及新收到的數據內容。

那麼再來回顧一下CModel類的另外一個成員變量,類型爲CTx的iTxAO。

類CTx是一個繼承自CActive的類,他也是一個活動對象。

CTx類的成員函數TxL(),如下所示;他想連接到服務器的客戶端進行了一個發送數據的一部請求操作。

    void CTx::TxL (TDesC& aData)
{
if (!IsActive())
{
// Take a copy of the data to be sent.
iDataBuffer = aData;
// Issue write request
iSocket->Send(iDataBuffer, 0, iStatus);
SetActive();
}
}

Send()函數將會在稍後,和其他一些讀取以及寫入socket的函數一同進行討論。

當數據發送請求完成的時候,CTx::RunL()函數將會被調用,同時返回的內容有發送操作完成的結果狀態。

傳輸數據

現在我們來看看兩臺網絡設備之間,究竟是如何利用socket來進行數據傳輸的。

如我們以前所知,在socket通信中,數據報通信和數據流通信是兩種十分不同的通信方式。

無論我們使用的是數據報還是數據流的傳輸方式,每一個獨立的數據單元在網絡通信的兩端被傳輸的時候都有可能經過十分不同的路由路徑,因爲在網絡通信的雙方之間總有着不計其數的子網絡,而通信雙方對數據單元的路由方向是無法控制的。這種情況是十分普遍而且正常的,由於數據流的傳輸方式也是以數據報形式爲基礎的,所以從這個角度來看的話,二者的路由特點是一致的。

 

接收數據

使用無連接的sockets

下面的函數,是RSocket提供的用來接收無連接的socket的接入數據的。

    void RecvFrom(TDes8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus);
void RecvFrom(TDes8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus, TSockXfrLength& aLen);

如果應用程序使用的是無連接的socket,那麼需要使用RSocket::RecvFrom()這個這個方法來讀取從另外一個遠程主機發送過來的數據。

該函數的第一個參數是一個字符串,是用來保存接收數據的。

調用該函數的程序,會在一個完整的數據報接收完成的時候,得到相應的通知。接收數據的長度,就是接收字符串的長度。如果接收數據報的長度要比字符串的最大長度更長,那麼接收數據的末尾將被截去。

該函書的第二個參數是要進行接收操作的遠程主機的地址。這個地址需要是一個根據socket打開方式定義的協議格式相匹配的地址。例如,如果打開socket的時候定義的是TCP/IP協議,那麼這個地址需要是一個TInetAddr類型的變量。

我們會發現,這個函數有兩個版本的重載,他們都進行了同樣的操作,方式也一樣。唯一不同的是,第二個函數可以將接收數據的長度,顯式地返回給調用者。

還有一點,一個單獨的socket在任何一個時間內,都只有一個狀態爲等待中的接收操作。

上面的方法,只能用於無連接(數據報)類型的socket連接。

使用連接的sockets

下面的函數是RSocket提供的用來從已經連接的socket中讀取數據的函數原形。

    void Recv(TDes8& aDesc, TUint flags, TRequestStatus& aStatus);
void Recv(TDes8& aDesc, TUint flags,
TRequestStatus& aStatus,TSockXfrLength& aLen);
void RecvOneOrMore(TDes8& aDesc, TUint flags,
TRequestStatus& aStatus, TSockXfrLength& aLen);

如果應用程序使用的是已連接的socket,那麼應該使用上面的函數來進行遠程主機的數據接收工作。

和前面的無連接socket類似,這些接收函數的第一個參數,仍然是接收數據要保存的目標字符串變量。

Recv()函數會在目標字符串變量被填滿或者連接斷開的時候完成。在該函數完成調用的時候,讀取數據的長度就是字符串的長度,除非在沒有讀取任何數據連接就斷開了。

第二個Recv()函數的重載可以顯式地獲取接收數據的長度,該長度被保存在了類型爲TSockXfrLength的函數參數中,這樣的話判斷接收數據長度就不必關聯接收字符串的長度了。

最後一個函數RecvOneOrMore(),與Recv()不同,這個函數是會在函數接收到任何數據之後立刻返回的。言外之意,調用 RecvOneOrMore()函數會接收到1--n個字節,其中n就是目標字符串的長度。同樣地,如果連接被斷開,RecvOneOrMore()函數仍然會立刻返回,並且不會返回任何數據。

雖然是已連接的socket,但是在發送過程中數據流並不一定都是物理上連續的,儘管從邏輯上看他們是流式的。所以,即便是使用已連接的socket,仍然應用程序--socket的調用者--來進行判斷數據流的結束與否,邊界切分等工作。

注意,由於我們使用的是已連接的socket,那麼我們不需要指定接收收據的socket地址,因爲已連接的socket是在連接動作發生的時候就已經指定好了傳輸目標主機地址信息了。

在這一部分的前半部分,我們介紹的各種函數都是具有比較高的複雜度的,可能對於應用程序開發者來說並不會具有特別的吸引力。

特別地,我們可以注意到所有的函數都以一個參數TUint aFlags作爲標示作用,到目前爲止還沒有對他進行討論。這個參數的作用是讓應用程序可以選擇特定協議的指定屬性,以此來設置協議接收處理數據的方式。

下面介紹的另外一個函數Read(),他將默認標示參數設置爲0,並且也去掉了TSockXfrLength類型的參數。如果使用該函數,那麼接收數據的長度就只能通過接收目標字符串的長度來獲得了。

     void Read(TDes8& aDesc, TRequestStatus& aStatus);

除了上述的兩個例外,這個Read()函數的操作效果就基本同Recv()一樣了。

注意,這個函數僅僅在已連接的socket通信中是可以使用的。

發送數據

使用未連接的sockets

下面的函數是RSocket中用來向未連接的socket發送數據的。

    void SendTo(const TDesC8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus);
void SendTo(const TDesC8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus, TSockXfrLength& aLen);

如果應用程序連接的是無連接的socket,那麼就要使用RSocket::SendTo()函數來向遠程主機發送數據。

這個函數中的第一個參數是包含了要發送數據內容的字符串,而要發送內容的長度,則是由字符串的長度決定的。

當數據發送完成的時候,調用該函數的應用程序將會得到通知。如果你使用的是帶有TSockXfrLength類型參數的函數重載,那麼已發送的數據的長度,將會在完成的時候被保存在該參數中。

第二個參數包含了要發送數據的遠程主機的地址,這個地址的格式應該符合socket被打開的時候制定的協議所支持的地址格式,比如,如果我們選擇了TCP/IP協議,那麼我們就需要使用TInetAddr作爲發送主機的地址。

第三個參數,TUint類型的標誌位,它是一個和協議相關的位標識符,定義了某些需要向協議模塊中傳遞參數的標誌信息。

需要注意的是,在一個socket連接中,在任意時間最多僅有一個發送操作時處於等待狀態的。

上述介紹的函數,僅僅可用於無連接的數據報socket使用。

使用連接的sockets

下面的函數,是RSocket提供的用來向一個已經連接的socket發送數據的。

    void Send(const TDesC8& aDesc, TUint someFlags,
TRequestStatus& aStatus);
void Send(const TDesC8& aDesc, TUint someFlags,
TRequestStatus& aStatus, TSockXfrLength& aLen);

如果你的應用程序使用的是已經連接的socket,那麼可以使用上面的函數來向遠程主機發送數據。

和上面類似,該函數的第一個參數是包含了要向遠程主機發送數據內容的字符串,該字符串的長度就是要發送數據的全部長度。

Send函數會在全部數據源發送完成之後,或者連接斷開之後返回。

第二個函數Send()可以讓調用者傳遞一個TSockXfrLength類型的參數進來,以此來確定發送數據的長度,這樣的話傳輸函數就不必以發送數據的內容的字符串長度來作爲原數據的長度了。

上面兩種函數衝在,都提供了一個TUint someFlags參數,該參數是用來定義和協議相關的標示位的,針對不同協議會有不同的協議標示定義。

正如前面提到的SendTo()函數,上面第二個方法中的TSockXfrLength類型的參數,會在異步調用請求完成的時候,被賦予已經發送的數據的長度。

請注意,因爲我們是在向已經連接的socket發送數據,所以我們並不需要指定目標主機地址。對於已經連接的socket來說,在socket打開的時候,遠程主機地址就已經被指定好了。

我們目前所提供的函數,可能對於應用程序的開發者來說還是有些過於複雜,並且更深入一些。

對於下面提供的Write函數來說,所有的標誌標示符都被去除,他們將使用默認值0。另外TSockXfrLength也被去除了,這樣的話,發送函數就僅僅從發送數據內容的字符串中獲得發送數據的長度了。

    void Write(const TDesC8& aDesc, TRequestStatus& aStatus);

除了上面說到的兩個不同點之外,其它部分都是和Send()函數幾乎沒有差別的。

注意,這裏提到的發送數據的函數,都僅僅適用於已經連接的socket。

總結

本文提供了一些Symbian OS的socket服務編寫說明,以及如何將通信功能加入到應用程序中。

Socket服務組件通過兩個主類RSocketServ和RSocket,提供了一個近乎標準Socket API的接口。 RSocketServ是連接到sockets服務的回話進程,而RSocket是連接到sockets服務的子會話。通過這兩個類,你可以實現面向連接或者無連接的socket。主機解析服務可以通過RHostResolver類來完成。

Socket服務組件的設計是基於協議模塊的,不同的插件模塊實現了在Socket通信中的不同協議的細節部分。這種設計可以使 Socket服務組件可以支持未來的通信協議,而並不對服務組件進行升級。到Symbian OS 6.0爲止,被支持的協議包括 TCP/IP(網絡控制協議和互聯網協議), IrDA(紅外), SMS(短信) and Bluetooth? (藍牙無線技術).

致謝: 本文是於2005年從www.symbian.com的開發文章部分引用於此的, 本文的作者是Gavin Meiklejohn。目前指向這篇文章的鏈接已經不再有效,所以此處再次發佈這篇十分有價值的文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章