- 翻譯說明
- 本文翻譯自英文Wiki部分的Using the sockets API,因為原文較長,所以我將一部分一部分地翻譯,同時歡迎大家?guī)椭乙黄鹜瓿蛇@個任務(wù),并且對我已經(jīng)翻譯的內(nèi)容作修改和指正。
簡介
通過這篇文章我們想為大家?guī)硪恍㏒ymbian操作系統(tǒng)的有關(guān)sockets API的基本介紹。 本文的讀者應(yīng)該是希望在他們的應(yīng)用程序中增添socket通信功能的Symbian操作系統(tǒng)的開發(fā)者,本文不僅提供了理論介紹,同樣給出了可供實踐參考的代碼范例。
本文包含的內(nèi)容有:
- 概括介紹了有關(guān)socket通信的有關(guān)組件。
- 概括介紹了socket服務(wù)架構(gòu)以及使用兩個主要API類RSocketServ和RSocket的使用。
- 討論了創(chuàng)建兩個終端之間進行通信的過程。
- 討論了socket之間通信的不同模式:基于一串數(shù)據(jù)流的模式以及基于離散消息的模式。
- 一個如何使用活動對象來進行socket連接的實踐范例。
有關(guān)Socket的服務(wù)構(gòu)架
本文的一個內(nèi)容是介紹給大家如何將基于Socket服務(wù)的通信功能加入到應(yīng)用程序中來。盡管如此,計算機通信系統(tǒng)乃是一個十分復(fù)雜的系統(tǒng),本文介紹的基于socket服務(wù)的通信仍然是在一個相對比較高級的層次,沒有深入底層探討的話題和技術(shù)。要想讓socket服務(wù)來發(fā)揮作用,許多底層支持軟件將是必須的。
下圖說明了socket服務(wù)組件在Symbian系統(tǒng)的通信子系統(tǒng)中的哪一層位置,扮演如何一個角色。
Symbian OS通信系統(tǒng)組件
首先我們來考慮傳輸層協(xié)議。上圖的Internet互聯(lián)網(wǎng)協(xié)議和紅外協(xié)議,從Symbian 6.0之后支持的藍牙®無線通訊技術(shù)以及都在這一層中。
當我們談到Internet協(xié)議時,我們其實包括了一個隱式的依賴動作,那就是向ISP(互聯(lián)網(wǎng)服務(wù)提供商)進行撥號連接。因此,如圖所示我們可以看到Symbian系統(tǒng)提供了撥號網(wǎng)絡(luò)接入組件。而在本圖中,最重要的的系統(tǒng)組件是電話通信服務(wù)組件。
最終,我們需要設(shè)計到一個硬件設(shè)備,有了硬件我們才可以在選定的網(wǎng)絡(luò)環(huán)境中接收和發(fā)送數(shù)據(jù)。上圖的核心部分就是使用Internet協(xié)議的撥號接入網(wǎng)絡(luò),并且顯示了串行通信組件在整個通信系統(tǒng)扮演了如何的角色。串行通信服務(wù)組件通過特定的硬件設(shè)備驅(qū)動,完成了硬件設(shè)備與它周圍環(huán)境的通信。
什么是socket?
那么什么是socket呢? 用一句引自伯克利(Berkeley)UNIX關(guān)于socket實現(xiàn)的經(jīng)典定義來回答就是“socket就是通信終端”。
那究竟是什么意思呢?
一個socket代表了一條通信‘通道’邏輯上的終端。而實際上講,socket是物理網(wǎng)絡(luò)地址和邏輯端口號的一個集合,而這個集合可以向另外一個位置的與他具有相同定義的socket進行數(shù)據(jù)傳輸。
因為socket是由機器地址和端口號來區(qū)分/識別的,那么在一個特定的計算機網(wǎng)絡(luò)上,每一個socket都是以此方式被唯一識別的。這就使得應(yīng)用程序可以唯一地去定位網(wǎng)絡(luò)上的另外一個位置的socket。
注意:對于同一臺機器上的兩個socket,他們是完全具備彼此間進行通信的可能的;在這種情況下,兩個socket具有相同的主機地址,但是他們擁有不同的端口號。
主機地址和端口號的組合,對于不同協(xié)議是不同的。在socket的經(jīng)典應(yīng)用中,網(wǎng)絡(luò)通信使用的是IP(Internet Protocol)協(xié)議,但是實際上socket是支持很多其它協(xié)議的,對于這方面的信息稍后會提到。
正如我們將會看到的,不管我們選擇怎樣的通信協(xié)議(傳輸層),我們都可以使用同一種已成熟的socket API來實現(xiàn)通信。
協(xié)議模塊
如上文所述,socket的經(jīng)典應(yīng)用是在TCP/IP協(xié)議的計算機網(wǎng)絡(luò)上,使兩個邏輯端點之間展開通信活動。最著名的應(yīng)用TCP/IP的計算機網(wǎng)絡(luò),當然就是Internet了。
絕大多數(shù)socket系統(tǒng)的實現(xiàn)都限定在了TCP/IP網(wǎng)絡(luò)的通信上。
但是,Symbian系統(tǒng)的socket服務(wù)組件,就實現(xiàn)了更多的內(nèi)容;不僅如此,它還為其他組件提供了支持模塊插件協(xié)議的基礎(chǔ)構(gòu)架。這就使得Symbian公司和它的開發(fā)伙伴們大大延長了socket服務(wù)組件以及支持socket的應(yīng)用程序的應(yīng)用時間。
由于新協(xié)議和傳輸層的引入,支持了新的傳輸‘語言’或協(xié)議的協(xié)議組件,從而使得socket服務(wù)組件可以隨之適應(yīng)新的應(yīng)用環(huán)境。
隨著Symbian系統(tǒng)第五版的socket服務(wù)組件支持了TCP/IP和紅外協(xié)議的稽核。在Symbian 6.0版的時候,就增加了藍牙®無線技術(shù)和短信息服務(wù)插件。
協(xié)議模塊其實就是標準的Symbian系統(tǒng)動態(tài)鏈接庫(DLL)。他們都有共同的UID2--KUidProtocolModule(0x1000004A)來表示他們的類型,并且擁有特殊的擴展名*.PRT。
一個關(guān)于經(jīng)典系統(tǒng)的方面,就是socket服務(wù)對PLP(Psion Link Protocol)協(xié)議也是支持的。PLP被用來進行Symbian系統(tǒng)的手機和運行Microsoft Windows的臺式或筆記本計算機之間進行通信。PLP的一個應(yīng)用就是Symbian Connnect - 目前的被用于名為‘PsiWin’的Psion計算機。
socket服務(wù)組件可以以兩種方式加載協(xié)議模塊:
- 最通常的做法就是,協(xié)議模塊會在第一個使用該協(xié)議的socket被打開的時候進行加載。
- 另外一種做法是,應(yīng)用程序可以顯式地加載協(xié)議模塊。這種做法的一個好處就在于,當協(xié)議加載需要一個比較長的時間的時候,應(yīng)用程序或用戶可以得到相應(yīng)的提示。使用這種方法調(diào)用的API在本文的后面將會進行討論。
要說明的幾點:一個協(xié)議模塊可以包含多種協(xié)議實現(xiàn)。比如,在TCPIP.PRT模塊中,就包含了UDP、TCP、ICMP、IP以及DNS協(xié)議的實現(xiàn)。單個協(xié)議的實現(xiàn)可以通過位于\system\data\.的.esk文件進行映射。而每個協(xié)議模塊都有一個.esk文件來指定該模塊所包含的協(xié)議,以及每個協(xié)議在插件模塊中所處的索引位置。
傳輸?shù)莫毩⑿?
上文已經(jīng)提到,socket服務(wù)組件的插件架構(gòu)特性可以使得新的協(xié)議模塊在任何時間被安裝到一部Symbian系統(tǒng)的手機當中。
這個架構(gòu)可以使得socket服務(wù)組件來實現(xiàn)獨立傳輸層的概念。借助于提供一個通用的核心socket API接口,這種架構(gòu)就可以處理所有一般性數(shù)據(jù)傳輸系統(tǒng)的需求,并且通過添加特定協(xié)議的協(xié)議模塊,socket服務(wù)組件就可以被廣大應(yīng)用程序開發(fā)者來給自己的產(chǎn)品增添通信功能,從而省下了大量的開發(fā)通信子系統(tǒng)的時間。
隨著時間的發(fā)展,新的協(xié)議逐步登上歷史舞臺,協(xié)議模塊都將會為了適應(yīng)socket接口而被重寫。而應(yīng)用程序開發(fā)者,他們只需要增添協(xié)議新近引入的屬性或者動作,來支持新的協(xié)議即可,Socket服務(wù)組件便會使用新的協(xié)議,借助操作系統(tǒng)底層的通信組件,來完成通信機制,而并不會影響到上層應(yīng)用程序開發(fā)者的接口和開發(fā)。
總而言之,socket服務(wù)組件可以讓應(yīng)用程序開發(fā)者在僅僅維護一套核心API接口的情況下,可以借助操作系統(tǒng)的通信子系統(tǒng)來使用多個協(xié)議,從而減少了自己的開發(fā)工作量以及開發(fā)時間。
“客戶端-服務(wù)器”接口
Symbian系統(tǒng)的一個特點就是它具有一個體積很小的微內(nèi)核(micro-kernel),因此我們只能把必須和硬件設(shè)備交互以及進行主機控制的核心服務(wù)放在內(nèi)核端運行。而另外許許多多的系統(tǒng)服務(wù)只能以用戶模式的服務(wù)器線程的形式運行,通常被稱為‘系統(tǒng)服務(wù)器’。
socket服務(wù)組件就是這些‘系統(tǒng)服務(wù)器’中的一個,第三方應(yīng)用程序就借助公開的客戶端API,通過該組件完成通信功能。其中最重要的四個類為:
- RSocketServer: 這個類是用來建立和socket服務(wù)組件之間的連接以及獲取必要的資源的。在客戶端-服務(wù)器架構(gòu)的定義中,該類表示了應(yīng)用程序與socket服務(wù)組件之間建立連接的會話。所有的其他客戶端接口類,在使用中都需要一個被打開的本類的實例來進行操作。
- RSocket: 這個類表示了一個socket連接。一個標準的應(yīng)用程序可能會在不同時間的時候,擁有若干個RSocket的實例在同時進行操作。
- RHostResolver: 這個類用來提供主機名稱解析服務(wù)的接口。
- RNetDatabase: 這個類用來提供網(wǎng)絡(luò)數(shù)據(jù)庫訪問的接口。
RSocket, RHostResolver & RNetDatabase 均表示了一個給定的應(yīng)用程序與socket服務(wù)組件之間進行的會話下的子會話,而應(yīng)用程序與socket服務(wù)組件之間的會話就是一個RSocketServer的實例。
sockets服務(wù)器的主要類
socket服務(wù)組件提供了兩個主類,供他的客戶端訪問內(nèi)部的API。
- RSocketServ: 在每個應(yīng)用程序線程中,只要需要連接socket請求,他就必須使用一個本類的實例,來為其他連接(會話)提供socket服務(wù)。
- RSocket: 每一個需要使用socket的應(yīng)用程序線程,同樣也需要一個或多個RSocket對象,這些對象就是子會話了。
下面的兩個部分將會介紹會話和子會話類(RSocketServ 和 RSocket)的詳細內(nèi)容。
使用RSocketServ類
RSocketServ類扮演了一個十分重要的角色,因為它是客戶端應(yīng)用程序與socket服務(wù)組建之間的連接會話。
但是,客戶端應(yīng)用程序并不直接使用這個類來進行數(shù)據(jù)的發(fā)送和接收,或者創(chuàng)建一個遠程通信端點;要完成這些任務(wù)的話,使用的是RSocket類,這個類將會在稍后進行介紹。
RSocketServ可以讓客戶端應(yīng)用程序來向socket服務(wù)組件發(fā)起一些查詢,查詢的內(nèi)容包括服務(wù)器支持的協(xié)議個數(shù)以及支持哪些協(xié)議,每個支持協(xié)議的具體信息等等。
希望使用socket的客戶端應(yīng)用程序,都將需要自己創(chuàng)建一個RSocketServ類的實例對象,用這個對象來表示該客戶端應(yīng)用程序和socket服務(wù)之間的會話。每一個獨立的socket連接,都是一個獨立的RSocket類的實例對象。可以說,在一個客戶端應(yīng)用程序中,該程序的RSocketServ類對象就是所有的RSocket類對象的容器。
RSocketServ類的兩個常用函數(shù)就是Connect()和StandardProtocol()。
建立一個連接到sockets服務(wù)的會話
使用Connect()方法,應(yīng)用程序就可以建立與socket服務(wù)之間的一個會話。它僅僅使用一個參數(shù)--該會話所提供的消息通道的個數(shù)。
TInt Connect (TUint aMessageSlots);
消息數(shù)參數(shù)被用來限定應(yīng)用程序向socket服務(wù)所同時并發(fā)的異步操作的請求通道數(shù)。每一個同步請求都將占用一個消息通道,并且請求準備中的異步操作也將占用一個消息通道。
一個普通socket進行的讀寫通信操作,都是異步進行的,也就是說這樣的操作要占用兩個消息通道。如果socket也可以進行同步操作的話,那么我們其實并不需要指定過多的消息通道,因為同步操作的消息通道是由socket客戶端-服務(wù)器框架來完成的。對于你的應(yīng)用程序在同一個時間內(nèi)會使用到多少個消息通道,這完全是由你來斷定的,而在大多數(shù)情況下,我們要盡可能的減少同時請求的消息通道數(shù)。
如果我們不指定任何特定的值,那么系統(tǒng)會使用一個默認值作為消息通道個數(shù)的參數(shù):KESockDefaultMessageSlots (0x08)。
預(yù)載入?yún)f(xié)議模塊
socket服務(wù)組件載入?yún)f(xié)議協(xié)議模塊的動作是動態(tài)進行的,當針對某一個協(xié)議的第一個socket被創(chuàng)建的時候,該協(xié)議模塊在此時才會被載入。盡管如此,載入?yún)f(xié)議仍然是一件比較費時的操作,RSocketServ提供了一個StartProtocol()函數(shù),來進行協(xié)議模塊的預(yù)載入操作,調(diào)用該函數(shù)可以在socket連接請求的時候節(jié)省載入?yún)f(xié)議模塊的時間。
如果你的應(yīng)用程序需要在程序啟動之初就載入?yún)f(xié)議模塊,而并非需要連接的時候才進行載入,那么可以使用下面的函數(shù)范例來調(diào)用StartProtocol()方法:
void StartProtocol (TUint aFamily, TUint aSockType,
TUint aProtocol, TRequestStatus& aStatus);
StartProtocol()函數(shù)的參數(shù)有:協(xié)議族(例如,KAfInet),使用該協(xié)議的socket類型(例如,KSockStream),協(xié)議族中的協(xié)議標示(例如,KProtocolInetTcp),最后一個參數(shù)是異步調(diào)用的完成狀態(tài)參數(shù)。這些參數(shù)的意義將會在下面做以簡短介紹。
請注意,盡管StartProtocol()函數(shù)是一個異步服務(wù),但是它卻是一個在操作過程中不能被取消的操作。
使用RSocket類
RSocket代表了應(yīng)用程序的一個socket連接,在一個應(yīng)用程序中,每一個socket連接都是一個單獨的RSocket的實例。事實上,客戶端應(yīng)用程序的代碼中使用更多的是RSocket類而并不是RSocketServ類。
RSocket是一個提供了許許多多服務(wù)的體積龐大的類,這些服務(wù)包括:
- 連接到服務(wù),無論作為客戶端還是服務(wù)端
- 設(shè)置或者查詢自己的地址,或者查詢遠程地址
- 從socket讀取數(shù)據(jù)
- 向socket寫入數(shù)據(jù)
- 其他更多...
在打開任何socket之前,我們必須有一個激活了的RSocketServ會話。并且,在上述提到的任何服務(wù)進行操作之前,我們要確保socket是打開的。作為打開一個socket的一部分,RSocket這個子回話對象(見上文說明)需要同一個socket服務(wù)器進行連接,這個服務(wù)器就是一個RScoketServ類的實例。
下面的章節(jié)介紹了RSocket的各種函數(shù),有了這些函數(shù)的介紹和幫助我們就可以寫出基于socket通信的應(yīng)用程序來。
主機解析服務(wù)
什么是主機解析?
在一個由計算機組成的網(wǎng)絡(luò)里,獨立的主機使用不同的地址格式來判斷各自是誰,是什么。
例如,你的電子郵件有可能保存在一臺主機當中,這臺主機可能有一個可讀的地址,比如pop3.freeserve.net。這個地址盡管對人來說是可讀的、是一個具有一定意義的地址,但是對于網(wǎng)絡(luò)上的計算機來說,并沒有任何直接的用處。
當你的郵件客戶端程序嘗試下載你可能會收到的電子郵件的時候,你的電腦就會使用你的電子郵件服務(wù)器的地址(先前舉例的pop3.freeserve.net)去進行查詢,將他們相對應(yīng)的數(shù)字網(wǎng)絡(luò)地址查詢出來。當獲得了機器可讀的數(shù)字網(wǎng)絡(luò)地址,應(yīng)用程序才可能建立起連接。在TCP/IP協(xié)議族中,地址解析轉(zhuǎn)換是由域名解析服務(wù)(Domain Name Service, DNS)進行的。
地址解析服務(wù)的用處有兩個。首先,它可以讓計算機網(wǎng)絡(luò)(在本例中指的是Internet)的用戶可以使用一個直接的、有意義的、人們可以理解并且可以記住的的地址來指向某一個網(wǎng)絡(luò)資源。也許你曾經(jīng)見過這樣的網(wǎng)絡(luò)地址212.134.93.203、204.71.202.160,但是一般情況下也許你并不會使用這樣的數(shù)字地址去訪問網(wǎng)絡(luò),一般情況下你更多使用的是例如www.symbian.com或者www.yahoo.com這樣的地址。
其次,這種將網(wǎng)絡(luò)物理地址和用戶記憶的網(wǎng)絡(luò)資源地址進行分割的服務(wù),達到了網(wǎng)絡(luò)硬件層進行升級或者替換的情況下并不會影響到用戶訪問的目的。這種機制也從另外一種情況下幫助了大的網(wǎng)絡(luò)服務(wù)提供商,比如微軟公司的Hotmail服務(wù),使這些運營商可以在世界各地部署本地服務(wù)器,從而讓每一個用戶獲得更快的訪問速度,無論用戶是在西雅圖或者別的任何地方。
使用RHostResolver類
作為客戶端API的一部分,socket服務(wù)組件提供了RHostResolver類,用這個類我們可以獲得一個通用的主機地址解析服務(wù),這項服務(wù)的內(nèi)部會自己處理相應(yīng)不同協(xié)議的主機地址解析的細節(jié)問題。如果我們針對TCP/IP協(xié)議族而言,那么RHostResolver類扮演的就是客戶端與域名解析服務(wù)(DNS)之間進行通信的服務(wù)角色。
每一個不同的協(xié)議,都提供了自己的主機解析服務(wù),這些服務(wù)是作為協(xié)議模塊的一個標準部分實現(xiàn)的。這樣的設(shè)計就使得客戶端可以僅僅訪問RHostResolver類,而并不需要關(guān)心socket使用的是哪一種協(xié)議。
RHostResolver接口提供了如下幾種功能供客戶端應(yīng)用程序訪問,他們是:
- 將一個數(shù)字網(wǎng)絡(luò)地址轉(zhuǎn)換為人所能識別的包含一定意義的文本表現(xiàn)形式
- 將人讀地址轉(zhuǎn)換為相對應(yīng)的機讀數(shù)字地址
- 讀取或者設(shè)置本地設(shè)備的主機名的方法/函數(shù)
就像是RSocket一樣,RHostResolver類繼承自RSubSessionBase。因此,要想使用RHostResolver類,客戶端應(yīng)用程序就必須先進行對socket服務(wù)組件的服務(wù)器的連接,這個服務(wù)組件的服務(wù)器就是一個RSocketServ類的實例。
RHostResolver類提供了許多主機地址解析服務(wù)的函數(shù)/方法,每一個函數(shù)都提供了兩個版本的多態(tài)函數(shù)--同步和異步操作。
請注意,因為這是一個通用的主機地址解析接口,但是并不是所有的協(xié)議都提供了主機地址解析服務(wù),所以有些協(xié)議可能并沒有提供任何主機地址解析服務(wù)。
如果客戶端應(yīng)用程序嘗試使用RHostResolver中的函數(shù)去對一個不支持主機地址解析服務(wù)的協(xié)議請求主機地址解析服務(wù),那么將會得到錯誤代碼KErrNotSupported。
在進行任何主機地址解析服務(wù)之前,我們要打開一個RHostResolver類的實例。正如前面所提到過的,因為主機解析服務(wù)類是一個子會話類,所以在調(diào)用RHostResolver::Open()函數(shù)之前,該子會話類必須關(guān)聯(lián)一個socket服務(wù)組件的服務(wù)器會話對象實例。
TInt Open(RSocketServ& aSocketServer, TUint anAddrFamily,
TUint aProtocol);
下一步,我們將會根據(jù)上面所示的函數(shù)原形,制定我們希望用哪個地址類型來解析的主機地址,地址類型應(yīng)該是和傳遞給RSocket::Open()函數(shù)的參數(shù)一致的。
最后,我們還需要指定一個協(xié)議來進行主機地址解析服務(wù)。如果之前選擇的地址類型是協(xié)議無關(guān)類型的,那么我們可以在這里指定KUndefinedProtocol。
其他的RHostResolver類提供的函數(shù)如下所示:
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);
這些函數(shù)中的大多數(shù)都是可以見名知意的;不過Next()函數(shù)例外,我們來進行一些解釋:對于有些協(xié)議來說,GetByName()和GetByAddress()函數(shù)可能會一次找到不止一個結(jié)果,比如地址假名被允許的時候。如果這樣的話,我們就需要調(diào)用Next()函數(shù)來返回下一個地址結(jié)果。
域名服務(wù)(DNS)
域名解析服務(wù)(Domain Name Service,DNS)是TCP/IP協(xié)議所提供的主機解析服務(wù)。
一個標準的DNS查詢一般由以下三個步驟組成:
- 一個在某一個網(wǎng)絡(luò)硬件設(shè)備(例如一塊以太網(wǎng)卡)設(shè)備上運行的客戶端應(yīng)用程序,將自己的查詢主機請求發(fā)送給網(wǎng)絡(luò)上的另外一臺主機--DNS服務(wù)器。
- DNS服務(wù)器將查詢請求進行查詢,查詢是在龐大的數(shù)字地址與主機名稱對應(yīng)列表中進行的,查詢到的結(jié)果將會被轉(zhuǎn)換成不同的地址格式。
- DNS服務(wù)器將地址發(fā)送回客戶端。
請注意,DNS服務(wù)可以將文本格式的地址(例如www.symbian.com)解析為數(shù)值格式地址(例如212.134.93.203),或者將數(shù)值地址(204.71.202.160)解析為文本格式的地址——www.yahoo.com。
互聯(lián)網(wǎng)服務(wù)提供商一般都提供了很多DNS服務(wù)器(一般都不只一臺)來供他們的客戶使用。如果沒有這些服務(wù)器,那么使用互聯(lián)網(wǎng)對于普通用戶來說將是一場災(zāi)難。如果沒有DNS的話我們將不得不記住我們感興趣的web站點的32位數(shù)字地址,或者使用十分十分冗長難記的地址去給其他人發(fā)電子郵件。
這里我們需要注意的重要一點是,實際上地址轉(zhuǎn)換這項工作并不是客戶端設(shè)備進行的,而是待轉(zhuǎn)換地址被發(fā)送到了另外一臺主機,由另外一臺主機進行的解析。所以我們在建立一個使用TPC/IP協(xié)議建立連接的時候,就必須提供一個DNS服務(wù)器地址,否則一切連接將幾乎無法進行。
在socket代碼中使用活動對象(active objects)
計算機網(wǎng)絡(luò)通信,在一般情況下都是使用異步操作的。下面我們先放下談?wù)撘丫玫膕ocket通信系統(tǒng),來看看一個打電話過程是如何進行的,這樣會有助于我們理解下面要討論的問題。
當一個朋友給你打電話,你的電話機會收到電話打入的電信號,它在收到這個信號后就開始振鈴,然后你聽到了鈴聲之后就拿起聽筒,開始進行通話,直到掛斷電話此次通話結(jié)束。
當?shù)却娫捄艚械臅r候,我們可以進行其他任何事情,并不會對我們的生活造成影響。與此的,假如你的朋友給你發(fā)送了一個是十分困難的問題讓你幫助解決,也許這是一個相當大的難題,你要花一些時間來考慮或者解決,當這個時候,你的朋友可以利用你考慮或者解決的時間,進行他自己的其他活動。
上面的電話通信例子,就是一個很好的一部通信系統(tǒng)的例子。
當我們使用socket來在兩臺計算機之間傳輸數(shù)據(jù)的時候,我們看到的是一個類似上面打電話例子的異步模型。
在一個使用socket進行網(wǎng)絡(luò)通信的應(yīng)用程序中,上述異步通信的事件包括:
- 連接, 斷開連接以及確認請求連接的要求
- 接受數(shù)據(jù)(因為我們并不知道有多少數(shù)據(jù)要發(fā)送過來,所以這個過程是異步的)
- 發(fā)出數(shù)據(jù)(因為對于應(yīng)用程序?qū)觼碚f,我們并不知道底層的硬件需要多長時間才能夠?qū)?shù)據(jù)發(fā)出,所以這個過程也是異步的)
- 其他,比如載入?yún)f(xié)議模塊之類的,看似并不是十分明顯的異步操作
因為我們需要在應(yīng)用程序中處理這些異步事件,所以我們需要用到Symbian OS的活動對象(Active objects, AOs)來解決這些問題。
活動對象的特點有:
- 使得應(yīng)用程序開發(fā)者可以很容易的控制對象的生存周期
- 在一個單線程程序中完成并非嚴格意義上的多任務(wù)操作
- 為Symbian系統(tǒng)提供了效率較高的單線程多任務(wù)解決方案,而并不是真正地使用多線程。
在Symbian系統(tǒng)中,所有的線程都是通過一個或者多個活動對象,使用一個激活的進度管理器來進行高效率的外部事件處理。
一個活動對象,在一個時間內(nèi)只能處理一個事件源。在實際情況中,活動對象通常也都是被設(shè)計為處理一類特定事件的。
在稍后的代碼示例中,這些代碼因為有不同的需求所以使用了不止一個活動對象,無論是客戶端還是服務(wù)器程序,都使用了不止三個活動對象。其中一個用來處理連接機制,一個用來接收數(shù)據(jù),另外一個用來發(fā)送數(shù)據(jù)。
下面我們就來看看如合利用活動對象來處理客戶端和服務(wù)器之間進行socket流式連接的范例。
代碼示例: 連接sockets
下面一部分就是借助代碼的演示來向大家說明如何利用活動對象進行socket連接。這寫代碼段是從一個進行監(jiān)聽接入連接的‘服務(wù)器’和發(fā)送連接請求到服務(wù)器的‘客戶端’程序中提取出來的。
服務(wù)‘監(jiān)聽’類的定義
下面的代碼是從一個完整的進行‘監(jiān)聽’(listening)的服務(wù)器類定義中取出的一部分。
class CModel : public CActive {
public:
void StartEngineL(void);
private:
void RunL(void);
void DoCancel (void);
private:
RSocketServ iSession;
RSocket iListen, iSocket;
CRx* iRxAO; // 用于接收數(shù)據(jù)的活動對象
CTx* iTxAO; // 用于發(fā)送數(shù)據(jù)的活動對象
};
請注意,在成員變量中有兩個socket,一個是用來監(jiān)聽和連接的,而另外一個是用來處理和客戶端之間進行數(shù)據(jù)的傳輸?shù)摹?
在這個類的定義中,還有兩個活動對象,他們是iRxAO和iTxAO。這兩個活動對象用來在連接到服務(wù)之后異步地、分別地處理數(shù)據(jù)的發(fā)送和接收工作。
(對上面已經(jīng)定義的類而言,這個類僅僅接收一個客戶端連接,那么請你不要對自己的創(chuàng)造力作任何限制地去想象和學習一下吧,你可以以這個類定義為基礎(chǔ),將他擴展為接收多個客戶端連接的服務(wù)器吧!)
下面我們來看看連接過程是如何實現(xiàn)的。
做好接收客戶端連接的準備
首先,在我們的服務(wù)器沒有進行服務(wù)接入請求之前,我們要先創(chuàng)建兩個socket,創(chuàng)建方法如下所示:
// 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,他扮演的就是‘監(jiān)聽者’的角色,用來監(jiān)聽是否有來自客戶端的接入請求。iListen是一個和協(xié)議流關(guān)聯(lián)的對象,在本例中這個協(xié)議就是TCP協(xié)議,因為我們使用的是Internet地址格式。
另外一個socket,叫做iSocket,在現(xiàn)在是被構(gòu)造為空socket的,它僅僅在客戶端連接請求的時候才會被準備好進入工作狀態(tài)。這個socket就是用來處理來自客戶端的任何請求,并且進行數(shù)據(jù)傳輸工作的。
那么下面,監(jiān)聽socket就可以去進行監(jiān)聽客戶端連接請求的工作了。
請注意上面例子中使用的兩個不同的RSocket::Open()函數(shù)的多態(tài)。
其中第一個,用在iListen成員變量的,它是用來進行客戶端請求連接監(jiān)聽的,所以它需要一個本地地址,只有這樣連接數(shù)據(jù)才能本正確地路由到該對象。
要設(shè)定本地地址,我們需要將一個地址和一個socket進行綁定(bind)操作:
// Bind the listening socket to the required
// port.
TInetAddr anyAddrOnPort(KInetAddrAny, KTestPort);
iListen.Bind(anyAddrOnPort);
在本例中,我們并沒有過多考慮socket的網(wǎng)絡(luò)地址,因為我們使用的是易于操作的主機地址名稱。盡管如此,我們還是需要指定端口號,這樣才能完整確定一個綁定地址。
這個時候,客戶端就可以通過我們的主機的Internet主機地址和端口號(事先在程序中用#define宏定義好了的KTextPort)向我們的主機(服務(wù)器)發(fā)送請求了。不過有一點,如果我們不向客戶端告知我們的主機名稱和端口號,那么客戶端將永遠無法訪問到我們的服務(wù)器。
還要注意,因為我們的socket是使用Internet地址格式協(xié)議族進行打開操作的,所以我們調(diào)用Bind()函數(shù)時送入的函數(shù)參數(shù)TSockAddr就是一個TInetAddr類型的一個實例。
在TInetAddr類中,它除了保存TSockAddr中定義的一般性數(shù)據(jù)值外,還保存了一個TUint32類型的IP地址數(shù)據(jù)。在協(xié)議族屬性中,TInetAddr類提供的永遠是KAfInet值,因為該值表示這個地址是一個TCP/IP地址。
當完成了socket的建立,綁定了監(jiān)聽socket,我們就幾乎完成了所有準備工作,可以相應(yīng)來自任何客戶端的連接請求。
下面我們就是需要把接入連接請求創(chuàng)建一個隊列,這個時候我們需要調(diào)用RScocket::Listen()函數(shù),另外還要注意我們應(yīng)該使用長度為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();
...
}
最后,我們調(diào)用異步函數(shù)RSocket::Accept()來準備接收客戶端連接請求。
那么我們再來回顧一下繼承自活動對象CActive類的CModel類,當一個客戶端連接到我們定義的服務(wù)器類的時候,CModel::RunL()函數(shù)將會被調(diào)用。
該函數(shù)被調(diào)用后的過程,請看下一部分。
處理連接請求
當一個客戶端連接請求被收到的時候,最前線的RSocket::Accept()函數(shù)執(zhí)行請求完成,然后活動對象的RunL()函數(shù)將會被調(diào)用,這一切步驟都是因為CModel類是一個被激活狀態(tài)的活動對象。
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
...
}
那么假設(shè)現(xiàn)在所有步驟都是正常進行,那么我們獲得的完成狀態(tài)變量就是KErrNone。在上面的范例代碼中,我們會向用戶界面層傳遞一個連接建立成功的消息,然后我們啟動活動對象,對接收到的數(shù)據(jù)進行處理,然后連接iSocket進行返回數(shù)據(jù)的準備。
因為我們進行操作的是一個異步系統(tǒng),所以現(xiàn)在因為客戶端和服務(wù)器是已經(jīng)連接的狀態(tài),那么客戶端可以在任何時間向服務(wù)器socket發(fā)送數(shù)據(jù)。所以我們需要在接收到數(shù)據(jù)之后,盡可能快地進行數(shù)據(jù)的處理。
有一點,在我們進行已連接的socket的數(shù)據(jù)發(fā)送的時候,我們并不會打開活動對象。數(shù)據(jù)僅僅會在客戶端程序或者用戶希望發(fā)送數(shù)據(jù)到客戶端的時候,才進行操作。
使用有連接的socket
回顧一下我們前面定義的CModel類,我們有一個成員變量,類型為CRx的iRxAO。
類CRx是一個繼承自CActive的類,他也是一個活動對象。
CRx類的成員函數(shù)RxL(),定義如下;這個函數(shù)向連接到我們的服務(wù)器的客戶端發(fā)出了一個一個異步請求。
void CRx::RxL ( ) //class CRx derived from CActive
{
// Issue read request
iSocket->RecvOneOrMore(iDataBuffer, 0, iStatus, iRecvLen);
SetActive();
}
函數(shù)RecvOneOrMore()將會在稍后,和其他一些讀取以及寫入socket的函數(shù)一同進行討論。
在接入數(shù)據(jù)請求完成的時候,CRx::RunL()函數(shù)將會被調(diào)用,完成后返回的內(nèi)容有完成狀態(tài)事件以及新收到的數(shù)據(jù)內(nèi)容。
那么再來回顧一下CModel類的另外一個成員變量,類型為CTx的iTxAO。
類CTx是一個繼承自CActive的類,他也是一個活動對象。
CTx類的成員函數(shù)TxL(),如下所示;他想連接到服務(wù)器的客戶端進行了一個發(fā)送數(shù)據(jù)的一部請求操作。
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()函數(shù)將會在稍后,和其他一些讀取以及寫入socket的函數(shù)一同進行討論。
當數(shù)據(jù)發(fā)送請求完成的時候,CTx::RunL()函數(shù)將會被調(diào)用,同時返回的內(nèi)容有發(fā)送操作完成的結(jié)果狀態(tài)。
傳輸數(shù)據(jù)
現(xiàn)在我們來看看兩臺網(wǎng)絡(luò)設(shè)備之間,究竟是如何利用socket來進行數(shù)據(jù)傳輸?shù)摹?
如我們以前所知,在socket通信中,數(shù)據(jù)報通信和數(shù)據(jù)流通信是兩種十分不同的通信方式。
無論我們使用的是數(shù)據(jù)報還是數(shù)據(jù)流的傳輸方式,每一個獨立的數(shù)據(jù)單元在網(wǎng)絡(luò)通信的兩端被傳輸?shù)臅r候都有可能經(jīng)過十分不同的路由路徑,因為在網(wǎng)絡(luò)通信的雙方之間總有著不計其數(shù)的子網(wǎng)絡(luò),而通信雙方對數(shù)據(jù)單元的路由方向是無法控制的。這種情況是十分普遍而且正常的,由于數(shù)據(jù)流的傳輸方式也是以數(shù)據(jù)報形式為基礎(chǔ)的,所以從這個角度來看的話,二者的路由特點是一致的。
接收數(shù)據(jù)
使用無連接的sockets
下面的函數(shù),是RSocket提供的用來接收無連接的socket的接入數(shù)據(jù)的。
void RecvFrom(TDes8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus);
void RecvFrom(TDes8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus, TSockXfrLength& aLen);
如果應(yīng)用程序使用的是無連接的socket,那么需要使用RSocket::RecvFrom()這個這個方法來讀取從另外一個遠程主機發(fā)送過來的數(shù)據(jù)。
該函數(shù)的第一個參數(shù)是一個字符串,是用來保存接收數(shù)據(jù)的。
調(diào)用該函數(shù)的程序,會在一個完整的數(shù)據(jù)報接收完成的時候,得到相應(yīng)的通知。接收數(shù)據(jù)的長度,就是接收字符串的長度。如果接收數(shù)據(jù)報的長度要比字符串的最大長度更長,那么接收數(shù)據(jù)的末尾將被截去。
該函書的第二個參數(shù)是要進行接收操作的遠程主機的地址。這個地址需要是一個根據(jù)socket打開方式定義的協(xié)議格式相匹配的地址。例如,如果打開socket的時候定義的是TCP/IP協(xié)議,那么這個地址需要是一個TInetAddr類型的變量。
我們會發(fā)現(xiàn),這個函數(shù)有兩個版本的重載,他們都進行了同樣的操作,方式也一樣。唯一不同的是,第二個函數(shù)可以將接收數(shù)據(jù)的長度,顯式地返回給調(diào)用者。
還有一點,一個單獨的socket在任何一個時間內(nèi),都只有一個狀態(tài)為等待中的接收操作。
上面的方法,只能用于無連接(數(shù)據(jù)報)類型的socket連接。
使用連接的sockets
下面的函數(shù)是RSocket提供的用來從已經(jīng)連接的socket中讀取數(shù)據(jù)的函數(shù)原形。
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);
如果應(yīng)用程序使用的是已連接的socket,那么應(yīng)該使用上面的函數(shù)來進行遠程主機的數(shù)據(jù)接收工作。
和前面的無連接socket類似,這些接收函數(shù)的第一個參數(shù),仍然是接收數(shù)據(jù)要保存的目標字符串變量。
Recv()函數(shù)會在目標字符串變量被填滿或者連接斷開的時候完成。在該函數(shù)完成調(diào)用的時候,讀取數(shù)據(jù)的長度就是字符串的長度,除非在沒有讀取任何數(shù)據(jù)連接就斷開了。
第二個Recv()函數(shù)的重載可以顯式地獲取接收數(shù)據(jù)的長度,該長度被保存在了類型為TSockXfrLength的函數(shù)參數(shù)中,這樣的話判斷接收數(shù)據(jù)長度就不必關(guān)聯(lián)接收字符串的長度了。
最后一個函數(shù)RecvOneOrMore(),與Recv()不同,這個函數(shù)是會在函數(shù)接收到任何數(shù)據(jù)之后立刻返回的。言外之意,調(diào)用RecvOneOrMore()函數(shù)會接收到1--n個字節(jié),其中n就是目標字符串的長度。同樣地,如果連接被斷開,RecvOneOrMore()函數(shù)仍然會立刻返回,并且不會返回任何數(shù)據(jù)。
雖然是已連接的socket,但是在發(fā)送過程中數(shù)據(jù)流并不一定都是物理上連續(xù)的,盡管從邏輯上看他們是流式的。所以,即便是使用已連接的socket,仍然應(yīng)用程序--socket的調(diào)用者--來進行判斷數(shù)據(jù)流的結(jié)束與否,邊界切分等工作。
注意,由于我們使用的是已連接的socket,那么我們不需要指定接收收據(jù)的socket地址,因為已連接的socket是在連接動作發(fā)生的時候就已經(jīng)指定好了傳輸目標主機地址信息了。
在這一部分的前半部分,我們介紹的各種函數(shù)都是具有比較高的復(fù)雜度的,可能對于應(yīng)用程序開發(fā)者來說并不會具有特別的吸引力。
特別地,我們可以注意到所有的函數(shù)都以一個參數(shù)TUint aFlags作為標示作用,到目前為止還沒有對他進行討論。這個參數(shù)的作用是讓應(yīng)用程序可以選擇特定協(xié)議的指定屬性,以此來設(shè)置協(xié)議接收處理數(shù)據(jù)的方式。
下面介紹的另外一個函數(shù)Read(),他將默認標示參數(shù)設(shè)置為0,并且也去掉了TSockXfrLength類型的參數(shù)。如果使用該函數(shù),那么接收數(shù)據(jù)的長度就只能通過接收目標字符串的長度來獲得了。
void Read(TDes8& aDesc, TRequestStatus& aStatus);
除了上述的兩個例外,這個Read()函數(shù)的操作效果就基本同Recv()一樣了。
注意,這個函數(shù)僅僅在已連接的socket通信中是可以使用的。
發(fā)送數(shù)據(jù)
使用未連接的sockets
下面的函數(shù)是RSocket中用來向未連接的socket發(fā)送數(shù)據(jù)的。
void SendTo(const TDesC8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus);
void SendTo(const TDesC8& aDesc, TSockAddr& anAddr, TUint flags,
TRequestStatus& aStatus, TSockXfrLength& aLen);
如果應(yīng)用程序連接的是無連接的socket,那么就要使用RSocket::SendTo()函數(shù)來向遠程主機發(fā)送數(shù)據(jù)。
這個函數(shù)中的第一個參數(shù)是包含了要發(fā)送數(shù)據(jù)內(nèi)容的字符串,而要發(fā)送內(nèi)容的長度,則是由字符串的長度決定的。
當數(shù)據(jù)發(fā)送完成的時候,調(diào)用該函數(shù)的應(yīng)用程序?qū)玫酵ㄖH绻闶褂玫氖菐в蠺SockXfrLength類型參數(shù)的函數(shù)重載,那么已發(fā)送的數(shù)據(jù)的長度,將會在完成的時候被保存在該參數(shù)中。
第二個參數(shù)包含了要發(fā)送數(shù)據(jù)的遠程主機的地址,這個地址的格式應(yīng)該符合socket被打開的時候制定的協(xié)議所支持的地址格式,比如,如果我們選擇了TCP/IP協(xié)議,那么我們就需要使用TInetAddr作為發(fā)送主機的地址。
第三個參數(shù),TUint類型的標志位,它是一個和協(xié)議相關(guān)的位標識符,定義了某些需要向協(xié)議模塊中傳遞參數(shù)的標志信息。
需要注意的是,在一個socket連接中,在任意時間最多僅有一個發(fā)送操作時處于等待狀態(tài)的。
上述介紹的函數(shù),僅僅可用于無連接的數(shù)據(jù)報socket使用。
使用連接的sockets
下面的函數(shù),是RSocket提供的用來向一個已經(jīng)連接的socket發(fā)送數(shù)據(jù)的。
void Send(const TDesC8& aDesc, TUint someFlags,
TRequestStatus& aStatus);
void Send(const TDesC8& aDesc, TUint someFlags,
TRequestStatus& aStatus, TSockXfrLength& aLen);
如果你的應(yīng)用程序使用的是已經(jīng)連接的socket,那么可以使用上面的函數(shù)來向遠程主機發(fā)送數(shù)據(jù)。
和上面類似,該函數(shù)的第一個參數(shù)是包含了要向遠程主機發(fā)送數(shù)據(jù)內(nèi)容的字符串,該字符串的長度就是要發(fā)送數(shù)據(jù)的全部長度。
Send函數(shù)會在全部數(shù)據(jù)源發(fā)送完成之后,或者連接斷開之后返回。
第二個函數(shù)Send()可以讓調(diào)用者傳遞一個TSockXfrLength類型的參數(shù)進來,以此來確定發(fā)送數(shù)據(jù)的長度,這樣的話傳輸函數(shù)就不必以發(fā)送數(shù)據(jù)的內(nèi)容的字符串長度來作為原數(shù)據(jù)的長度了。
上面兩種函數(shù)沖在,都提供了一個TUint someFlags參數(shù),該參數(shù)是用來定義和協(xié)議相關(guān)的標示位的,針對不同協(xié)議會有不同的協(xié)議標示定義。
正如前面提到的SendTo()函數(shù),上面第二個方法中的TSockXfrLength類型的參數(shù),會在異步調(diào)用請求完成的時候,被賦予已經(jīng)發(fā)送的數(shù)據(jù)的長度。
請注意,因為我們是在向已經(jīng)連接的socket發(fā)送數(shù)據(jù),所以我們并不需要指定目標主機地址。對于已經(jīng)連接的socket來說,在socket打開的時候,遠程主機地址就已經(jīng)被指定好了。
我們目前所提供的函數(shù),可能對于應(yīng)用程序的開發(fā)者來說還是有些過于復(fù)雜,并且更深入一些。
對于下面提供的Write函數(shù)來說,所有的標志標示符都被去除,他們將使用默認值0。另外TSockXfrLength也被去除了,這樣的話,發(fā)送函數(shù)就僅僅從發(fā)送數(shù)據(jù)內(nèi)容的字符串中獲得發(fā)送數(shù)據(jù)的長度了。
void Write(const TDesC8& aDesc, TRequestStatus& aStatus);
除了上面說到的兩個不同點之外,其它部分都是和Send()函數(shù)幾乎沒有差別的。
注意,這里提到的發(fā)送數(shù)據(jù)的函數(shù),都僅僅適用于已經(jīng)連接的socket。
總結(jié)
本文提供了一些Symbian OS的socket服務(wù)編寫說明,以及如何將通信功能加入到應(yīng)用程序中。
Socket服務(wù)組件通過兩個主類RSocketServ和RSocket,提供了一個近乎標準Socket API的接口。 RSocketServ是連接到sockets服務(wù)的回話進程,而RSocket是連接到sockets服務(wù)的子會話。通過這兩個類,你可以實現(xiàn)面向連接或者無連接的socket。主機解析服務(wù)可以通過RHostResolver類來完成。
Socket服務(wù)組件的設(shè)計是基于協(xié)議模塊的,不同的插件模塊實現(xiàn)了在Socket通信中的不同協(xié)議的細節(jié)部分。這種設(shè)計可以使Socket服務(wù)組件可以支持未來的通信協(xié)議,而并不對服務(wù)組件進行升級。到Symbian OS 6.0為止,被支持的協(xié)議包括 TCP/IP(網(wǎng)絡(luò)控制協(xié)議和互聯(lián)網(wǎng)協(xié)議), IrDA(紅外), SMS(短信) and Bluetooth® (藍牙無線技術(shù)).
致謝: 本文是于2005年從www.symbian.com的開發(fā)文章部分引用于此的, 本文的作者是Gavin Meiklejohn。目前指向這篇文章的鏈接已經(jīng)不再有效,所以此處再次發(fā)布這篇十分有價值的文章。