使用Socket API(下)
2009年11月30日
原帖链接http://blog.sina.com.cn/s/blog_59fd7a6f0100fs8z.html
域名服务(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® (蓝牙无线技术).
2009年11月30日
原帖链接http://blog.sina.com.cn/s/blog_59fd7a6f0100fs8z.html
域名服务(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® (蓝牙无线技术).