×
Namespaces

Variants
Actions

使用Socket API

From Nokia Developer Wiki
Jump to: navigation, search
Article Metadata

文章
翻译:
kcomex
最后由 hamishwillee 在 06 Aug 2012 编辑
翻译说明
本文翻译自英文Wiki部分的Using the sockets API,因为原文较长,所以我将一部分一部分地翻译,同时欢迎大家帮助我一起完成这个任务,并且对我已经翻译的内容作修改和指正。

Contents

简介

通过这篇文章我们想为大家带来一些Symbian操作系统的有关sockets API的基本介绍。 本文的读者应该是希望在他们的应用程序中增添socket通信功能的Symbian操作系统的开发者,本文不仅提供了理论介绍,同样给出了可供实践参考的代码范例。

本文包含的内容有:

  • 概括介绍了有关socket通信的有关组件。
  • 概括介绍了socket服务架构以及使用两个主要API类RSocketServRSocket的使用。
  • 讨论了创建两个终端之间进行通信的过程。
  • 讨论了socket之间通信的不同模式:基于一串数据流的模式以及基于离散消息的模式。
  • 一个如何使用活动对象来进行socket连接的实践范例。

有关Socket的服务构架

本文的一个内容是介绍给大家如何将基于Socket服务的通信功能加入到应用程序中来。尽管如此,计算机通信系统乃是一个十分复杂的系统,本文介绍的基于socket服务的通信仍然是在一个相对比较高级的层次,没有深入底层探讨的话题和技术。要想让socket服务来发挥作用,许多底层支持软件将是必须的。

下图说明了socket服务组件在Symbian系统的通信子系统中的哪一层位置,扮演如何一个角色。

CommsComponents CN.gif

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。目前指向这篇文章的链接已经不再有效,所以此处再次发布这篇十分有价值的文章。

This page was last modified on 6 August 2012, at 10:09.
1036 page views in the last 30 days.